原文 : Flutter: The Advanced Layout Rule Even Beginners Must Know

By Marcelo Glasberg

Translator: Vadaski

Proofread by Luke Cheng and Alex

preface

This article was originally published in Medium by Marcelo Glasberg in Flutter: The Advanced Layout Rule Even Beginners Must Know. This was discovered by Flutter Team and included in Flutter. Dev.

After reading this article carefully, I think it is very instructive for Flutter developers. It is essential that every Flutter developer understand the layout constraint process carefully! Therefore, in the process of translating this text, we polished the translation repeatedly, trying to retain the content of the original text to the reader. Hopefully, every developer who reads this article will find something to learn.

Understand layout constraints in depth

We often hear developers learning about Flutter wonder why I set width:100 but it doesn’t look 100 pixels wide. (Note that “pixels” in this article refer to logical pixels.) Normally you would say, put the Widget in the Center, right?

Don’t do that.

If you do, they’ll keep coming back to you with questions like: Why isn’t FittedBox working again? Why Column overflows the boundary, or what IntrinsicWidth should do.

The first thing we should do is tell them that the layout of Flutter is quite different from the HTML layout (these developers are probably Web developers) and then memorize this rule:

  • First, the upper widgets pass constraints to the lower widgets.

  • The lower-level widgets then pass size information to the upper-level widgets.

  • Finally, the upper-layer widgets determine the location of the lower-layer widgets.

If we don’t master this rule in development, we won’t fully understand it in layout, so the sooner we learn it, the better!

More details:

  • A Widget gets its own constraints from its parent. The constraint is essentially a collection of four floating-point types: maximum/minimum width, and maximum/minimum height.

  • The widget will then iterate through its children list one by one. Pass the constraints to the children (constraints may vary between the children), and then ask each of its children how big they need to be for the layout.

  • The widget then lays out its children one by one. (Horizontal is the X-axis, vertical is the Y-axis)

  • Finally, the widget will pass its size information up to the parent widget (including its original constraints).

For example, if a widget contains a Column with padding, and the Column’s child widgets are laid out as follows:

The negotiation would look something like this:

Widgets: “hey! My father. What are my constraints?”

Parent: “You must be between 80 and 300 pixels wide and between 30 and 85 pixels tall.”

Widgets: “HMM… I want a 5-pixel inner margin so that my children can be up to 290 pixels wide and 75 pixels high.”

Widget: “Hey, my first child, you have to be 0 to 290 wide and 0 to 75 tall.”

First Child: “OK, I want 290 pixels wide and 20 pixels high.”

Widgets: “HMM… Since I want to put my second child below the first, we only have 55 pixels left for the second child.”

Widget: “Hey, my second child, you must be between 0 and 290 in width and between 0 and 55 in height.”

Second Child: “OK, I want 140 pixels wide and 30 pixels high.”

Widget: “Good. My first child will be placed at x: 5&y: 5 and my second child will be placed at x: 80&y: 25.”

Widget: “Hey, my parent, I decided to make my size 300 pixels wide and 60 pixels high.”

limit

As mentioned in the layout rules introduced above, the layout engine of Flutter has some important limitations:

  • A widget can only determine its size if its parent restricts it. This means that widgets generally cannot be as large as they want.

  • A widget cannot know and does not need to determine its position on the screen. Because its position is determined by its parent.

  • When it is the parent’s turn to determine its size and position, it also depends on its own parent. So, it’s almost impossible to define exactly the size and location of any widget without considering the entire tree.

The sample

The following example is provided by DartPad and has a good interactive experience. Switch 29 different examples using the number of horizontal scroll bars below.

You can find the source code on flutter.cn.

You can also get the code in the Github repository if you wish.

These examples are described in the following sections.

Sample 1

Container(color: Colors.red)
Copy the code

The entire screen acts as the parent of the Container and forces the Container to be the same size as the screen.

So the Container fills the screen and is painted red.

The sample 2

Container(width: 100, height: 100, color: Colors.red)
Copy the code

The red Container wants to be 100 x 100, but it can’t because the screen forces it to be the same size as the screen.

So the Container fills the screen.

Sample 3

Center(
   child: Container(width: 100, height: 100, color: Colors.red)
)
Copy the code

The screen forces Center to be as big as the screen, so Center fills the screen.

Center then tells the Container that it can be any size, but not larger than the screen. Now the Container can truly become 100 × 100.

The sample of 4

Align(
   alignment: Alignment.bottomRight,
   child: Container(width: 100, height: 100, color: Colors.red),
)
Copy the code

