Debugging Layout Issues Using the Widget Inspector

In this article, you’ll see how layouts are constructed as well as learn about the Widget Inspector and how to use it to solve common layout errors in an app. By Ayush.

See course reviews 5 (1) · 1 Review

Download materials
Save for later
Share

In this article, you’ll see how layouts are constructed as well as learn about the Widget Inspector and how to use it to solve common layout errors in an app.

You can construct a large variety of complex layouts in Flutter. Because of this flexibility, you’ll also face UI issues that hinder app development. Dart devtool’s Widget Inspector page is a versatile tool to understand and tackle these errors. In this tutorial, you’ll learn:

  • The relationship between different widgets in a layout.
  • The Widget Inspector.
  • Common layout errors and solutions.
  • Responsive app design.
Note: This tutorial assumes a basic knowledge of Dart and Flutter along with pre-installed plugins for VS Code.

Getting Started

Click the Download Materials button at the top or bottom of this tutorial to download the starter project. The project is a basic app that displays the solar system members in the proper order. Click any of them to open its corresponding details page. The project has a simple folder structure and includes all the required images.

Build and run the app to see the output:

Image of Earth with full width

Click the planet Jupiter to navigate to its details page. Do this by scrolling the PageView down or clicking the sixth page indicator.

Detail page for Jupiter containing its facts

Feel free to explore the app. Notice the layout issues and visual inconsistencies in both pages. Don’t worry. You’ll learn about different options available to fix them in this article.

Using the Widget Inspector

Because you have seen the app’s issues, now is an excellent time to introduce the debugging tool used for Flutter, known as the Dart devtools. It’s a collection of inspection tools used to check the app’s layouts and performance. You’ll focus on the Widget Inspector page of the devtools in this article.

To open the Widget Inspector, you can either click the Dart devtools icon or use the key combination Control-Shift-P in VS Code.

Widget explorer initial look

The first column (red) displays the current state of the widget tree. Clicking any of its elements will allow you to explore two tabs in the second column (orange). The first tab (Layout Explorer in blue) shows the dimensions of the currently selected element in the widget’s tree and its parent. The second tab (Details Tree in green) shows the selected widget’s properties and all the widgets around it.

Note: The “Show Guidelines” button on the top of the Widget Inspector shows both the padding/margin used on a widget as well as its alignments.

Click the Container widget under BodySurfaceData in the widget tree. If you switch to the Details Tree, you see the Container widget actually consists of two widgets. The Padding and DecoratedBox widgets, as padding and decoration properties, are specified in the Container widget.

Detail tree for the detail page

Understanding Constraint Flow

This Container also has an interesting relation with its parent widget. If you switch to the Layout Explorer tab, you see the Container has a range below its width. The parent of the Container determines this range, which represents the minimum/maximum values for the width. Starting with the Column, you can see it has the same width range as the Container. This is true for the FutureBuilder and DecoratedBox above it as well.

Container composed of Padding and Constraints

The transfer of this minimum/maximum range is an important rule to understand when it comes to Flutter layouts. Looking at the widgets from the DecoratedBox to Column, you’ll see this rule still applies for height as well. But Column, by design, gives its children unconstrained height.

Note: A BoxConstraint class represents this combination of width and height ranges.

But why do these constraints start only from the DecoratedBox and not above it? Well, they do exist, but the constraints above DecoratedBox are a special kind known as tight constraints. These constraints have equal minimum and maximum values.

Displaying Overflowing Content

In contrast to tight constraints, loose constraints have different minimum and maximum values. Because the child of the Container is a Row widget, it also inherits these constraints for its width. Further, because Row is a child of Container, which has an unconstrained height, it will behave the same. This means it can adjust its height freely according to its contents.

Open lib/widgets/body_surface_data.dart and replace the width and height next to //TODO increase size with the following code snippet.

width: 90,
height: 90,

Build and run, and you’ll see that the Row height has auto-adjusted as the planet image size has increased.

Jupiter's size increases along with its parent

