For learners of Flutter, mastering the behavior of Flutter layout directly determines whether the developer can develop Flutter layout efficiently and quickly. However, it is difficult for beginners to translate their thoughts into the code of Flutter in the face of numerous widgets and unexpected behavior.
By the way, my open source project, Flutter_dojo, has just been released 2.0, so please check it out. Flutter_dojo
When someone learning Flutter asks you why some widgets with a width of 100 are displayed but not 100 pixels wide, your default answer is to tell them to put the widget inside the Center, right?
Don’t do that. If so, they will come back again and again asking why some of the FittedBoxes didn’t work, why the Column overflowed, or what IntrinsicWidth should do.
Instead, tell them that the Flutter layout is very different from the HTML layout (which they are probably very familiar with) and then make them remember the following rules:
Constraints go down. Sizes go up. Parent sets position.
Without this rule, you cannot really understand the layout of Flutter, so Flutter developers should learn it as soon as possible.
In more detail: The Widget gets its own constraints from its parent. The constraint is just a set of four doubles:
- Minimum and maximum widths
- Minimum and maximum height
The Widget then iterates over all of its child widgets. Widget one by one to tell their child restraint (each child may vary), and then ask each child want size, and then, the Widget will position its children (on the x axis layout horizontally and vertically on the y axis layout), in the end, the Widget will tell the size of its own parent (of course, within the original constraint).
For example, if a composite Widget contains some Padding and Column, and you want to lay out its two widgets as shown:
The negotiations went like this:
- Widget: Hey Parent, what are my constraints?
- Parent Widget: You must be between 80 and 300 pixels wide and between 30 and 85 pixels tall.
- Widget: Well, since I’m going to have a 5 pixel Padding, my child Widget can be up to 290 pixels wide and 75 pixels high.
- Widget: Hey, the first child Widget, you must be between 0 and 290 pixels wide, and you must be between 0 and 75 pixels high.
- First Child: Ok, so I want 290 pixels wide and 20 pixels high.
- Widget: Well, since I want to put the second Widget below the first, the second Widget is only 55 pixels high.
- Widget: Hey, second sub-widget, you must be between 0 and 290 height, and you must be between 0 and 55 height.
- Second Child: Ok, I want 140 pixels wide and 30 pixels high.
- Widget: Great. My first child was in position X: 5 and Y: 5, and my second child was in position X: 80 and Y: 25.
- Widget: Dear parents, I have decided to set the size to 300 pixels wide and 60 pixels high.
Limitations
Due to the above layout rules, the layout engine of Flutter has some important limitations:
- A Widget can only determine its own size within the limits imposed by its parent. This means that widgets usually can’t be any size they need. The layout is top-down, and currently widgets have some basic constraints (from their parent elements) on the minimum and maximum width and height
- A Widget cannot know or determine its position on the screen because the Widget’s parent determines the location of the Widget. It will in turn ask the child elements for basic layout restrictions, ask the child elements to report the desired layout results, and then, based on the status quo and the nature of its layout algorithm, tell the child elements where to go and how much space to take up
Because the size and position of the parent are dependent on the parent, it is impossible to define the size and position of any widget precisely without considering the entire tree.
Each widget does not necessarily get the layout size it expects. A notable example of this is ConstrainedBox, which can be confusing. Each widget does not determine its position on the screen, but on the parent element. Because this layout logic needs to consider the elements above it, the final layout of an element needs to consider the widget tree in the ENTIRE UI. Container and ConstrainedBox are a viable decoration layout for precise local layout.
Examples
The following 29 examples demonstrate the idea of Flutter layout.
Github.com/marcglasber…
Example 1
Container(color: Colors.red)
Copy the code
The screen is the parent of the Container, which forces the Container to be exactly the same size as the screen. Therefore, the container fills the screen and paints it red.
Example 2
Container(width: 100, height: 100, color: Colors.red)
Copy the code
You want the red container to be 100 by 100, but not because the screen forces it to be exactly the same size as the screen. So the container fills the screen.
Example 3
Center(
child: Container(width: 100, height: 100, color: Colors.red)
)
Copy the code
The screen forces Center to be exactly the same as the screen, so Center fills the entire screen. Center tells the Container that it can be any size you want, but not larger than the screen size. So now the container really could be 100 by 100.
Example 4
Align(
alignment: Alignment.bottomRight,
child: Container(width: 100, height: 100, color: Colors.red),
)
Copy the code
This is different from the previous example because it uses Align instead of Center. Align also tells the Container that it can be any size you want, while being bottom-right aligned in the remaining free space.
Example 5
Center(
child: Container(
color: Colors.red,
width: double.infinity,
height: double.infinity,
)
)
Copy the code
The screen forces Center to be exactly the same as the screen, so Center fills the entire screen. Center tells the Container that it can be any size you want, but not larger than the screen size. The container wants to be infinite in size, but because it cannot be larger than the screen, it can only fill the screen.
Example 6
Center(child: Container(color: Colors.red))
Copy the code
The screen forces Center to be exactly the same as the screen, so Center fills the entire screen. Center tells the Container that it can be any size you want, but not larger than the screen size. Since the Container has no Child and no fixed size, it decides to be as large as possible, so it fills the entire screen. But why does Container make this decision? Simply because it was the design decision of the person who created the Container. Other widgets may be created differently, depending on the situation.
Example 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 exactly the same as the screen, so Center fills the entire screen. Center tells the red Container that it can be any size you want, but no larger than the screen. Since the red Container does not have a size, but has a Child, it decides to have the same size as the Child. The red Container tells its children that they can be any size they want, but not larger than the screen size. The Child is a green Container that wants to be 30 by 30. Considering that the red Container is the same size as its child, which is also 30 by 30, the red is not visible because the green Container will completely cover the red Container.
Example 8
Container(color: Colors. Red, padding: const EdgeInsets. All (20.0), child: Container(color: color) Colors.green, width: 30, height: 30), ) )Copy the code
The red Container adjusts itself to the child’s size, but takes its own padding into account. So it’s also 30 by 30 plus the padding. Because of the padding, you can see that the red and green containers are the same size as in the previous example.
Example 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. Here, the screen forces ConstrainedBox to be exactly the same size as the screen, so it tells its child Widget to assume the screen size as well, ignoring its constraint parameter.
Example 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 any size smaller than the screen size. ConstrainedBox imposes additional constraints from its constraint parameters on its children. The Container must be between 70 and 150 pixels. It wanted 10 pixels, so it ended up with 70 pixels (minimum).
Example 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 allows ConstrainedBox to be any size smaller than the screen size. ConstrainedBox imposes additional constraints from its constraint parameters on its children. The Container must be between 70 and 150 pixels. It wanted 1000 pixels, so it ended up with 150 (Max).
Example 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 allows ConstrainedBox to be any size of screen size. ConstrainedBox imposes additional constraints from its constraint parameters on its children. The Container must be between 70 and 150 pixels. It wants to be 100 pixels, that’s how big it is, because it’s between 70 and 150.
Example 13
UnconstrainedBox(
child: Container(color: Colors.red, width: 20, height: 50),
)
Copy the code
The screen force UnconstrainedBox is exactly the same size as the screen. However, UnconstrainedBox allows its children to set an arbitrary size.
Example 14
UnconstrainedBox(
child: Container(color: Colors.red, width: 4000, height: 50),
)
Copy the code
The screen forces the UnconstrainedBox to be exactly the same size as the screen, and the UnconstrainedBox sets its child Container to any size. Unfortunately, in this case, the container is 4000 pixels wide and is too large to fit in the UnconstrainedBox, so the UnconstrainedBox displays overflow warnings.
Example 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 enforces the OverflowBox to be exactly the same size as the screen, and the OverflowBox allows its children to be set to any size. The OverflowBox is similar to UnconstrainedBox, except that it will not display any warning if the Child does not fit into the space. In this case, the container is 4000 pixels wide and too large to fit inside the OverflowBox, but the OverflowBox will display as much content as possible without warning.
Example 16
UnconstrainedBox(
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
)
)
Copy the code
You will see errors in the console. UnconstrainedBox can make its child Widget any size you want, but its child Widget is a Container with an infinite size. A Flutter cannot be infinite in size, so the following error message appears: BoxConstraints forces an infinite width.
Example 17
UnconstrainedBox(
child: LimitedBox(
maxWidth: 100,
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
)
)
)
Copy the code
There will be no more errors because when UnconstrainedBox assigns infinite size to LimitedBox, it passes the constraint down to a maximum width of 100 pixels. 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. This explains the difference between LimitedBox and ConstrainedBox.
Example 18
FittedBox(
child: Text('Some Example Text.'),Copy the code
The screen will force FittedBox to be exactly the same as the screen. The text will adjust its own width attributes, font attributes, etc. The FittedBox allows the text to be any size, but after the text is told the size of the FittedBox, it scales the text until all available widths are filled.
Example 19
Center(
child: FittedBox(
child: Text('Some Example Text.')))Copy the code
But what if you put the FittedBox inside the Center? Center sets the FittedBox to whatever size you want, up to the screen size. Then, adjust the FittedBox to Text size and make Text whatever size you want. Since FittedBox and Text have the same size, no scaling occurs.
Example 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
But what happens if the FittedBox is in the Center, but the text is too big to fit 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.
Example 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 FittedBox, Text takes its maximum width from the screen and wraps lines where appropriate.
Example 22
FittedBox(Child: Container(height: 20.0, width: double-.infinity,))Copy the code
The FittedBox can scale a Child only at a limited width (width and height are not infinite). Otherwise, it won’t render anything, and you’ll see errors in the console.
Example 23
Row(
children:[
Container(color: Colors.red, child: Text('Hello! ')),
Container(color: Colors.green, child: Text('Goodbye! ')))Copy the code
The screen force line is exactly the same size as the screen. Just like unconstrainedboxes, a Row does not impose any constraints on its children, but lets them be any size they want. Row then places them side by side, leaving any extra space blank.
Example 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 the child widgets are too large to accommodate the available width of the Row. In this case, just like UnconstrainedBox, Row displays an overflow warning.
Example 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’s Child is wrapped in Expanded, the Row no longer lets that Child define its own width. Instead, a Row calculates the width it should have based on all Expanded children. In other words, once you use Expanded, the width of the original Widget becomes irrelevant and is ignored.
Example 26
Row( children:[ Expanded( child: Container(color: Colors.red, child: Text(' This is a very long Text that won't fit the line. '),), Expanded(child: Container(color: color.green, child: The Text (' Goodbye! '),),)Copy the code
If all Row Child widgets are wrapped in Expeded, each Expeded size is proportional to its Flex parameter, and the Child Child is set to the calculated Expanded width. In other words, Expanded ignores the width of its child widgets.
Example 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 makes its child elements equal to or less than its own width, while Expanded enforces its child elements to have exactly the same width as Expeded. However, when sizing, both Expanded and Flexible ignore the width of the child.
Note: This means that the Row either uses the Child’s width or uses Expanded and Flexible to ignore the Child’s width.
Example 28
Scaffold(
body: Container(
color: blue,
child: Column(
children: [
Text('Hello! '),
Text('Goodbye! '))))Copy the code
The screen forces the Scaffold to be exactly the same size so that the Scaffold fills the screen. Scaffold tells the container that it can be any size you want but not larger than the screen size.
Note: When a Widget tells its Child Widget that it can be smaller than a certain size, we say that the Widget provides a LOOSE constraint for its Child.
Example 29
Scaffold(
body: SizedBox.expand(
child: Container(
color: blue,
child: Column(
children: [
Text('Hello! '),
Text('Goodbye! '),],))))Copy the code
If you want your Scaffold’s Child widgets to be exactly the same size as your Scaffold, you can wrap its Child with SizedBox. Expand.
Note: When the widget tells its children that they must have a certain size, we say that the widget provides tight constraints for its children.
Tight vs loose constraints
It’s often mentioned that some constraints are tight or loose, so it’s worth knowing what that means.
Tight constraint provides one possibility, the exact size. In other words, the maximum width of a tight constraint equals its minimum width. And its maximum height is equal to its minimum height.
If you go to the box.dart file of Flutter and search for the BoxConstraints constructor, 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 revisit example 2 above, it will tell us that the screen forces the red Container to be exactly the same as the screen. Of course, the screen is implemented by passing tight constraints to the Container.
On the other hand, loose constraints set the maximum width and height, but keep the widgets as small as possible. In other words, the minimum width and height of the loose constraint are equal to zero:
Loose (Size Size) : minWidth = 0.0, maxWidth = size.width, minHeight = 0.0, maxHeight = size.height;Copy the code
If you revisit Example 3, it will tell us that Center makes the red Container smaller, but no larger than the screen. Center does this by passing loose constraint to the Container. Ultimately, the main purpose of Center is to convert the Tight constraints it obtains from the parent (screen) to loose constraints on its children (container).
Learning the layout rules for specific widgets
Knowing general layout rules is necessary, but not sufficient. Each Widget has a lot of freedom to apply the general rules, so you can’t just read the Widget’s name and know what it might do. 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 it’s best to read the documentation. However, if you decide to explore 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 navigate to its source code. To do this, use Command + B (macOS) or Control + B (Windows/Linux) in Android Studio or IntelliJ. You will be taken 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 RenderFlex source code, which takes you to the flex.dart file. Scroll down until you find a method called performLayout (). This is how the column layout is performed.
Those who are interested in Flutter can join my Flutter meditation group.