devShakib

LayoutBuilder Does Not Support Returning Intrinsic Dimensions

Why auto sizing text throws this in a Table cell or IntrinsicHeight — a framework limit rather than a bug — and how to size text without LayoutBuilder.

You put auto-sizing text in a Table cell, hot-reloaded, and got this:

LayoutBuilder does not support returning intrinsic dimensions.Calculating the intrinsic dimensions would require running the layoutcallback speculatively, which might mutate the live render object tree.  at _RenderLayoutBuilder.computeMaxIntrinsicHeight

The stack trace is long, it points at framework code you did not write, and the

widget worked perfectly ten minutes ago in a Column. So the obvious first

thought is that something is misconfigured.

Nothing is misconfigured. This is Flutter refusing to do something it genuinely

cannot do, and understanding why tells you exactly which layouts will break and

which will not — which is more useful than a workaround, because the same wall

is waiting behind three or four different widgets.

What an intrinsic dimension actually is

Most Flutter layout is a single downward pass. A parent hands its child a set of

constraints — you may be between 0 and 300 logical pixels wide — the child

picks a size within them, and the parent positions it. One pass, no

backtracking, which is why Flutter's layout is linear in the number of widgets

rather than quadratic.

Some widgets cannot work that way. IntrinsicHeight has to make all its

children the height of the tallest one, so it has to know how tall each wants

to be before it can decide what to hand them. Table sizes a column to fit its

widest cell by default. Baseline alignment needs to know where text sits before

it can align on it.

For those, Flutter has a second protocol. A parent can ask a child a hypothetical

question:

If I gave you 200 pixels of width, how tall would you want to be?

That question is computeMaxIntrinsicHeight, and the crucial part is the word

hypothetical. Nothing is being laid out. The child is being asked to predict a

size for constraints it has not been given and may never be given. The answer

must be computed without touching the render tree, because the tree is in the

middle of a layout pass and nothing may move yet.

Why LayoutBuilder cannot answer it

LayoutBuilder exists to run your code with the constraints as an argument:

LayoutBuilder(  builder: (context, constraints) {    final size = _bestFontSizeFor(constraints.maxWidth);    return Text(label, style: TextStyle(fontSize: size));  },)

This is how essentially every auto-sizing text package works, and it is a

perfectly good idea. It measures the text at a candidate size, checks whether it

fits the constraints it was handed, and adjusts.

Now look at what answering an intrinsic query would require. To tell the parent

how tall it would be at 200 pixels wide, LayoutBuilder would have to **run your

builder callback** with maxWidth: 200. Your callback constructs widgets. Those

widgets get mounted into the element tree. That is a mutation — during a

speculative query, for a layout that may never happen, possibly several times

with different widths as the parent searches for a fit.

Flutter's own comment on the matter is blunt about it: doing so *"might mutate

the live render object tree"*. So it refuses, loudly, rather than corrupting your

layout quietly. The error is the framework protecting you.

This is why it is not going to be fixed. It is not an oversight, it is the

boundary between two layout protocols that cannot both be satisfied by the same

widget.

The two-pass problem, and why Flutter avoids it

There is a deeper reason this protocol exists at all, and knowing it makes the

constraint feel less arbitrary.

Web layout is famously able to do this sort of thing — a CSS table sizes its

columns to content without anyone thinking about it. It manages that by doing

multiple passes over the tree, and by having a layout engine that can revisit

decisions.

Flutter deliberately does not. Its layout is single-pass and linear:

constraints go down, sizes come back up, each render object is visited once.

That is a large part of why Flutter can hit 60 or 120fps on a deep tree — there

is no risk of a pathological case where a small change triggers repeated

re-layout of a large subtree.

The intrinsic protocol is the pressure valve. It lets a parent ask a question

without triggering a real layout pass, at the cost of the query being a pure

computation with no side effects. Widgets that use it are explicitly documented

as more expensive, and IntrinsicHeight's own API docs warn that it can be

O(N²) in the depth of the tree.

So when Flutter refuses to let LayoutBuilder participate, it is protecting the

guarantee that makes the whole layout system fast. Allowing it would mean

speculative builder invocations, which is a second pass through user code — the

exact thing the architecture is designed to avoid.

Where this bites

The failure only appears when something above your widget asks an intrinsic

question. That makes it feel random — the same widget works in one screen and

throws in another. It is not random at all. These are the layouts that ask:

| Widget | Why it asks |