You can also observe this behavior in the Widget Inspector. View the Row containing the Jupiter illustration in the Layout Explorer and notice both the Row height and Expanded have jumped to 90.

Row expanding to fit its children

It would be better if the planet grew out of the Row boundaries. You can achieve this look in two steps. First, add further constraints to the Row height using a ConstrainedBox.

Replace //TODO add constraints with the following snippet:

constraints: const BoxConstraints(maxHeight: 70),
Note: This internally creates a ConstrainedBox you can verify using the Widget Inspector.

Hot reload, and the Row height is now constrained. It won’t adjust according to its child anymore and remains at the constant height specified in maxHeight. Layout Explorer will also reflect the effects of the constraints, with height now having maximum and minimum values.

Row height constrained

Next, apply a different set of constraints to the Container child so it can go beyond the Row boundary. You can use the OverflowBox widget for this usecase.

Replace the Expanded widget below //TODO add OverflowBox with the following code snippet:

Expanded(
 child: OverflowBox(
   maxHeight: 100,
   child: SizedBox(
     width: 100,
     height: 100,
     child: _body,
   ),
 ),
),
Note: You’ll learn about Expanded later in this article. Hold tight.

Hot reload and see that Jupiter finally goes beyond the Row boundary.

Jupiter after overgrowing the row

Making Fitting Content

OverflowBox is used primarily to render overflowing content. Sometimes you need to make sure content fits perfectly within a parent boundary as well.

Click the “Moons” card and see the grid of moons for the current planet. Most of the moon names fit the box perfectly. However, certain names, such as Philophrosyne, are too long to fit in the box and get wrapped to the following line.
Moons of Jupiter

Look at the Details Tree for the Text widget to see the reason for this wrap.

Text softwrap property

The Text widget has a softWrap property that controls how a text content behaves when it no longer fits its parent boundary. By default, softWrap is set to true, causing the excess content to be wrapped into the next line. A better alternative in this case would be to scale the name so it fits perfectly. This is exactly the usecase for FittedBox.

Note: Setting the softWrap property to false causes the excess text to clip.

Open lib/widgets/moons_grid.dart and replace the child below //TODO add FittedBox with the following code snippet:

child: FittedBox(
   child: Padding(
   padding: const EdgeInsets.all(5.0),
   child: Container(
       color: Colors.black,
       child: Text(
         _item.name,
         style: const TextStyle(fontSize: 18.0, color: Colors.white),
      ),
    ),
  ),
),

Hot reload and observe that now the name fits perfectly without wrapping, thanks to the FittedBox.

Moon names with no line wrap

Preventing Infinite Viewports

Overflowing and fitting content are aesthetic requirements that help prevent visual inconsistencies. But some issues cause serious crashes when they occur.

Click the “Pop culture” card to open the list of movies with this planet as a location. You’ll see a white bottom sheet modal.

Result of infinite viewport

The IDE logs will also report several logs, starting with the main cause of the error at the top.

Blank white screen caused by an infinite viewport

The vertical viewport in the message refers to the ListView, which is a child of a Column widget. This message becomes clearer when you recall that Column gives its children unconstrained height. In this case, its child happens to be a ListView, which extends vertically to infinity. Thus, the Flutter framework cannot render such a layout. If you could somehow provide a maximum height for the ListView, the UI would render.

One option is to use a LimitedBox, which is specifically used whenever the parent provides unconstrained dimensions for its children. It sets a maximum width or height for its child that doesn’t set its own dimensions in an unconstrained environment.

But this solution requires you to manually set a height for the ListView, which in this case needs to occupy the remainder of the bottom sheet modal. The other, more generally accepted solution is to use an Expanded widget. Expanded, when used as a child of the Column widget, occupies the maximum height that it can within its parent.

Open lib/widgets/movie_location_pageview.dart and replace the ListView below //TODO add vertical Expanded with the following code snippet:

