Lesson 1 of 5
Constraints Go Down, Sizes Go Up
"It is in working within limits that the master first shows himself." — Johann Wolfgang von Goethe, Natur und Kunst (1802)
Flutter's layout model fits in one sentence, and unlike most such claims, the sentence is genuinely sufficient:
Constraints go down. Sizes go up. The parent sets the position.
Every layout surprise in Flutter — the SizedBox that ignores its size, the Column that overflows, the Row whose children vanish, the widget that fills the screen when you wanted it small — is that sentence being applied somewhere you did not expect. Learn it properly once and the debugging becomes mechanical.
This Part measures the model rather than describing it, because the framework's test harness runs the real layout engine and will tell you exactly what any widget receives. We begin with the most useful measurement in the volume.
Before that, it is worth being clear about which tree this is. Part I established three — widgets describe, elements persist, render objects lay out and paint — and everything in this Part happens in the third. Layout is not something widgets do; it is something RenderObjects do, and the widgets you write are descriptions that produce them. That matters for debugging, because the error messages in this Part all name render objects rather than widgets, and the correspondence is not one-to-one: your Center is a RenderPositionedBox, your Padding is a RenderPadding, and a great many widgets produce no render object at all.
It also explains why the model is so uniform. There are around 197 render object classes against 850 widget classes, and every one of those 197 implements the same protocol — receive constraints, report a size. A Row and a Text and a CustomPaint differ enormously in what they do and not at all in how they are asked.
A widget in Flutter cannot know where it is. It is told what sizes it may be, it picks one, and its parent places it — so position is information the widget never has, deliberately. That makes layout composable and locally reasonable, and it makes a whole category of question unanswerable from inside a widget: am I near the bottom of the screen? is something overlapping me? how far have I scrolled? Each has an answer available by other means, and none is available where you would naturally ask. It is worth asking whether that restriction buys enough to justify how often people go looking for the thing it withholds.
One widget, six parents, four sizes
SizedBox(width: 200, height: 100) is about as unambiguous as a request gets. Here is what it actually measures, placed under different parents.
Edit here, then open the pad to run it on dartpad.dev.
Commissioning a piece of furniture for an alcove. You do not tell the carpenter how big to make the shelf; you tell them the alcove is between 80 and 90 centimetres wide and no more than 200 tall, and they build something that fits and tell you what they made. Then you decide where in the alcove it sits. Three separate decisions belonging to three different parties, and none of them can be made by the others — the carpenter cannot know the alcove, you cannot know the joinery, and neither of you decides the position until the piece exists. What makes the arrangement work is that the constraint travels one way and the answer travels back the other, so nobody needs to hold the whole problem. And what makes it frustrating is exactly the same thing: when the alcove is specified as "exactly 85 wide", the carpenter's opinion about the ideal width is not merely overruled, it is never asked for.
What each parent hands down
The way to see the model directly is to measure the constraints a child receives, which LayoutBuilder will report.
Edit here, then open the pad to run it on dartpad.dev.
Single-pass constraint-based layout is a deliberate rejection of what browsers do, and the reason is performance measured in the worst case rather than the average. CSS layout can require multiple passes — a float may affect what came before it, a percentage height may depend on a parent whose height depends on its children, and resolving that in general means iterating until it settles. That is fine most of the time and produces the pathological cases every web developer has met, where one element's size oscillates or a page reflows visibly as it loads. Flutter's model forbids the loop by construction: information flows down as constraints and back up as sizes, exactly once per node per frame, so layout is linear in the number of render objects and there is no case where it is not. The price is expressiveness, and it is a real price — you cannot say "as wide as my sibling" or "as tall as the tallest of these" without an explicit mechanism for it, because neither is answerable in a single downward pass. IntrinsicHeight and friends exist as escape hatches for exactly that, and the documentation warns that they are expensive, which is the framework admitting it has reintroduced the thing it was avoiding.
Tight, loose, and why the distinction is the whole game
Edit here, then open the pad to run it on dartpad.dev.
The measurement that reorganised this chapter was the SizedBox at the root reporting Size(800.0, 600.0). I knew the rule, I could recite it, and I still half-expected 200 by 100 — because a SizedBox with explicit width and height reads like an instruction rather than a request. What it actually compiles to is a RenderConstrainedBox holding BoxConstraints.tightFor(width: 200, height: 100), and the only thing it does with those is enforce them against whatever came down from above. When the incoming constraint is tight, enforcing anything against it changes nothing, because there is nothing to change: the permitted range is a single point. So the widget is not ignored, exactly — it is applied, and it has no effect. That distinction matters for debugging, because it means there is never a bug to find in the SizedBox. The information you want is always further up, in whichever ancestor decided the range, and I have watched people (myself included) stare at the widget that displays the wrong size rather than the one that caused it.
The three steps, walked
Edit here, then open the pad to run it on dartpad.dev.
Seeing constraints live
Edit here, then open the pad to run it on dartpad.dev.
Every box shows the same widget reporting a different answer. Nothing about Probe differs between them; the only variable is what it was allowed to be.
One detail in that block is worth a note, because it is a genuine oddity I only found by compiling it: ConstrainedBox has no const constructor. Every other widget in the demo does, and ConstrainedBox looks like it should, and it does not:
class ConstrainedBox extends SingleChildRenderObjectWidget {
ConstrainedBox({super.key, required this.constraints, super.child})
: assert(constraints.debugAssertIsValid());The reason is the assertion. A const constructor's initialiser list may not call arbitrary methods, and debugAssertIsValid() is one — so validating the constraints in debug builds costs the widget its constancy in every build. That is a small, concrete instance of Part II's rules arriving somewhere real: const is not a modifier you can add wherever you like, and a framework primitive lost it to a debug check.
The practical consequence is minor and worth knowing: a subtree containing a ConstrainedBox cannot be const all the way up through it, so the const boundary stops there. If you need one and the constancy matters, SizedBox and BoxConstraints can both be const, and a ConstrainedBox with a const child still lets that child be skipped.
Where the rule runs out
The model is genuinely sufficient for describing what happens, and there are three places where knowing it is not enough on its own, each of which gets a chapter later in this Part.
Unbounded constraints. A Column passes unbounded height to its children, so a child that wants to fill the available space has nothing to fill. Put a Column inside a Column, or anything inside a SingleChildScrollView, and widgets that behaved perfectly elsewhere start throwing. The measurement table above shows exactly where the infinities come from.
Shared axes. When a Row has several children, the available width has to be divided, and the rule as stated says nothing about how. Expanded and Flexible are the mechanism, and the measurement above already hints at what Expanded does — it converts a loose main-axis constraint into a tight one, which is why a SizedBox inside an Expanded loses its width.
Intrinsics. Sometimes a parent genuinely needs to know how big a child would like to be before deciding what to permit — "make all these cards as tall as the tallest". That question is unanswerable in one downward pass, which is why IntrinsicHeight exists and why it is documented as expensive: it runs a speculative extra pass to ask.
Everything else follows from the sentence. It is worth resisting the urge to learn layout as a collection of widget behaviours — Center does this, Expanded does that — because the list is long and the rule is short, and each widget's behaviour is a one-line consequence of it: Center loosens, Padding deflates, Expanded tightens, SizedBox enforces, scroll views unbind their axis.
Reading a layout error
The model also tells you how to read the framework's layout errors, which are unusually informative once you know what they are describing.
A RenderBox was not laid out or an overflow message names a render object and reports the constraints it was given. That is the input, not the output — so the question it answers is "what was this thing allowed to be", and the fix is nearly always in whatever produced that constraint rather than in the widget named.
The pattern to look for in the numbers:
- An
Infinityin the constraint means an unbounded axis, so an ancestor is a scroll view or an unboundedRow/Column, and a descendant tried to fill it. - Equal min and max means tight, so a descendant's requested size was discarded and the ancestor that tightened is the one to change.
- A size larger than the constraint permitted is the overflow case, where a child insisted on more room than it was given.
None of those require understanding the widget that failed. They require finding the ancestor that set the number, which is a search up the parent chain and is exactly what the inspector's layout explorer displays.
There is one more habit worth building, and it is the cheapest diagnostic in the framework. When a size is wrong and you cannot see why, drop a LayoutBuilder directly around the widget in question and print the constraints — the same technique that produced this chapter's tables:
LayoutBuilder(builder: (context, constraints) {
debugPrint('$constraints');
return TheWidgetThatIsWrong();
})Three lines, removed afterwards, and it answers the only question that matters. It works because LayoutBuilder is not an inspection tool bolted on for debugging — it is an ordinary widget that receives constraints and is given the opportunity to build differently depending on them, so printing them is just using it for its own purpose and discarding the interesting half.
The reason to prefer this over reasoning is that the parent chain in real code is rarely the one in your head. Widgets arrive from helper methods, from packages, from a Scaffold that wraps its body in things you did not write — and any one of those can be where the constraint was decided.
The debugging habit this supports is worth more than the theory, and it is one question asked in one direction: what constraint is reaching this widget, and which ancestor decided it? Almost every layout problem people bring to a colleague is being investigated from the wrong end — staring at the widget whose size is wrong, adding and removing properties on it, when the widget is doing exactly what it was told by something three levels up. Dropping a LayoutBuilder in temporarily and printing the constraints is usually a thirty-second answer, and it works because it reports the input rather than the output. The same applies to the widget inspector's layout explorer, which shows the constraint chain directly and is chronically under-used. The broader point is that Flutter's layout errors are unusually diagnosable compared with CSS — there is always exactly one answer to "why is it this size", it is always the parent chain, and the framework will tell you if you ask it rather than guessing.
Common Mistakes
- Expecting
SizedBoxto always win. Under tight constraints it is applied and has no effect. - Debugging the widget whose size is wrong. The cause is the ancestor that set the constraint.
- Thinking
Centercentres by magic. It loosens the constraint, then positions the result. - Assuming
Paddinggives its child freedom. Tight in means tight out, just smaller. - Forgetting constraints are per-axis.
Expandedcan force width while leaving height loose. - Missing that
RowandColumnare unbounded on the main axis. That is the overflow chapter. - Expecting a widget to know where it is. Position is assigned by the parent and never travels down.
- Reaching for
IntrinsicHeightcasually. It reintroduces the extra pass the model exists to avoid. - Reading "bounded" as "loose". A
minabove zero clamps upward as well as down. - Guessing instead of measuring. A temporary
LayoutBuilderanswers it in thirty seconds.
Interview Corner
SizedBox asks for 200 pixels and its parent passed down a constraint permitting exactly 800, it will be 800.SizedBox not get the size it asks for?SizedBox compiles to a RenderConstrainedBox that enforces its requested constraints against the incoming ones, and when the incoming constraint has min equal to max there is only one legal size, so enforcing anything against it changes nothing. I have measured this: the same SizedBox(width: 200, height: 100) reports 800 by 600 at the root of an 800 by 600 surface, and 200 by 100 inside a Center — because Center loosens the constraint before passing it down. The fix is to introduce something that loosens — Center, Align, or UnconstrainedBox — or to accept that the parent has legitimately decided. And it is per-axis: inside a Row with Expanded, the same widget gets a forced width and a honoured height, because Expanded tightens only the main axis.IntrinsicHeight and IntrinsicWidth, and the documentation warns they are expensive, which is an honest admission that they reintroduce the extra pass the model was designed to eliminate. The other cost is conceptual: a widget cannot know its own position, so questions like "am I on screen" have no answer where you would naturally ask them, and people go looking for that a lot more often than the design seems to anticipate.SizedBox that did nothing.Hands-on
Build the model. Implement classify, naming a constraint; resolve, computing the size a child actually gets; loosen and deflate, reproducing what Center and Padding pass down; enforce, the operation behind SizedBox; and whoDecided, pointing the blame at the right ancestor. The checks below run either way — a stub scores zero, so a passing run means the behaviour is real.
Edit here, then open the pad to run it on dartpad.dev.
Summary
- Constraints go down, sizes go up, the parent sets the position. The whole model.
- Measured: one
SizedBox(200×100)produced four different sizes under six different parents. - At the root it measured 800×600 — the request was applied and had no effect.
Centerloosens, which is why the sameSizedBoxinside one measures 200×100.Paddingdeflates but stays tight — tight in means tight out, just smaller.- Constraints are per-axis:
Expandedforces a tight width while leaving height loose. - Tight means min equals max, and a tight constraint is not a suggestion.
- Unbounded means the child decides alone, which is where overflows come from.
- A widget never knows its position — that is assigned by the parent after sizes are known.
- Diagnose upward. The wrong size is caused by the ancestor that chose the constraint.
Next: Row, Column and Flex — the multi-child case, where Expanded and Flexible decide how a shared axis is divided, measured against the real engine.
What I find genuinely elegant here is that the model's hardest limitation and its best property are the same fact. Information flows one way, so layout cannot loop, so it is linear and predictable and has no pathological cases — and information flows one way, so a widget cannot ask about anything outside its own subtree, which rules out an entire vocabulary of layout that designers use casually. "Match the height of the tallest card" is a normal sentence in a design review and a genuinely awkward request in Flutter. I notice the framework does not pretend otherwise: IntrinsicHeight exists, it does what you want, and the documentation says plainly that it is expensive because it requires an extra pass. That is more honest than the alternative of providing it quietly and letting people discover the cost in a profile. But it does mean the model has a shape you can feel through the API — a set of things that are easy, a set that are possible with a warning attached, and a boundary between them that is not where a designer's intuition would put it.
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.