| --- | --- |

| IntrinsicHeight | Matches children to the tallest one |

| IntrinsicWidth | Matches children to the widest one |

| Table | Columns size to their widest cell by default |

| Baseline-aligned Row | Needs the text baseline before aligning |

| ListView with shrinkWrap in some nestings | Sizes to content |

Table is the one that catches people, because nothing about writing

Table(children: [...]) suggests you have just opted into a second layout

protocol. You get a table. You put a heading in a cell. It explodes.

How to tell before you hit it

You do not need to memorise the table. There is a quicker heuristic: **if a

widget needs to know its children's sizes before deciding what constraints to

give them, it will use intrinsics.**

Column does not — it hands down its own constraints and takes whatever comes

back. IntrinsicHeight does, by definition. Table does, because a column width

is a function of every cell in it.

A second signal is the error itself. If a stack trace mentions

computeMinIntrinsicWidth, computeMaxIntrinsicWidth,

computeMinIntrinsicHeight or computeMaxIntrinsicHeight, you are in the

intrinsic protocol regardless of which widget triggered it.

The fix: do the fitting in a RenderBox

The way out is to stop asking the widget layer to measure things and drop to the

layer that is allowed to. A RenderBox can measure text with a TextPainter

whenever it likes, because a TextPainter is not part of the render tree — it is

a standalone object that lays out a paragraph and reports its size. No mutation,

no speculative element mounting.

That means the fitting can happen in two places that both work:

measuring with a TextPainter is exactly the kind of pure computation the

intrinsic protocol requires

The binary search for the largest font size that fits is the same algorithm the

LayoutBuilder approach uses. The difference is entirely about where it runs.

This is what I built fit_text to do:

FitText('A headline that must never wrap', maxLines: 1, minFontSize: 12)

It works inside Table, IntrinsicHeight, IntrinsicWidth, baseline rows,

Expanded, and unbounded constraints. The package's test suite contains a case

that puts a LayoutBuilder-based sizer and a FitText in the same widget tree

and asserts that the first throws while the second lays out — so the difference

is pinned by a test rather than claimed in a README.

It has no dependencies, and it runs on all six platforms.

Why TextPainter is allowed and LayoutBuilder is not

The distinction is worth stating precisely, because it is the whole trick.

TextPainter is a standalone object. Constructing one and calling layout()

on it does not touch the render tree, does not mount elements, does not schedule

a frame, and has no effect on anything outside the object itself. It is a pure

function from (text, style, constraints) to a size, with some caching inside.

LayoutBuilder is the opposite. Its entire purpose is to run a callback that

builds widgets, and building widgets means creating elements and mounting them

into the tree. That is a side effect, and side effects are exactly what an

intrinsic query is forbidden to have.

So the rule generalises beyond text. **If you can compute your intrinsic size

with a pure function, you can implement the intrinsic protocol.** If computing

it requires building widgets, you cannot, and no amount of cleverness at the

widget layer will change that. Drop to a render object and compute directly.

If you would rather not add a package

You do not have to. The approach is the point, and it is reproducible:

didExceedMaxLines and the resulting size against the constraints.

computeMinIntrinsicHeight and computeMaxIntrinsicHeight using the same

measurement, and do not touch the tree in any of them.

than you expect, sometimes several times per frame.

Point five is the one people miss. Table may ask each cell more than once while

resolving column widths, and a binary search over twenty font sizes per query

adds up quickly in a list.

The subtleties that bite in a custom implementation

If you do write your own, these are the ones that cost an evening each:

maxLines and still be wider than the constraint if a single word cannot

break. Check the painted size against the constraints as well as the line

count, or a long unbroken token will silently overflow at your minimum size.

font size will not invalidate a cache keyed only on the string and the box.

from the ambient Directionality. In a render object you have to thread it

through yourself.

happily spend iterations distinguishing 14.001 from 14.002. Search in whole or

half points and stop.

deliberately whether to overflow, clip or ellipsise — and make it a parameter,

because different call sites genuinely want different answers.

The short version

LayoutBuilder cannot answer intrinsic queries because answering them means

running your builder, and running your builder means mutating the tree during a

speculative measurement. Any auto-sizing built on LayoutBuilder inherits that

limit. Move the measurement into a RenderBox with a TextPainter and the

limitation disappears, because measuring a paragraph was never the thing that

needed the tree.

If you want it already done: fit_text on pub.dev, MIT

licensed, no dependencies.