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 heightmaxIntrinsicHeight
: 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 largestmaxIntrinsicHeight
. In this case, it's the firstContainer
height which is100
. - It’s
maxIntrinsicWidth
to match the combinedmaxIntrinsicWidth
of all the children, which is100
+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 theWidget
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.