Flutter Deep Dive, Part 4: “RenderFlex children have non-zero flex…”

Deep Dive Part 4, by Scott Stoll. Twitter: @scottstoll2017

This article is part of a four-part series. You can find the others here:

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 10 flex 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 with Flexible.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 the mainAxis of your Flex / 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 the mainAxis 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 the mainAxisAlignment 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 Columnor 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 Columnor 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 parent SizedBox is passing a constraint of 500 dp high. The Container is going to pass this 500 dp high constraint to our Column.
  • The second child is our Column, which needs to consider its children before it can calculate a height. It has its mainAxisSize set to 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 Containers inside of the Column, with heights of 100 and 10, for a total height of 110.
  • The Column will say that its children add up to 110 and its mainAxisSize 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 the Column.

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 a Flexible 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 a Flexible 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 Flexespecially 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