Demystifying Intrinsic Size in Flutter

Lê Dân
6 min readApr 6, 2021

--

Introduction

As you may know, one of the most important things to understand when developing your UI in Flutter is this sentence:

Constraints go down. Sizes go up. Parent sets position.

If you haven’t, I suggest you read this article before continuing.

And as always, I came a little late to the party and only learned about this a year into my Flutter career.

In some scenarios though, maybe it’s worth diving a little deeper.

Consider this conversation between a parent Widget and its child:

Child: Mom, Dad, how big should I be?

Parent: Hey buddy, you just need to be between certain sizes. It's alright, we won't be too harsh on you.

Child: Well, since I am a little young and have no children yet. I decided that…I just want to be myself.

This raises a stoicism-ish question:

In terms of sizing, what does it mean for a Widget to be itself?

Widgets and their intrinsic sizes

Example 1:

Scaffold(
body: Text("Hello Word"),
);

If we inspect the RenderObject of Text, we can see this:

constraints: BoxConstraints(0.0<=w<=202.0, 0.0<=h<=165.0)
size: Size(77.0, 16.0)

Here, Scaffold tells Text to be between (0,0) and (202, 165). And then Text decided to be (77, 16).

We can say that (77, 16) is Text's intrinsic size.

in·trin·sic (adjective): belonging naturally; essential.

What’s going on under the hood

In Flutter, a Widget is only served as a configuration, and the hard parts of sizing, layout, and rendering are done by a RenderObject.

The most common type of RenderObject is RenderBox.

And when a RenderBox has certain sizes they want to be in, it will be called its intrinsic size.

Intrinsic sizing is one of RenderBox's additional protocols that its derived classes must implement.

RenderBox intrinsic sizing protocol is about 4 values:

  • minIntrinsicWidth: the minimum width that this box could be without failing to correctly paint its contents within itself, without clipping.
  • minIntrinsicHeight: the minimum height that this box could be without failing to correctly paint its contents within itself, without clipping.
  • maxIntrinsicWidth: the smallest width beyond which increasing the width never decreases the preferred height
  • maxIntrinsicHeight: the smallest height beyond which increasing the height never decreases the preferred width

In Example 1, since the incoming constraints are bigger than what Text needed, Text will use its maxIntrinsicWidth, which is 77, to calculate its intrinsic height to be 16. Text defines its intrinsic height to be the height to layout one line of text.

Nevertheless, for the sake of simplicity, whenever I mention a Widget it's a Widget's associated RenderObject that I'm talking about.

When would a Widget use its intrinsic size?

Now what happens if the incoming width constraints are smaller than Text's maxIntrinsicWidth ?

Example 2

Scaffold(
body: Container(
child: Text("Hello World"), // maxIntrinsicWidth = 77
width: 50.0,
),
);
// Text's RenderObject properties
constraints: BoxConstraints(w=50.0, 0.0<=h<=165.0)
size: Size(50.0, 32.0)

Container is now passing down a tight width constraint of 50. Text will respect this and use this width along with its maxIntrinsicWidth to calculate its needed height of 32 which is two lines of text.

Example 3

Scaffold(
body: Container(
height: 10,
child: Text("Hello World"),
),
);
// Text's RenderObject properties
constraints: BoxConstraints(0.0<=w<=284.0, h=10.0)
size: Size(77.0, 10.0)

Container is now passing down a tight height constraint of 10 . Since the width is unbounded,Text will default to its maxIntrinsicWidth . Text will then respect the height constraint of 10 , "Hello World" will be cropped because its minIntrinsicHeight is 16

Example 4

Scaffold(
body: Container(
width: 14.0,
child: Text("Hello World"),
),
);
// Text's RenderObject properties
constraints: BoxConstraints(w=14.0, 0.0<=h<=797.0)
size: Size(14.0, 96.0)

Container tells Text to have a tight width of 14. Text respects the constraints and calculated a whopping height of 96 in order to show all the words.

Now you don’t know this, but through some logging magic, I can tell you that 14 is the Text's minIntrinsicWidth and it is calculated by getting the width of the largest character in the text: W.

If we reduce the width furthermore, Text will still try to show all the texts without cropping anything. Text will even overflow the parent Container . I'm not sure why this is the intended behavior though.

As we can see from Example 2, 3 & 4, some Widgets, upon receiving the constraints from their parents, will use their intrinsic size to resolve and calculate their sizes.

Not all Widget will have an intrinsic size

Text is one of the most popular Widgets with an intrinsic size.

Another one is Image. An Image widget's intrinsic size is the width and height of the associated image.

But consider a ConstrainedBox. Since it's a proxy Widget, meaning it only resembles its child, its intrinsic size is calculated using the incoming constraints along with its child's intrinsic size.

A Flex widget will also use its children's intrinsic sizes to calculate its own.

Row(
children: [
Container(height: 100, width: 100, color: Colors.green,),
Text("Hello World"),
Container(height: 50, width: 50, color: Colors.yellow,),
],
)

This Row, for example, will have:

  • It’s maxIntrinsicHeight to match the child with the largest maxIntrinsicHeight. In this case, it's the first Container height which is 100.
  • It’s maxIntrinsicWidth to match the combined maxIntrinsicWidth of all the children, which is 100 + 77 + 50 = 227.

Some Widgets will have their own intrinsic sizes, others will borrow from their children.

IntrinsicHeight and IntrinsicWidth widgets

If we take the Row above and set its properties to CrossAxisAlignment.strech MainAxisAlignment.spaceBetween.

Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
height: 100,
width: 100,
color: Colors.green,
),
Text("Hello World"),
Container(
height: 50,
width: 50,
color: Colors.yellow,
),
],
)

The Row will fill the whole screen both vertically and horizontally.

Then let’s try to wrap this Row in a IntrinsicHeight widget.

IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
height: 100,
width: 100,
color: Colors.green,
),
Text("Hello World"),
Container(
height: 50,
width: 50,
color: Colors.yellow,
),
],
),
)

The Row will now stop expanding vertically to the whole screen and only stretch all its children to a height of 100, which is, of course, the Row's maxIntrinsicHeight.

IntrinsicHeight first asks for its child's maxIntrinsicHeight and then combines it with the incoming constraints to force the child to, basically, be itself as much as possible.

The same goes for IntrinsicWidth as well.

IntrinsicHeight / IntrinsicWidth are useful when unlimited height/width is available and you would like a child that would otherwise attempt to expand infinitely to instead size itself to a more reasonable height/width

However, you should only use these when it’s absolutely needed:

This class is relatively expensive because it adds a speculative layout pass before the final layout phase. Avoid using it where possible. In the worst case, this render object can result in a layout that is O(N²) in the depth of the tree.

Conclusion

  • A Widget's intrinsic size is the natural the Widget would take if there aren't any constraints.
  • Some Widgets will have their own intrinsic sizes, others will borrow from their children.
  • Use IntrinsicHeight/IntrinsicWidth to give a child constraints as close to that child's intrinsic size as possible.

--

--