Flutter Deep Dive, Part 4: “RenderFlex children have non-zero flex…”
This article is part of a four-part series. You can find the others here:
- Part 1: “RenderFlex children…”, wading into the Baby Pool
- Part 2: Taking the Plunge
- Part 3: A Flex is not a flex
- Part 4: The Flex Layout Algorithm
Before we start, you might want to take a moment to go to the bathroom and refill your coffee, tea, vodka or whatever else you’re drinking. You might also want to take your headache medicine of choice, now. A gram of prevention is supposed to be worth a metric ton of cure or something like that…
Ready? Let’s go.
There are six steps involved in laying out the children of a Flex.
I’ll paste what the docs say about each step and then try my best to translate each one into human:
Step 1:
“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. If the crossAxisAlignment is CrossAxisAlignment.stretch, instead use tight cross axis constraints that match the incoming max extent in the cross axis.”
First, lay out all the children that don’t have a flex
parameter at all or a flex
parameter that’s set to zero. Believe it or not, we’re not going to set a minimum or maximum size for these children in the direction of the mainAxis yet. That’s what “with unbounded main axis constraints” means.
If the CrossAxisAlignment
parameter is set to stretch, then make the child as big as its parent will allow in the cross axis.
Step 2:
“Divide the remaining main axis space among the children with non-zero flex factors (e.g., those that are
Expanded
) according to their flex factor. For example, a child with a flex factor of 2.0 will receive twice the amount of main axis space as a child with a flex factor of 1.0.”
In human: Divide the leftover space along the main axis among all the children that do have a specified flex
and that flex
is not zero. However, not all children get treated the same way. Maybe you like some children more than others, and you want to give them more space than the others.
So how do you do this? You determine who gets how much space by their flex.
If you have 3 children and the flex
of each is 1, 4, and 5; then here’s what happens:
- The total amount of all
flex
is calculated. Here, 1 + 4 + 5 = 10, so we have 10flex
in total. - The first child has a
flex
of 1 and the total is 10, so the first child gets 1/10 of the leftover space reserved for it.
Stick a pin in that phrase, “reserved for it.” We’ll be back for it later.
- The second child gets a reservation for 4/10 (40%) of the total leftover space and the last one, your favorite, gets 5/10 (half) reserved for it to use.
We now calculate the actual number of dp that will be reserved for each one, based on what their final share of the leftover space could be.
Not is. Could be.
Step 3:
“Layout each of the remaining children with the same cross axis constraints as in step 1, but instead of using unbounded main axis constraints, use max axis constraints based on the amount of space allocated in step 2. Children with
Flexible.fit
properties that are FlexFit.tight are given tight constraints (i.e., forced to fill the allocated space), and children withFlexible.fit
properties that are FlexFit.loose are given loose constraints (i.e., not forced to fill the allocated space).”
The first thing a human must realize here is that we didn’t lay anything out in Step 2; we just did a little math because we have to know exactly how many dp (written as a double) each of those children is going to have reserved for it to work with.
Each child that has a flex comes into this process without its main axis constraint set (the parent hasn’t put any limits on its size yet, so it’s still “unbounded”).
So the next step is going to be to make each child’s maximum “main axis constraints” be the maximum number we came up with in Step 2. This will allow it to be up to that big, but never bigger than that. Never forget “never bigger than that”. It becomes important later on in this article and even more important anytime you’re coding a UI.
But wait, we’re not done yet. Remember that shoebox thing called fit
? It’s half of the problem when all this goes horribly wrong. If the FlexFit
is loose, it basically means no one cares. Just be as big as the child says it wants to be, just like it didn’t even have aflex
.
‘Hey, Scott? If no one cares and the Flexible
is just going to make the child be the same size it was going to be anyway, then why would I want to use a loose fit? Why not just use the child without wrapping it in a Flexible
?
Hey reader? That’s a really good question and you know what? The only reason I can think of is that it’s one of two things we need to do if we want to fix our Boogeyman RenderFlex error. Other than dealing with “RenderFlex children have non-zero flex but incoming height constraints are unbounded”, no-one I’ve spoken to was able to think of a single reason to use a loose fit, ever.
Back to our example… If the FlexFit
is tight, then the child tries to take up all the leftover space it can get. Remember, “tight” means to push up tight against whatever limit your parent is setting. Again, this is what an Expanded
does, because it’s really just a Flexible
with its FlexFit
hard-coded to tight.
Step 4:
“The cross axis extent of the Flex is the maximum cross axis extent of the children (which will always satisfy the incoming constraints).”
Figuring this one out took three dictionaries and a Gypsy named Wanda with a Tarot deck.
The simple version? The cross axis size of a Colum / Row
is going to be as big its biggest child in that direction. If a child tries to be bigger than that, throw an error. Usually, this will be an overflow that shows you the black and yellow bars but if you nested a Colum / Row
inside of another Colum / Row
, then you might see one of the brothers or sisters of our Boogeyman error:
“BoxConstraints forces an infinite height (or width).” (in the cross axis)
Thank you, Wanda.
Step 5:
“The main axis extent of the Flex is determined by the mainAxisSize property. If the mainAxisSize property is MainAxisSize.max, then the main axis extent of the Flex is the max extent of the incoming main axis constraints. If the mainAxisSize property is MainAxisSize.min, then the main axis extent of the Flex is the sum of the main axis extents of the children (subject to the incoming constraints).”
- If the
MainAxisSize
is set to max (this is default) the length of themainAxis
of yourFlex
/Row/Column
is going to be as large as its parent will allow it to be. - If the
MainAxisSize.min
is being used, then just add up the lengths of all of its children along themainAxis
and then it’s going be that big subject to the incoming restraints.
There’s another one you should never forget: “subject to the incoming restraints.” If you forget that one, you will definitely regret it. Don’t ask how I know that… it’s embarrassing.
Step 6:
“Determine the position for each child according to the mainAxisAlignment and the
crossAxisAlignment.
For example, if themainAxisAlignment
is MainAxisAlignment.spaceBetween
, any main axis space that has not been allocated to children is divided evenly and placed between the children.”
Have you noticed that we still haven’t put anything into the Column
or Row
yet? We’ve figured out how big each child is supposed to be in both the main and cross-axis, and we’ve figured out how big the Column
or Row
could be. But we still haven’t put any children into it… and we can’t do that yet because we have no idea which end of the Row
or Column
we’re supposed to start from.
Ooops.
We’re also going to need to know if we’re supposed to squish the children together, or spread them out and leave some space in between them.
All those things are outside the scope of this article because we’re here to deal with those error messages and the things that cause them. We’re not covering every single thing about Rows
and Columns.
I’ll let someone else write that deep dive.
In the following code, what is the height of the Column
?
So what do you think? 100 +10 = 110 and there’s no flex. The mainAxisSize
is set to min, so the Column
will not try to fill all of the space its parent gives it. Therefore the Column
is going to have a height of 110, right?
Wrong.
Read the Fine Print
Let’s review:
- Step 1: “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.”
Both children have a fixed height and neither uses a flex factor, so they get laid out immediately. There are no other children to place, and the total height of both children is 110.0
- Step 5: “… If the mainAxisSize property is MainAxisSize.min, then the main axis extent of the Flex is the sum of the main axis extents of the children …”
So does that mean the Column
has to be the size of the sum of its children? No, that’s not what it says. But did you forget “never forget this” part two?
“subject to the incoming constraints.”
It’s like you need to be a lawyer to write an app these days… you gotta read the fine print, people. When you put it all back together, the part of Step 5 that has to do with MainAxisSize.min
is: “If the mainAxisSize property is MainAxisSize.min, then the main axis extent of the Flex is the sum of the main axis extents of the children (subject to the incoming constraints).”
What’s happening here is the incoming constraints are telling MainAxisSize.min
to sit down and shut up because they’re going to force our Column
to be as big as the max constraints the parent passed in. So here, the fact that mainAxisSize is set to MainAxisSize.min
doesn't matter, because constraints were passed in.
But wait, the parent Container
of our Column
didn’t have a specified size, so where did the constraints come from?
Passing Constraints Down the Tree
The answer, dear reader who now has a headache, lies in the source code for the Container:
container.dart line 171:
… the widget has a [child] but no `height`, no `width`, no [constraints], and no [alignment], and the [Container] passes the constraints from the parent to the child and sizes itself to match the child.
Clearly, this is only part of a much larger (and even more confusing) set of comments, but the part we care about is in bold. Our parent container has “no `height`, no `width`, no [constraints], and no [alignment]”. So, in our Container
is going to pass on the constraints that were given to it from its parent, and then it will size itself to match its child (our Column
).
Let’s break it all down so it’s easier to see:
- Our
SizedBox
has a height of 500. - The first child is the first
Container.
It has no specified height and its parentSizedBox
is passing a constraint of 500 dp high. TheContainer
is going to pass this 500 dp high constraint to ourColumn.
- The second child is our
Column
, which needs to consider its children before it can calculate a height. It has itsmainAxisSize
set toMainAxisSize.min,
so it will try to shrink itself down to match whatever size its children decide to be. But try to do it is not the same as will do it! - Children 3 and 4 are
Containers
inside of theColumn,
with heights of 100 and 10, for a total height of 110. - The
Column
will say that its children add up to 110 and itsmainAxisSize
is min so it wants to be 110… - Then the parent of the
Column
says, “Nope, you’re subject to the incoming constraints I gave you and that’s how big you’re going to be. Now sit down, be quiet and do as you’re told.” - So, the first
Container
is going to be 100, the second one is 10 and then there is 390 dp of dead space inside theColumn.
When you look at your screen, it looks like the Column
is 110 dp and there is 390 dp of dead space below the Column. But that 390 dp of dead space is actually inside of the Column.
And that’s how a Container
with its mainAxisSize
set to min and 110 dp worth of children still ends up being 500 dp even though there’s no flex in any of this.
But wait! There’s more!
Now Alice, make sure that safety rope is good and tight around your waist because from here on the rabbit hole really starts to get weird…
How tall is the Column
this time? What size is the green Container
going to be? What size is the yellow Container
going to be?
Thinking it Through…
So, the Flexible
with a tight fit is really just an Expanded,
so that’s easy. And the Flexible
with a loose fit will only take up 10, since a loose fit means it doesn’t have to take all the space given.
The SizedBox
is going to pass a constraint of 200 down through the Container,
so we know our Column
is going to be 200 dp high because we learned that in the last Pop Quiz. And if the yellow Container
takes 10, there’s going to be 190 left over. And we all know that an Expanded
is what we use to take up any remaining space in a Column,
so the green one has to be 190, right?
Not even close.
Welcome to Wonderland, Alice. The Laws of Physics don’t apply here.
Behind the Wizards Curtain
Remember that time long, long ago in a part of this article far, far away? That time I had you stick a pin in the phrase “reserved for it” during Step 2?
I also told you to never forget something in Step 3. Did you forget it?
In Step 2 we said:
“We now calculate the actual number of dp that will be reserved for each one, based on what their share of the leftover space could be.”
In Step 3 we said:
“This will allow it to be up to that big, but never bigger than that. Never forget “never bigger than that”. ”
What happened here is this:
- Both
Flexibles
have a flex of 1, so in Step 2 the amount of space that was reserved for each one was the same. They both got reservations for 50% each. So, 100 dp was set aside for each of them, and now they each can never be bigger than that 100 dp. - The green
Container
(on top) is surrounded with aFlexible
that has a tight fit, so it’s going to take every last dp it can get (100). - The yellow
Container
(on bottom) is surrounded with aFlexible
that has a loose fit, so doesn’t care that 100 dp was reserved for it. It’s only going to use 10.
End result:
- The
Column
is 200 dp - The green
Container
is 100 dp - The yellow
Container
is 10 dp - There is 90 dp of dead space inside the
Column
I want to be absolutely clear here so that there is no misunderstanding: There are going to be times when your Flex
has unused, extra space inside of it. Even if you put Expanded
into the Flex,
that does not guarantee every pixel of available space is going to get used by the children of the Flex.
The Flex
is usually going to use all of the available space, often because the incoming constraints are forcing it to, but there are going to be times when there is unused leftover space inside of your Flex
… especially if you have a loose fit Flexible
in there.
So when people tell you to use an Expanded
to eat up all of the leftover space in a Row / Column,
you need to remember that if there is a loose fit Flexible
in there then you need to throw everything most people know about Rows, Columns
and Expandeds
… wait for it…
Right down the Rabbit Hole.
And they lived with a horrible migraine forever after.
THE END
You can stalk the author on Twitter at https://twitter.com/scottstoll2017