Constraints go down, sizes go up. Parent sets positions. We all know (should know) this rule. However, different kinds of widgets apply different constraints and react differently to incoming constraints.

If you are a member, please continue; otherwise, read the full story here.

Content

· The box layout model · Tight constraints · Loose constraints · Bounded constraints · Unbounded constraints · Infinite constraints · Constrained size/dimension/axis · Screen (MaterialApp, CupertinoApp, or WidgetApp) · SafeArea · Scaffold · Container · Center and Align · Column, Row, and Flex · ListView and SingleChildScrollView · UnconstrainedBox · ConstrainedBox · SizedBox · FittedBox

I am reading (once again) the most-read Flutter article on Medium, which has more claps than all my articles combined. The article is great, but there is some missing information that prevents me from completely understanding some of the provided examples.

Here is the quote from the article itself:

None

So, that's what I am going to do — read Flutter docs on Container, Center, Align, Row, Column, etc., and make some experiments to see how they behave depending on incoming constraints.

The first thing to read while starting learning Flutter layouts is the BoxConstraint class page. It contains necessary terminology.

The box layout model

Render objects in the Flutter framework are laid out by a one-pass layout model which walks down the render tree passing constraints, then walks back up the render tree passing concrete geometry.

For boxes, the constraints are BoxConstraints, which, as described herein, consist of four numbers: a minimum width minWidth, a maximum width maxWidth, a minimum height minHeight, and a maximum height maxHeight.

The geometry for boxes consists of a Size, which must satisfy the constraints described above.

Each RenderBox (the objects that provide the layout models for box widgets) receives BoxConstraints from its parent, then lays out each of its children, then picks a Size that satisfies the BoxConstraints.

Render objects position their children independently of laying them out. Frequently, the parent will use the children's sizes to determine their position. A child does not know its position and will not necessarily be laid out again, or repainted, if its position changes.

Tight constraints

When the minimum constraint and the maximum constraint on an axis are the same (i.e. minWidth == maxWidth), that axis is tightly constrained.

When the parent widget applies a tight constraint on its child, the child has no choice. It must be exactly the size as specified by the constraint, i.e. width == minWidth == maxWidth or height == minHeight == maxHeight.

 Widget build(BuildContext context) {
    return  Center(
      child: ConstrainedBox(
          constraints: BoxConstraints(
            minWidth: 100,
            minHeight: 100,
            maxWidth: 100,
            maxHeight: 100,
          ),
          child: Container(
            color: Colors.green,
            width: 200,
            height: 200,
          ),        
      ),
    );
  }
None

Despite the green container having its size defined as 200 x 200, the parent applied the tight constraints of 100 x 100, and the green container was forced to be 100 x 100.

The BoxConstraints class also has the tight constructor, so the above can be rewritten as:

... 
constraints: BoxConstraints.tight(Size(100, 100)), 
...

Loose constraints

An axis with a minimum constraint of 0.0 (i.e. minWidth == 0) is loose (regardless of the maximum constraint; if it is also 0.0 (maxWidth == 0), then the axis is simultaneously tight and loose!)

Purely logically, if minWidth < maxWidth, it is a loose constraint, since there is some room for the child to vary his size, but in Flutter, the loose constraint is defined as above, so I am a bit confused.

Widget build(BuildContext context) {
    return Center( 
      child: Container(
        color: Colors.red,
        width: 50,
        height: 50,
      ),
    );
  }
None

The Center takes the whole screen. It provides a loose constraint for the child (the child can be any size between 0 and screen size). The red container wants to be 50 x 50, so this is the size it gets.

In Flutter, loose means that the child is able to shrink down to 0 size.

  Widget build(BuildContext context) {
    return Center( 
      child: ColoredBox(
        color: Colors.red,        
      ),
    );
  }
None

In the above example, the Center provides a loose constraint; ColoredBox doesn't have a child, so it shrinks to 0 size.