Unlike the previous example, we used Align instead of Center.

Align also tells Container that you can be any size. However, it will not be centered in a Container if there is empty space left. Instead, it will place the Container in the bottomRight, within the space allowed.

The sample 5

Center(
   child: Container(
      color: Colors.red,
      width: double.infinity,
      height: double.infinity,
   )
)
Copy the code

The screen forces Center to be as big as the screen, so Center fills the screen.

Center then tells the Container that it can be any size, but not larger than the screen. Now, the Container wants an unlimited size, but since it can’t be larger than the screen, it just fills the screen.

The sample of 6

Center(child: Container(color: Colors.red))
Copy the code

The screen forces Center to be as big as the screen, so Center fills the screen.

Center then tells the Container that it can be any size, but not larger than the screen. Because the Container has no children and no fixed size, it decides to be as big as it can be, so it fills the entire screen.

But why did Container make this decision? This is very simple, because the decision is made by the creator of the Container Widget. It may vary from creator to creator, and you’ll have to read the Container documentation to understand how it behaves in different scenarios.

The sample 7

Center(
   child: Container(
      color: Colors.red,
      child: Container(color: Colors.green, width: 30, height: 30)))Copy the code

The screen forces Center to be as big as the screen, so Center fills the screen.

Center then tells the red Container that it can be any size, but not larger than the screen. Because the Container has no fixed size but children, it decides to become the size of its child.

The red Container then tells its child to be any size, but not out of the screen.

And its child is a Container that wants to be 30 by 30 green. Because the red Container is the same size as its children, it is also 30 x 30. Because the green Container completely covers the red Container, you can’t see it anymore.

The sample of eight

Center(
   child: Container(
     color: Colors.red,
     padding: const EdgeInsets.all(20.0),
     child: Container(color: Colors.green, width: 30, height: 30)))Copy the code

The red Container changes to the size of its children, but it carries its padding into the constraint’s evaluation. So it has a 30 x 30 margin. Now you can see red because of this margin. The green Container is the same as before.

The sample of 9