Expanded(
  child: ListView(
    children: [
       ListTile(
         dense: true,
         title: const Text('Cast'),
         subtitle: Text(_item.cast),
       ),
       ListTile(
         dense: true,
         title: const Text('Genre'),
         subtitle: Text(_item.genre),
       ),
       ListTile(
         dense: true,
         title: const Text('Director'),
         subtitle: Text(_item.director),
       ),
       ListTile(
         title: Text(_item.synopsis),
       ),
    ],
 ),
),

Hot reload and note that the ListView now occupies the whole remaining height in its parent.

Overflow due to long title

Switch to the Layout Explorer and you can verify Expanded‘s default behavior of occupying the available space in its parent (a Column in this case).

Expanded occupying all available space of its parent column

Preventing Horizontal Infinite Viewports

The bottom sheet content has appeared. But another issue has appeared along with it. The movie title is displaying an overflow warning as well. Unlike Column, Row gives its children unconstrained width. Because the movie title exceeds the remaining width, an overflow occurs. This warning also appears in the widget tree of the Widget Inspector.

Row without Expanded showing an error

The lesson here is that infinite viewports are independent of direction and can occur anywhere.

The fix is to again wrap the Column in an Expanded widget to give it the available width of the Row. Replace the Column below //TODO add horizontal Expanded with the following snippet:

Expanded(
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    mainAxisSize: MainAxisSize.min,
    children: [
      Text(
       '${_item.name}',
       maxLines: 2,
       style: const TextStyle(fontSize: 40),
      ),
      Text(
       '${_item.year}',
       style: const TextStyle(fontSize: 20),
     ),
   ],
  ),
),

Hot reload, and you’ll finally see the list of movies containing the selected planet as a location. Neat!

Expanded used to solve infinite viewport

Using UnconstrainedBox

Unlike the usecase for OverflowBox, where it imposes its own constraints, sometimes you might need your content to have no constraints at all. This usecase commonly arises in widgets such as PageView that force their children to occupy their entire space.

Open libs/solar_system.dart and look at the solar system implementation, using a PageView. Both Layout Explorer and Details Tree in the Widget Inspector show a fixed width and height for the PageView child as well.

PageView child fixed dimensions

Hence, the framework will render all the planets and the sun at the same size.

To correct this, you need to add a widget that provides its children the freedom to choose their size. Luckily, Flutter comes with the UnconstrainedBox widget.

The UnconstrainedBox does exactly what its name implies. Even though its parent may impose a constraint on it, it allows its child to be any size it wants.

Open lib/solar_system.dart and replace the return below //TODO add UnconstrainedBox with the following

return UnconstrainedBox(
  child: GestureDetector(
    child: body,
    onTap: () {
      context.explore(path);
    },
  ),
);

Hot reload and see that Jupiter and every other heavenly body have been rendered at a proper size.

Jupiter after being unconstrained

Using LimitedBox and FractionallySizedBox

But in some scenarios, using only an UnconstrainedBox isn’t enough.

Click Jupiter and then the “Events” card.

Events page view before being unconstrained

You should see a horizontal PageView listing various major events in the planet’s history. Go deeper into the Widget Inspector and look at the widget tree for the Card. You’ll find that because Card is a PageView child, it’s forced to occupy the entire area. How about giving it some room to breathe? Wrap it in an UnconstrainedBox to grant it the freedom of defining its dimensions.

Open lib/widgets/solar_events_pageview.dart and add the following snippet below //TODO add LimitedBox

return UnconstrainedBox(
  child: _EventBody(item: _item),
);

Build and run the app to see it in action.

Events list empty

You should see the familiar white bottom sheet again. Open the Widget Inspector to see why this happens.

Unconstrained with a ListView child

One look at the Widget Inspector will tell you UnconstrainedBox has a ListView as its child. From earlier experience, you should rightly assume that wrapping the ListView inside Expanded is the way. That won’t do in this case, though, because of a limitation of the Expanded widget. Only flex widgets such as Row and Column can have Expanded widgets as children.

Another layout error occurs if a child of a non-Flex widget has an Expanded child, producing the message “Incorrect use of ParentData widget”. This error also occurs for Positioned, TableCell and Flexible, which require Stack, Table and Flex widgets, respectively.