Why did we not use the Container for the above example? Because the Container is a convenience widget that consists of many widgets and its behavior is very complicated. (Read below.) Specifically for the above example, the Container will occupy all the space instead of being shrunk to nothing.

Bounded constraints

An axis whose maximum constraint is not infinite (i.e. maxWidth != double.infinity) is bounded.

My guess: tight constraints are also bounded (cannot be unbounded), since the parent cannot tell the child to be exactly infinite size. (The opposite is not true; bounded can be tight or loose. )

Therefore, our first example of tight constraints (with a green container) can be applied here.

Unbounded constraints

An axis whose maximum constraint is infinite (i.e. maxWidth == double.infinity) is unbounded.

The unbounded constraints are also loose (cannot be tight), for the very same reason: the parent cannot force the child to be exactly infinite size. The opposite is not true: loose can be either bounded or unbounded.

So, practically instead of 4 possible combinations (tight-bounded, tight-unbounded, loose-bounded, and loose-unbounded), we have only 3: 1. tight (they are always bounded) 2. loose-bounded 3. unbounded (they are always loose)

ListView, SingleChildScrollView, and PageView have unbounded constraints for the scroll direction axis. Flex, Column, and Row pass unbounded constraints for their children on their main axis. UnconstrainedBox by default provides unbounded constraints for both axes.

Putting unbounded constraints inside unbounded constraints like this:

    return Column( 
      children: [
        ListView(
        ),
      ],
    );

or like this:

return ListView(
      children: [
        Column(
          children: [
            Expanded(child: Container(color: Colors.red, height: 100)),
          ],
        ),
      ],
    );

or this way:

   return UnconstrainedBox(
      child: ListView(
        children: [          
        ],
      ),
    );

Will result in an exception: "Vertical viewport was given unbounded height."

Infinite constraints

An axis whose minimum constraint is infinite (minWidth == double.infinity) is just said to be infinite (since by definition the maximum constraint must also be infinite in that case).

Flutter docs provide this definition, but I haven't yet found the use for it.

Constrained size/dimension/axis

A size is constrained when it satisfies a BoxConstraints description.

Flutter's docs use this definition a lot, so it is important to understand the meaning.

The constrained dimension means that the parent applies some constraints to it. For example, tight constraints (minWidth == maxWidth).

The unconstrained dimension means that the parent does not apply any constraint, and the child has complete freedom in choosing its size (minWidth == 0, maxWidth == double.infinity). Practically, in Flutter, unconstrained means loose-unbounded, and constrained means everything else.

Incoming constraints The incoming constraints are constraints applied by the parent.

Screen (MaterialApp, CupertinoApp or WidgetApp)

The screen provides tight constraints to its child. As a result, the child always takes up the entire screen.

Widget build(BuildContext context) {
    return SizedBox(
      width: 100,
      height: 100,
      child: ColoredBox(
        color: Colors.red,        
      ),
    );
  }
None

Although SizedBox gives tight constraints to its child, those constraints are ignored, and ColoredBox fills the screen.

SafeArea

The SafeArea behaves the same as the screen (applies tight constraints), with only the difference that it adds some padding around the child.

  Widget build(BuildContext context) {
    return SafeArea(
      child: SizedBox(
        width: 100,
        height: 100,
        child: ColoredBox(
          color: Colors.red,
        ),
      ),
    );
  }
None

Scaffold

Scaffold takes the whole screen (because of tight constraints applied by the screen) and applies loose-bounded constraints to its body (child) widget.

 Widget build(BuildContext context) {
    return Scaffold(
      body: SizedBox(
        width: 100,
        height: 100,
        child: ColoredBox(
          color: Colors.red,
        ),
      ),
    );
  }
None

Because incoming constraints are loose and the size of the SizedBox < screen size, the SizedBox is allowed to take its natural size 100 x 100.

Looks like the implicit alignment of Scaffold is topLeft.