ConstrainedBox(
   constraints: BoxConstraints(
      minWidth: 70, 
      minHeight: 70,
      maxWidth: 150, 
      maxHeight: 150,
   ),
   child: Container(color: Colors.red, width: 10, height: 10),Copy the code

You might expect the Container to be between 70 and 150 pixels in size, but it’s not. ConstrainedBox imposes only other constraints on the constraints it receives from its parent.

In this case, the screen forces the ConstrainedBox to be exactly the same size as the screen, so it tells its child Widget to use the screen size as a constraint as well, ignoring the impact of its constraints parameters.

The sample 10

Center(
   child: ConstrainedBox(
      constraints: BoxConstraints(
         minWidth: 70, 
         minHeight: 70,
         maxWidth: 150, 
         maxHeight: 150,
      ),
      child: Container(color: Colors.red, width: 10, height: 10)))Copy the code

Center now allows ConstrainedBox to be as large as the screen allows. ConstrainedBox appends the constraints imposed by the Constraints argument to its children.

The Container must be between 70 and 150 pixels. Although it wanted to be 10 pixels, it ended up with 70 pixels (minimum 70).

The sample of 11

Center(
  child: ConstrainedBox(
     constraints: BoxConstraints(
        minWidth: 70, 
        minHeight: 70,
        maxWidth: 150, 
        maxHeight: 150,
        ),
     child: Container(color: Colors.red, width: 1000, height: 1000)))Copy the code

Center now allows ConstrainedBox to be as large as the screen allows. ConstrainedBox appends the constraints imposed by the Constraints argument to its children.

The Container must be between 70 and 150 pixels. Although it wanted to be 1000 pixels, it ended up with 150 pixels (maximum 150).

The sample of 12

Center(
   child: ConstrainedBox(
      constraints: BoxConstraints(
         minWidth: 70, 
         minHeight: 70,
         maxWidth: 150, 
         maxHeight: 150,
      ),
      child: Container(color: Colors.red, width: 100, height: 100)))Copy the code

Center now allows ConstrainedBox to be as large as the screen allows. ConstrainedBox appends the constraints imposed by the Constraints argument to its children.

The Container must be between 70 and 150 pixels. Although it wanted to be 100 pixels, 100 is in the range of 70 to 150, so it got 100 pixels.

The sample of 13

UnconstrainedBox(
   child: Container(color: Colors.red, width: 20, height: 50),Copy the code

The screen forces the UnconstrainedBox to be as large as the screen, and the UnconstrainedBox allows its children’s Containers to be any size.

The sample of 14

UnconstrainedBox(
   child: Container(color: Colors.red, width: 4000, height: 50),Copy the code

The screen forces the UnconstrainedBox to be as large as the screen, and the UnconstrainedBox allows its children’s Containers to be any size.

Unfortunately, in this case, the container is 4000 pixels wide, which is too large to fit in an UnconstrainedBox, so the UnconstrainedBox will display overflow warning.

The sample of 15

OverflowBox(
   minWidth: 0.0,
   minHeight: 0.0,
   maxWidth: double.infinity,
   maxHeight: double.infinity,
   child: Container(color: Colors.red, width: 4000, height: 50));Copy the code

The screen forces the OverflowBox to be as big as the screen, and the OverflowBox allows its children to be set to any size.

OverflowBox is similar to UnconstrainedBox, except that it will not display any warnings if its children exceed the space.

In this case, the container is 4000 pixels wide and too large to fit in the OverflowBox, but the OverflowBox displays it all without warning.

The sample of 16

UnconstrainedBox(
   child: Container(
      color: Colors.red, 
      width: double.infinity, 
      height: 100)),Copy the code

This will render nothing, and you can see error messages on the console.

UnconstrainedBox lets its children decide to be any size, but its children are a Container of infinite size.

A Flutter cannot render something infinitely large, so it throws the following error:

The sample of 17

UnconstrainedBox(
   child: LimitedBox(
      maxWidth: 100,
      child: Container( 
         color: Colors.red,
         width: double.infinity, 
         height: 100))),Copy the code

You won’t get an error this time. UnconstrainedBox Gives LimitedBox an unlimited size; But it passes a constraint of up to 100 to its children.

If you replace UnconstrainedBox with Center, LimitedBox no longer applies its limit (because its limit only applies if you get an infinite constraint), and the container width is allowed to exceed 100.

The above example explains the difference between LimitedBox and ConstrainedBox.

The sample of 18

FittedBox(
   child: Text('Some Example Text.'),Copy the code

The screen forces the FittedBox to be the same size as the screen, whereas Text has a natural width (also known as the intrinsic width) that depends on the amount of Text, font size, and other factors.

FittedBox allows Text to be any size. But after Text tells FittedBox its size, FittedBox scales the Text until it fills up all the available widths.

The sample of 19

Center(
   child: FittedBox(
      child: Text('Some Example Text.')))Copy the code

But what happens if you put FittedBox in the Center Widget? Center will allow the FittedBox to be any size, depending on the screen size.

The FittedBox then resizes itself based on the Text, and then allows the Text to be any size you want, without scaling because they are the same size.

The sample of 20

Center(
   child: FittedBox(
      child: Text('This is some very very very large text that is too big to fit a regular screen in a single line.')))Copy the code

However, what happens if the FittedBox is in the Center, but the Text is too big for the screen?

FittedBox tries to adjust the size to the Text size, but not larger than the screen size. Then assume the screen size and resize the Text to fit the screen as well.

The sample of 21

Center(
   child: Text('This is some very very very large text that is too big to fit a regular screen in a single line.'),Copy the code

However, if you remove the FittedBox, Text takes its maximum width from the screen and wraps lines where appropriate.

The sample of 22

FittedBox(
   child: Container(
      height: 20.0, 
      width: double.infinity,
   )
)
Copy the code

FittedBox can only scale child widgets at a limited width (width and height do not become infinite). Otherwise, it won’t render anything, and you’ll see errors in the console.

The sample of 23

Row(
   children:[
      Container(color: Colors.red, child: Text('Hello! ')),
      Container(color: Colors.green, child: Text('Goodbye! ')))Copy the code

The screen forces the Row to be as big as the screen, so the Row fills the screen.

Like unconstrainedboxes, rows do not impose any constraints on their children, but let them be as large as they need to be. Row then places them side by side, leaving any extra space blank.

The sample of 24

Row(
   children:[
      Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.')),
      Container(color: Colors.green, child: Text('Goodbye! ')))Copy the code

Because Row does not impose any constraints on its children, it is likely that its children are too large for the available width of Row. In this case, Row displays an overflow warning as UnconstrainedBox does.

The sample of 25

Row(
   children:[
      Expanded(
         child: Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.'))
      ),
      Container(color: Colors.green, child: Text('Goodbye! ')))Copy the code

When a Row child is wrapped in a Expanded Widget, the Row no longer lets it determine its own width.

Instead, a Row calculates the width it should have based on all the children of Expanded.

In other words, once you use Expanded, the width of the child itself becomes irrelevant and is simply ignored.

The sample of 26

Row(children:[Expanded(child: Container(color: Colors. Red, child: Text(' ThisisA very long text that won't fit the line. ')),), Expanded(child: Container(color: Colors. Green, child: text (' Goodbye! '),),)Copy the code

If all Row children are wrapped with Expanded Widgets, each Expanded size will be proportional to its Flex factor, and Expanded Widgets will force its children to have the same width as Expanded.

In other words, Expanded ignores the desired width of its child widgets.

The sample of 27

Row(children:[
  Flexible(
    child: Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.'))),
  Flexible(
    child: Container(color: Colors.green, child: Text(‘Goodbye!’))),
  ]
)
Copy the code

If you use Flexible instead of Expanded, the only difference is that Flexible gives its children the same or smaller width. Expanded will force its children to have the same width as Expanded. But both Expanded and Flexible ignore the width when they determine the size of the child.

This means that Row can either use the child’s width or use Expanded and Flexible to ignore the child’s width.

The sample of 28

Scaffold(
   body: Container(
      color: blue,
      child: Column(
         children: [
            Text('Hello! '),
            Text('Goodbye! '))))Copy the code

The screen forces that Scaffold to be as big as the screen, so that Scaffold fills the screen. Scaffold then tells the Container that it can be any size but not larger than the screen.

When a widget tells its children that it can be smaller than itself, it is often said to be loose on its children.

The sample of 29

Scaffold(
body: SizedBox.expand(
   child: Container(
      color: blue,
      child: Column(
         children: [
            Text('Hello! '),
            Text('Goodbye! '),],))))Copy the code

If you want the Scaffold’s child to be as big as the Scaffold itself, you can coat the child with a SizedBox. Expand.

When a widget tells its children that they must become a certain size, it is often said that the widget uses tight constraints on its children.

Tight vs. loose constraints

You’ll often hear about constraints that are strict or loose, and it’s worth your time to figure them out.

Tight constraints give you an option to get the exact size. In other words, it has the same maximum/minimum width and the same height.

If you search for the BoxConstraints constructor in the box.dart file of Flutter, you will find the following:

BoxConstraints.tight(Size size)
   : minWidth = size.width,
     maxWidth = size.width,
     minHeight = size.height,
     maxHeight = size.height;
Copy the code

If you reread Sample 2, it tells us that the screen forces the Container to be as big as the screen. The reason screens can do this is because they pass strict constraints to the Container.

A loose constraint, in other words, sets the maximum width/height, but allows its child widgets to be any size smaller than it is. In other words, the minimum width/height of the loose constraint is 0.

BoxConstraints.loose(Size size)
   : minWidth = 0.0,
     maxWidth = size.width,
     minHeight = 0.0,
     maxHeight = size.height;
Copy the code

If you go to Sample 3, it will tell us that Center makes the red Container smaller, but does not exceed the screen. The reason Center can do this is because it gives the Container a loose constraint. In general, the Center’s role is to take strict constraints from its parent (screen) and translate them into loose constraints for its children (Container).

Know how to make layout rules for a particular widget

Knowing the general layout is important, but it’s not enough.

When applying general rules, each widget has a great deal of freedom, so there is no way to know what a widget might look like just by looking at its name.

If you try to guess, you might be wrong. Unless you have read the widget’s documentation or studied its source code, you cannot know for sure how the widget behaves.

Laying out the source code is often complex, so reading the documentation is a better choice. But when you’re exploring the layout source code, you can easily find it using the IDE’s navigation features.

Here’s an example:

  • Find a Column in your code and follow up to its source code. To do this, use Command +B (macOS) or Control +B (Windows/Linux) in (Android Studio/IntelliJ). You will jump to the basic.dart file. Since Column extends Flex, navigate to the Flex source code (also in basic.dart).

  • Scroll down until you find a method called createRenderObject(). As you can see, this method returns a RenderFlex. This is the render object for Column. Now navigate to the source code for RenderFlex in the flex.dart file.

  • Scroll down until you find the performLayout() method, which performs the column layout.


Finally, thank you very much to Cheng Lu and Alex who participated in the proofreading, as well as CaiJingLong, Ren Yujie and Sun Kai who helped polish the translation. Thank you!

I hope after reading this article, I can gain something for you. If you have any questions or want to discuss them with me, please feel free to share them in the comments section at the bottom or contact me via email. Happy coding!

English link: flutter.cn/docs/develo…