You need a widget that sets a maximum height and width for its child in unconstrained environments. LimitedBox is the answer in this case.

Open lib/widgets/solar_events_pageview.dart and replace the child of UnconstrainedBox with the following snippet:

LimitedBox(
  //1
  maxHeight: MediaQuery.of(context).size.height / 2,
  //2
  maxWidth: MediaQuery.of(context).size.width,
  child: FractionallySizedBox(
     //3
     widthFactor: 0.75,
     child: _EventBody(item: _item),
  ),
),

Here’s what’s happening in this snippet:

  1. The maxHeight for the LimitedBox child is set to half the device height.
  2. The maxWidth for the LimitedBox child is set equal to the device width.
  3. Here, you set the widthFactor of the FractionallySizedBox. This widget takes the size given by its parent and multiplies it by the corresponding factor. The result is set as a constraint for its own child.

The usefulness of the LimitedBox widget becomes more evident after you look at its Layout Explorer blueprint, which shows that it sets maximum dimensions for its children when its parent provides it with unconstrained dimensions.

LimitedBox inside UnconstrainedBox

Implementing Responsiveness

All the fixes and fancy widgets you’ve used until now will be fruitless if you don’t handle device orientation. A well-implemented screen responsiveness solution will go far in improving your app’s UX.

Change the device orientation from portrait to landscape. The solar system pageview will remain more or less the same in both orientations. But the details page is a different story. Because Column is a non-scrollable widget, if it does not get enough height, Flutter probably will show an overflow warning. Layout Explorer itself will display a different message as well for the Column height.

Overflow in landscape orientation

Consult the Layout Explorer for a deeper dive into the issue.

Layout OK in portrait orientation

Layout error in landscape orientation

Comparing the Layout Explorer blueprint in portrait and landscape orientations, you can see the width and height have switched places more or less. But because the dimensions of the widgets remain the same and only the constraints passed down by the widget tree are now different, you get the overflow warning.

As a limited solution, widgets such as GridView allow setting the total column count. Using MediaQuery.of(context).orientation should handle the orientation change for GridView easily.

Open lib/widgets/moons_grid.dart and replace crossAxisCount below //TODO add MediaQuery flag with the following snippet (To remove the compile-time error, you might need to remove the const modifier.):

crossAxisCount:
   MediaQuery.of(context).orientation == Orientation.portrait
    ? 3
    : 6,

Hot reload and see the GridView column count change with every orientation change.

GridView column count changing dynamically on orientation change

However, OrientationBuilder is the standard widget used to handle orientation changes.

Open lib/details.dart and replace the Column below //TODO add OrientationBuilder with the following snippet:

return OrientationBuilder(builder: (_, orientation) {
  if (orientation == Orientation.portrait) {
    return _DetailPortrait(path: path, member: member);
  } else {
    return _DetailLandscape(path: path, member: member);
  }
});

Hot reload and see the details page change UI according to the device orientation.

Landscape orientation with correct UI

Where to Go From Here?

Download the final project by clicking the Download Materials button at the top or bottom of this tutorial. You have gotten an introduction to the Widget Inspector, learned the concept of constraint flow and used the following widgets to overcome specific UI issues:

  • OverflowBox: Imposes constraints different from the ones set by the parent widget.
  • FittedBox: Scales content according to the size of the parent widget.
  • Expanded: Ensures the content occupies the available space either vertically or horizontally.
  • UnconstrainedBox: Renders the content at its natural size unconstrained.
  • LimitedBox: Constrains the content even if the parent does not impose any constraints on it.
  • FractionallySizedBox: Constrains the content to a certain fraction of the parent dimension.
  • OrientationBuilder: Ensures the layout reacts properly to orientation changes.

To dive deeper, check out the following resources

  • Visit the Flutter widget catalog to learn about even more useful widgets.
  • Learn about slivers which are another important part of UI design in Flutter.
  • Dive deeper into the subject of constraint flow.
  • Study the Dart devtools in greater depth including the Widget Inspector.

If you have any questions, comments or suggestions, feel free to join the discussion below!