Container

Layout behavior

Since Container combines a number of other widgets each with their own layout behavior, Container's layout behavior is somewhat complicated.

Summary: Container tries, in order: to honor alignment, to size itself to the child, to honor the width, height, and constraints, to expand to fit the parent, to be as small as possible.

Flutter docs rules with examples.

Rule 1

If the Container has no child, no height, no width, no constraints, and the parent provides unbounded constraints, then Container tries to size as small as possible.

I tried to accomplish the above, and the only way I succeeded was:

  Widget build(BuildContext context) {
    return UnconstrainedBox(      
     child: Container(color: Colors.red, ),
    );
  }
None

The Container width and height became 0.0; therefore, nothing appears on the screen. (If I got the things right.)

Rule 2

If the Container has no child and no alignment, but a height, width, or constraints are provided, then the Container tries to be as small as possible given the combination of those constraints and the parent's constraints.

Same as the above, but with width and height:

  Widget build(BuildContext context) {
    return UnconstrainedBox(
      child: Container(
        color: Colors.red,
        width: 100,
        height: 100,
      ),
    );
  }
None

Rule 3

If the Container has no child, no height, no width, no constraints, and no alignment, but the parent provides bounded constraints, then Container expands to fit the constraints provided by the parent.

Same as first, but Center instead of UnconstrainedBox:

Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: Colors.red,
      ),
    );
  }
None

The Center takes up the whole screen and provides bounded constraints; therefore, the Container fills the Center.

Rule 4

If the Container has an alignment, and the parent provides unbounded constraints, then the Container tries to size itself around the child.

  Widget build(BuildContext context) {
    return UnconstrainedBox(
      child: Container(
        color: Colors.red,
      //  alignment: Alignment(1, 1),
        child: Container(color: Colors.green, width: 100, height: 100,)
      ),
    );
  }
None

It works like this with and without alignment. Red container sized itself around the green one, and only the green one is displayed.

Rule 5

If the Container has an alignment, and the parent provides bounded constraints, then the Container tries to expand to fit the parent, and then positions the child within itself as per the alignment.

  Widget build(BuildContext context) {
    return Center(
      child: Container(
        alignment: Alignment(1, 1),
        color: Colors.red,
        child: Container(color: Colors.green, width: 100, height: 100,)
      ),
    );
  }
None

More examples for better understanding, with highlighting the impact of tight/loose constraints.

The Container without size takes the entire screen (screen applies tight constraints) and provides tight constraints to its child.

  Widget build(BuildContext context) {
    return Container(
      color: Colors.green,
      child: SizedBox(
        width: 100,
        height: 100,
        child: ColoredBox(
          color: Colors.red,
        ),
      ),
    );
  }
None

So, the child takes the entire screen as well.

The Center provides loose constraints, then the Container without size shrink-wraps its child.

  Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: Colors.green,
        child: SizedBox(
          width: 100,
          height: 100,
          child: ColoredBox(
            color: Colors.red,
          ),
        ),
      ),
    );
  }
None

The screen provides tight constraints, so the Container's size is ignored.

  Widget build(BuildContext context) {
    return Container(
      color: Colors.green,
      width: 200, 
      height: 200,
    );
  }
None

And the Container fills the entire screen.

The screen provides tight constraints, Container fills the screen, and provides tight constraints to its child.

  Widget build(BuildContext context) {
    return Container(
      color: Colors.green,
      width: 200, 
      height: 200,
      child: SizedBox(
        width: 100,
        height: 100,
        child: ColoredBox(
          color: Colors.red,
        ),
      ),
    );
  }
None

So, the SizedBox also fills the screen and provides tight constraints toColoredBox, which also fills the screen.

The Center provides loose constraints to the green Container, so it grows to its natural size 200 x 200. The green Container provides tight constraints to the red one, therefore, the size of the red Container is ignored, and it fills the parent.

  Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: Colors.green,
        width: 200, 
        height: 200,
        child: Container(
          width: 100,
          height: 100,
          color: Colors.red,
        ),
      ),
    );
  }
