Multi-Child Layout Alg
October 10, 2022
Multi-Child Layout Alg
Written by Scott Stoll on behalf of DevAngels Limited
As they stand currently, the docs explaining how a Row or Column are laid out are pretty confusing for beginners, but we’re hoping that will change soon. Regardless, we feel it’s important that the process be explained in detail, and in terms that are easy to understand even for those who have never used Flutter before. So, go refill your coffee barrel (you still drink from a cup?) and let’s dive right in!
What is it?
Flutter’s multi-child layout algorithm is used when the constructor of a Flex is called, and the Flex is what we use as a basis for Rows and Columns. One of the things you have to pass into a Flex is the direction parameter, which takes an enum called Axis, and Axis can be vertical or horizontal.
If you use a Column, the constructor for the Column is going to take all of the parameters you gave it and then call the constructor of a Flex, after adding the direction parameter,axis.vertical”.
Does Flex’s superclass use the algorithm?
No. The superclass of the Flex is the abstract class, MultiChildRenderObjectWidget. The Flex is not the only class that extends it. MultiChildRenderObjectWidget is also the superclass for Stack, which uses a completely different process for its layout.
Do I have to know this in order to be a Flutter Developer?
Nope, but it could make your life a lot easier when trying to understand why your Row or Column refuses to work. The error you’ll get involves the RenderFlex, which is the Render Layer’s counterpart of the Flex. You see, Flex is a blueprint for what you want, but the RenderFlex is the actual thing that gets rendered. The reason you get a RenderFlex error instead of a Row or Column error is because the error isn’t happening until Flutter is going through the process of actually rendering your UI. As we said earlier, a Row or Column just calls a Flex constructor and adds the appropriate direction parameter. So, as far as the Render Layer knows, you might have just put a Flex in your code instead of using a Row or Column. In fact, the Render Layer never sees anything called Row or Column, which is why it throws an error for the thing it does see; a RenderFlex.
If a Flex isn’t a flex but the Flex can flex, does that make it a Flexible?
In order to understand what’s going on here, there are a few terms you’ll need to understand:
- Flex: The superclass of Row and Column, the real thing that gets used when you put a Row or Column in your code.
- Main Axis: The main direction. This is up and down for a Column, left and right for a Row.
- MainAxisAlignment: How the children should be aligned or spaced out in the main axis.
- Cross Axis: The other direction. Up and down for a Row, left and right for a Column.
- CrossAxisAlignment: How the children should be aligned or made to fit in the main axis.
- Flexible: A widget that can expand or contract as needed, along the main axis. It does not have a set size.
- flex: The parameter that determines the “strength” of the Flexible (this is called Gravity in Android development). This is also referred to as the “flex factor”.
- FlexFit: This determines how the Flexible will fit into the space allotted for it.
- FlexFit.tight: The Flexible will be forced to fill the available space in the main axis.
- FlexFit.loose: The Flexible will be allowed to be whatever size it wants to be, up to the size reserved for it. If it is smaller than the space that was reserved for it then the unused space will be blank (transparent). That unused space will NOT be given to another Flexible.
- Expanded: A class that extends Flexible. Just as the Row and Column extend the Flex and then add a direction parameter, the Expanded extends the Flexible and then forces the fit to be FlexFit.tight. If you want control over the fit, you have to use its superclass (the Flexible).
- Constraints: Minimum and Maximum sizes a widget has to respect. There are constraints in both the main axis and the cross axis.
That should take care of that. So, without further ado, “let’s crack on”…
The multi-child layout algorithm.
Since the description in the docs is subject to change, we won’t show them here. Instead, we’ll go over what each step is actually doing.
Step 1) Layout things that don’t have any flex.
At this point, we’re not going to set any sizes in the main axis. In fact, the docs expressly state: “Layout each child a null or zero flex factor (e.g., those that are not Expanded) with unbounded main axis constraints and the incoming cross axis constraints”. This means each child will be laid out without any minimum or maximum sizes being specified, yet. So, at the moment, we’re basically laying them out in the order they’ll appear, without specifying their main axis size.
In the cross axis, we let the children be whatever size they want to be unless the CrossAxisAlignment is set to stretch. If it is, then force each child to be as large as the Flex’s parent will allow it to be in the cross axis.
Step 2) Make reservations for the leftover space.
Now we will take a look at how much leftover space there is and start reserving chunks of it for the Flexibles. The docs actually say the space will be divided, “among the children with non-zero flex factors”. However, the only widgets in Flutter that can have a non-zero flex factor are the Flexible and the Expanded, which is just a specific kind of Flexible.
Also, there is no reason for a Flexible to have a flex factor of 0, they even default to 1. So, it’s pretty safe to just say we’re going to be reserving chunks of the leftover space for all of the children that are Flexibles. After all, if they had a zero flex factor then they’d have been laid out already in Step 1.
Which Flexibles get how much space is dependent on the values of their flex parameters, or “flex factor”. If there are three Flexibles and their flex parameters are 1, 1, and 2, then the reservations are determined like this:
- The total flex is: 1 + 1+ 2 = 4
- Each of the Flexibles with a flex of 1 will get 1/4 of the total space reserved for them.
- The Flexible with a flex of 2 will get 2/4 of the total space reserved for it.
So, if there is 100 dp of space that is still available after laying out the children that have set sizes, our four Flexibles will get 25, 25, and 50 dp reserved for them, respectively.
Step 3) Lay out the Flexibles.
There are a few steps involved in this… step.
- Lay out the Flexibles with Cross Axis constraints we determined in Step 1. In other words, let them be the size they want to be unless the CrossAxisAlignment parameter was set to CrossAxisAlignment.stretch. If it was, then force the Flexibles to be as big as the Flexible is, in the cross axis.
- Flexibles that have their fit parameter set to FlexFit.tight (this includes all Expandeds) are “given tight constraints”. This means they’re not going to be allowed to be a size in between the minimum and maximum sizes allowed. Instead, they’re going to be forced to be the maximum size allowed no matter what size they say they should be. This means if you have a Container or SizedBox that has a set size in the main axis, that given size will be ignored and the Container or SizedBox will instead be forced to use all the space that was reserved for it.
- Flexibles that have their fit parameter set to FlexFit.loose will be allowed to be whatever size they want in the main axis, as long as that size falls within the minimum and maximum constraints. This means they might not use all the space that was reserved for them.
But wait… What if a Flexible doesn’t use all of the space that was reserved for it? Is the unused space given to an Expanded, or some other widget?
Well then, if all of the children are not using all of the space, won’t that make the Flex (the Row or Column) shorter than the size its parent told it to be?
Confusing, isn’t it?
If the children aren’t using all the space, and the Row or Column is, then there is only one other possibility. The Row or Column has unused space inside of it. And this, dear coffee guzzler, is the correct answer.
What makes this especially tricky is the fact that a Flexible doesn’t have a background color, it’s transparent. This makes it look like the Row or Column is shorter than it should be, but what’s really happening is that you’re able to see through the empty space that’s not being used. This creates the illusion the Flex is shorter than it really is, since it doesn’t have a border to show you its boundaries.
Step 4) Determine the cross axis size of the Flex.
This one’s easy:
- Were tight constraints passed to the Flex by its parent? Then make the Flex the max size those constraints will allow.
- Is the CrossAxisAlignment set to CrossAxisAlignment.tight? Then, again, make the Flex the max size in the cross axis that the constraints passed to it will allow.
- Otherwise, make the size of the Flex in the cross axis the same size as its largest child in that direction. This means the width of the widest child for a Column, or the height of the tallest child for a Row.
Step 5) Determine the main axis size of the Flex.
- If the MainAxisSize is set to max (which is the default if you don’t put anything), then make the Flex’s main axis size be the maximum allowed by the constraints it was given from its parent.
- If you set the MainAxisSize to min, then the Flex will be as small as it can be, while still being large enough to contain all its children.
But, what if the parent didn’t tell the Flexible what size to be in the main axis, and there’s a Flexible with tight constraints. How does the Flex determine what size to make that Flexible if it doesn’t know what size the Flex can be, and how does the algorithm determine the size of the Flex if it can’t resolve that?
It can’t. This is when you get a RenderFlex error, and what we affectionately call, “The Red Screen of Death”.
Step 6) Align the children.
There are a number of alignment options, and this is when the one you chose, then the defaults, are applied.
In the Main Axis:
- If the user’s language is set to one that reads left to right and we’re using a Row, the horizontal axis will be aligned to the left.
- If the user’s language reads right to left and this is a Row, the horizontal axis will align to the right.
- If this is a Column, the children will be vertically aligned to the top of the Flex.
- Center: The children will be centered in the main axis.
- If the user’s language is set to one that reads left to right and we’re using a row, the horizontal axis will be aligned to the right.
- If the user’s language reads right to left and this is a row, the horizontal axis will align to the left.
- If this is a Column, the children will be vertically aligned to the bottom of the Flex.
- spaceBetween: Put the first and last children on the ends, and if there is any unused space then make the gaps between the children all the same size.
- spaceAround: This one is a little strange. In the end, you end up with the same amount of space on each side of each child. It’s just like having all the children using padding with EdgeInsets.all. If the value for “all” was set to 10, then you would have the first and last children with 10 dp of padding between them and the ends, but 20 dp of space between each of the children since each child has 10 dp surrounding it. Using spaceAround has the same effect. The space on the ends will be half the size of the space between the children.
- spaceEvenly: Make the space before and after every child, even the ends, all the same.
In the Cross Axis:
- If the user’s language is set to one that reads left to right and we’re using a Column, the horizontal axis will be aligned to the left.
- If the user’s language reads right to left and this is a Column, the horizontal axis will align to the right.
- If this is a Row, the children will be vertically aligned to the top.
- Center: The children will be centered in the main axis.
- If the user’s language is set to one that reads left to right and we’re using a Column, the horizontal axis will be aligned to the right.
- If the user’s language reads right to left and this is a Column, the horizontal axis will align to the left.
- If this is a Row, the children will be vertically aligned to the bottom.
- stretch: Force all children to expand in the main axis until they fill every pixel of available size.
- baseline: Align the baselines of all the children. This value is intended for Rows and it’s not as simple as it sounds.
- The baselines are on the bottom, and so all of the children will be aligned vertically in a way that lines their bottom edges.
- If the baselines are on the bottom then it’s impossible to align the baselines of children that are in a Column. Therefore, if the CrossAxisAlignment of a Column is set to baseline, it will be treated as if the CrossAxisAlignment had been set to start.
- This next one is the tricky part:
- You have a Row.
- The minimum constraint passed in to the Row is larger than even the tallest child in the row.
- You align the baselines of all the children so their bottoms align however…
- You then shift all the children up as a single group, so the top of the tallest child touches the top of the Row, and all of the bottoms of those children are still aligned with each other.
Summing Up: A test of your mastery.
If any of this is unclear to you, then read the article again. You will know you have achieved mastery of this algorithm when the following actually makes sense to you:
- A Flex is flexible but don’t confuse the fact that it can be flexible with an actual Flexible.
- A FlexFit has nothing to do with the fit of the Flex. Flexes don’t fit, Flexibles do.
- The flex of a Flexible determines the size of the Flexible, except when it doesn’t.
- You can set the flex to 9,999,999,999,999,999,999... if your FlexFit is loose it’s still not going to do anything.
- The FlexFit of the fit determines if the flex of the Flexible will be allowed to flex the Flexible so it better fits into the Flex.
Come back next time for an overview of Theme Extensions. Until then, happy Fluttering