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:
“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.
CrossAxisAlignment parameter is set to stretch, then make the child as big as its parent will allow in the cross axis.
“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
flexis calculated. Here, 1 + 4 + 5 = 10, so we have 10
- The first child has a
flexof 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.
“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.fitproperties that are FlexFit.tight are given tight constraints (i.e., forced to fill the allocated space), and children with
Flexible.fitproperties 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 a
‘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
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.
“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.
“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
MainAxisSizeis set to max (this is default) the length of the
Row/Columnis going to be as large as its parent will allow it to be.
- If the
MainAxisSize.minis being used, then just add up the lengths of all of its children along the
mainAxisand 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.
“Determine the position for each child according to the mainAxisAlignment and the
crossAxisAlignment.For example, if the
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
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
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
Column we’re supposed to start from.
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
Columns. I’ll let someone else write that deep dive.
In the following code, what is the height of the
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?
Read the Fine Print
- 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.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
Let’s break it all down so it’s easier to see:
SizedBoxhas a height of 500.
- The first child is the first
Container.It has no specified height and its parent
SizedBoxis passing a constraint of 500 dp high. The
Containeris going to pass this 500 dp high constraint to our
- The second child is our
Column, which needs to consider its children before it can calculate a height. It has its
MainAxisSize.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
Containersinside of the
Column,with heights of 100 and 10, for a total height of 110.
Columnwill say that its children add up to 110 and its
mainAxisSizeis min so it wants to be 110…
- Then the parent of the
Columnsays, “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
Containeris going to be 100, the second one is 10 and then there is 390 dp of dead space inside the
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
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…
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.
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:
Flexibleshave 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 a
Flexiblethat 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 a
Flexiblethat has a loose fit, so doesn’t care that 100 dp was reserved for it. It’s only going to use 10.
Columnis 200 dp
- The green
Containeris 100 dp
- The yellow
Containeris 10 dp
- There is 90 dp of dead space inside the
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 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.
You can stalk the author on Twitter at https://twitter.com/scottstoll2017