None

Center and Align

This widget will be as big as possible if its dimensions are constrained. If a dimension is unconstrained then the widget will match its child's size in that dimension.

Widget build(BuildContext context) {
    return Center(
      child: Container(
        width: 100,
        height: 100,
        color: Colors.red,
      ),
    );
  }
None

The Center takes the entire screen, because the screen provides tight constraints. It will provide loose-bounded constraints to the child. A child can be any size from 0 to the screen size, so the Container took its natural size.

Widget build(BuildContext context) {
    return ListView(
      children: [
        Center(
          child: Container(
            color: Colors.red,
            width: 100,
            height: 100,

          )
        ),
      ],
    );
  }
None

Because the ListView provides an unconstrained y dimension, the Center became 100 tall same as its child. In axis x, the constraints are loose-bounded (the child can have a width from 0 to screen width). In axis y, the constraints are loose-unbounded, ie, the dimension is unconstrained. A child can be any height it wants.

The Center is practically an Align widget with implicit alignment set to center. Therefore, in the context of layout, all the above can be applied to the Align widget as well.

Column, Row, and Flex

I will talk about Column here, but everything is applied to Row and Flex since the Row is just a horizontal Column and Flex is a widget which can be Row or Column, depending on the direction property value.

If the incoming constraints are tight, the Column will occupy all available space (even if it has no children).

  Widget build(BuildContext context) {
    return Column();
  }
None

On its cross-axis (horizontal), Column applies to the child incoming tight constraints.

Widget build(BuildContext context) {
    return Column(
      children: [
        Container(
          color: Colors.red,
          width: 500,
          height: 100,         
        )
      ],
    );
  }
None

Despite the Container having a width of 500, it cannot be wider than the screen, and becomes 390 pixels wide.

On its main axis (vertical) Column applies loose-unbounded constraints to its children, while the Column itself has a finite height (because of tight constraints applied by the parent).

  Widget build(BuildContext context) {
    return Column(
      children: [
        Container(
          color: Colors.red,
          width: 500,
          height: 1000,         
        )
      ],
    );
  }
None

The Container has height=1000, and since constraints are unbounded, it becomes 1000 pixels tall. But the Column remains 844 pixels tall, and we got an exception: A RenderFlex overflowed by 156 pixels on the bottom.

A big question: why was the Column designed like that?

The above example with a single child does not give any clue.

Let's make a simple example with many children.

  Widget build(BuildContext context) {
    return Column(
      children: _makeChildren(),
    );
  }

  List<Widget> _makeChildren() {
    List<Widget> children = [];
    for (int i = 0; i < 10; i++) {
      children.add(Container(
        color: Colors.red,
        padding: EdgeInsets.all(10),
        height: 100,
      ));
    }

    return children;
  }
None

Without exception, we wouldn't know that some widgets (children) were not rendered on the screen.

So the answer is: the Column was designed this way to inform us (developers) occasionally that some information would be lost, our designs are wrong, and should be fixed.

The above code could be fixed in two ways.

  1. Wrap every child (or some of them) in a Flexible or Expandedwidget
  List<Widget> _makeChildren() {
    List<Widget> children = [];
    for (int i = 0; i < 10; i++) {
      children.add(Flexible(  //here
        child: Container(
          color: Colors.red,
          margin: EdgeInsets.all(3),
          height: 90,
        ),
      ));
    }

    return children;
  }
None

2. Use the ListView instead of the Column to make the view scrollable.

 Widget build(BuildContext context) {
    return ListView(
      children: _makeChildren(),
    );
  }
None

As we learned before, putting any scrollable widget inside a Column like this:

  Widget build(BuildContext context) {
    return Column(
      children: [
        ListView(),
      ],
    );
  }

will cause an exception: Vertical viewport was given unbounded height.

The reason is that ListView has infinite height, and Column applies an unbounded vertical constraint to children. As a result, Flutter tries to render a widget with infinite(unbounded) height, fails, and throws an exception.

The fix is easy; we need to apply bounded constraints to the ListView. For example, using Expanded or SizedBox

  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(child: ListView()),
      ],
    );
  }

If the Column is given unbounded constraints by the parent:

    return ListView(
      children: [
        Column(
          children: [
            Container(color: Colors.red, height: 100,)
          ],
        ),
      ],
    );

Then the Column shrinks to the size of the child in that dimension.

None

If we add the Expanded to the above example:

    return ListView(
      children: [
        Column(
          children: [
            Expanded(child: Container(color: Colors.red, height: 100,))
          ],
        ),
      ],
    );

We will get an exception:

None

The exception has sufficient description and suggests a fix:

   return ListView(
      children: [
        Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Flexible(child: Container(color: Colors.red, height: 100,))
          ],
        ),
      ],
    );

We replaced Expanded with Flexible and set MainAxisSize to min.

ListView and SingleChildScrollView

Both those widgets provide unbounded constraints in the scrolling direction dimension.

   return ListView(
      children: [
        Container(
          height: 10000,
          color: Colors.red,
        )
      ],
    );

The container wants to be 10000 in height? No problem, this is what it gets.

Meanwhile, the ListView itself abides by the incoming constraints from its parent. In the above example, the ListView is 844 pixels tall (the size of the screen).

In the other axis (X) ListView passes the incoming constraints to the child, and the container becomes 390 pixels wide.

UnconstrainedBox

A widget that imposes no constraints on its child, allowing child to be rendered at its "natural" size.

This allows a child to render at the size it would render if it were alone on an infinite canvas with no constraints. This container will then attempt to adopt the same size, within the limits of its own constraints. If it ends up with a different size, it will align the child based on alignment. If the box cannot expand enough to accommodate the entire child, the child will be clipped.

In other words UnconstrainedBox provides unbounded constraints to its child.

It has a constrainedAxis property, which (if specified) retains incoming constraints in the specified dimension.

Here are two code examples that demonstrate how UnconstrainedBox changes the layout behavior of Container.

    return Scaffold(
      body: Center(
        child: Container(
          color: Colors.red,
          width: 300,
          height: 200, 
          child: Container(
            alignment: Alignment.topCenter,
            color: Colors.green,
            child: Text(
              "Hello Flutter",
              style: TextStyle(fontSize: 20),
            ),
          ),
        ),
      ),
    );
None

The green Container with the alignment property expands to fit its parent red Container and positions its Text child according to the alignment property value, because the incoming constraints are bounded.

Now, when we wrap the green Container in UnconstrainedBox:

    return Scaffold(
      body: Center(
        child: Container(
          color: Colors.red,
          width: 300,
          height: 200, 
          child: UnconstrainedBox(
            child: Container(
              alignment: Alignment.topCenter,
              color: Colors.green,
              child: Text(
                "Hello Flutter",
                style: TextStyle(fontSize: 20),
              ),
            ),
          ),
        ),
      ),
    );
None

The green Container shrink-wraps its child, because the incoming constraints have become unbounded.

ConstrainedBox

A widget that imposes additional constraints on its child.

The ConstrainedBox is the most straightforward way to provide loose-bounded (min < max, max < infinity) to its child. (It can be used to provide tight constraints as well, but for that, we can use SizedBox which is simpler. )

  Widget build(BuildContext context) {
    return Center(
             Container(color: Colors.red,)
           );
  }
None

The Container without constraints takes the entire screen.

Let's wrap it in ConstrainedBox:

 Widget build(BuildContext context) {
    return Center(child: ConstrainedBox(
      constraints: BoxConstraints(
        minWidth: 100, 
        maxWidth: 200, 
        minHeight: 100, 
        maxHeight: 200, 
      ),
    child: Container(color: Colors.red,)));
  }
None

The ConstrainedBox specifies a maximum width and height of 200, and that's what the Container obeys.

Another example: Container has its own width and height specified as 50.

  Widget build(BuildContext context) {
    return Center(
        child: ConstrainedBox(
            constraints: BoxConstraints(
              minWidth: 100,
              maxWidth: 200,
              minHeight: 100,
              maxHeight: 200,
            ),
            child: Container(
              color: Colors.red,
              width: 50,
              height: 50,
            )));
  }
None

However, the ConstrainedBox says it should be a minimum of 100 x 100, and the Container follows.

Here is a practical example of using ConstrainedBox.

Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AppBar'),
        centerTitle: true,
        backgroundColor: Colors.teal,
      ),
    );
  }

The above looks good 😎 on phone.

None

But not so on web/desktop.

None

The idea is to limit the maxWidth of the Scaffold.

 Widget build(BuildContext context) {
    return Center(
      child: ConstrainedBox(
        constraints: BoxConstraints(
          maxWidth: 500,
          maxHeight: MediaQuery.of(context).size.height - 20,
        ),
        child: Scaffold(
          appBar: AppBar(
            title: Text('AppBar'),
            centerTitle: true,
            backgroundColor: Colors.teal,
          ),
        ),
      ),
    );
  }
None

Voila. We don't need responsive design. 😉

SizedBox

A box with a specified size.

SizedBox applies tight constraints to its child.

Widget build(BuildContext context) {
    return Center(
        child: SizedBox(
            width: 100,
            height: 100,
            child: Container(
              color: Colors.red,
              width: 50,
              height: 50,
            )));
  }
None

The Container wants to be 50 x 50, but SizedBox forces it to be 100 x 100.

Let's add another parent SizedBox.

Widget build(BuildContext context) {
    return Center(
        child: SizedBox(
          width: 200,
          height: 200,
          child: SizedBox(
              width: 100,
              height: 100,
              child: Container(
                color: Colors.red,
                width: 50,
                height: 50,
              )),
        ));
  }
None

The child SizedBox applies tight constraints 100 x 100, but they are ignored since its parent SizedBox applies tight constraints 200 x 200.

This is how tight constraints work: they propagate down the tree.

To make the child SizedBox meaningful, we should wrap it with a widget that applies loose constraints, such as Center, Column, Row, Constrained-, or UnconstrainedBox.

  Widget build(BuildContext context) {
    return Center(
        child: SizedBox(
          width: 200,
          height: 200,
          child: Row(
            children: [
              Row(
                children: [
                  SizedBox(
                      width: 100,
                      height: 100,
                      child: Container(
                        color: Colors.red,
                        width: 50,
                        height: 50,
                      )),
                ],
              ),
            ],
          ),
        ));
  }
None

Row applied loose constraints, then inner SizedBox made them tight again, and Container became 100 x 100.

FittedBox

Scales and positions its child within its parent according to fit.

FittedBox applies tight constraints to its child, but does it in a complicated way.

First, it applies unbounded constraints to the child and lets the child grow to its natural size. Then it fits the child into the parent according to the fit property. And finally, it calculates the child's fitted constraints and applies them as tight constraints.

Widget build(BuildContext context) {
    return Center(
        child: SizedBox(
          width: 100,
          height: 200,
          child: ColoredBox(
            color: Colors.greenAccent,
            child: FittedBox(
                fit: BoxFit.contain, //BoxFit.fill / BoxFit.cover / BoxFit.none
                child: Container(
                  color: Colors.red,
                  width: 200,
                  height: 100,
                )),
          ),
        ));
  }
None

That's it. I feel like my understanding of Flutter layout grew from 5% to 75% in the process of writing this article.

I doubt that I got everything right, so every critical note is very welcome!

Thank you for reading, and happy coding!