iTranslated by AI
Understanding Flutter's Layout Calculation with SizedBox
Let's start with a quick question.
What kind of UI do you think will be produced when the following Flutter code is executed?
void main() => runApp(
const MaterialApp(
home: Scaffold(
body: SizedBox(
// Inside a box with width 300 and height 500
width: 300,
height: 500,
child: SizedBox(
// A box with width 100 and height 200
width: 100,
height: 200,
// Place a blue box
child: ColoredBox(color: Colors.blue),
),
),
),
),
);
The answer is below.

Since the device screen size is 390x844, placing a 100x200 blue box as specified should make it appear smaller than half the screen, but that is not what happened. The 300x500 specified by the parent of the 100x200 SizedBox is being applied.
In this article, we will deepen our understanding of Flutter's layout calculation mechanism by reading through the documentation and code to consider why this behavior occurs.
By the way, this article is for Day 2 of the Flutter Advent Calendar 2022. I'm sorry for posting this a full month late...
Recommended Reading
Before reading this article, I'd like to introduce a few articles from the 2022 Flutter Advent Calendars that cover similar topics.
All of these are great articles for deepening your understanding of layout calculation and painting with RenderObject, so please check them out.
Main Topic
Now, let's think about why the blue box was rendered at 300x500 in the initial code.
Constraints and Size
The foundation of Flutter's layout calculation is the concept: Constraints go down. Sizes go up. Parent sets position..
A parent Widget (or the RenderObject it creates) passes Constraints to its child Widget (or its RenderObject), and the child returns its own size based on those constraints. Finally, the parent decides where to place the child based on that size.
While there are several important points, two in particular that we application developers should keep in mind when building UI are:
- The size of a Widget is calculated based on the constraints provided by its parent.
- How those constraints are used depends on the implementation of the individual Widget (and the RenderObject it creates).
In this article, we will use SizedBox and its corresponding RenderObject, RenderConstrainedBox, as examples to understand these two points by actually reading the code.
BoxConstraints with Minimum/Maximum Width/Height
A class named Constraints is defined in the framework's object.dart, as its name suggests.
However, this is an abstract class, and what RenderConstrainedBox actually handles is an object called BoxConstraints, which inherits from Constraints.
BoxConstraints is an object that holds the "minimum size" and "maximum size" of a box. Here is a partial excerpt of the implementation:
class BoxConstraints extends Constraints {
/// The minimum width that satisfies the constraints.
final double minWidth;
/// The maximum width that satisfies the constraints.
///
/// Might be [double.infinity].
final double maxWidth;
/// The minimum height that satisfies the constraints.
final double minHeight;
/// The maximum height that satisfies the constraints.
///
/// Might be [double.infinity].
final double maxHeight;
}
What is important to note is that this object strictly holds information like "Please create a layout with a size somewhere between here and here." It is not the final size that will be displayed on the screen.
The actual size at which a Widget is rendered on the screen is determined by the results of the layout calculations performed by RenderConstrainedBox based on these BoxConstraints.
The performLayout() Method for Calculating Layout
RenderObject implements the layout calculation logic for each concrete class by overriding the performLayout() method. In other words, we can understand why the SizedBox specified as 100x200 was not displayed at that size by following this method.
The performLayout() method of RenderConstrainedBox is implemented as follows:
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
if (child != null) {
child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
size = child!.size;
} else {
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}
Since in this case child is not null, let's focus on the first conditional branch.
Note that in our code, child refers to the ColoredBox(color: Colors.blue) (and the RenderObject it creates) passed as the child of the SizedBox.
Extracting only the code inside the first branch gives us the following two lines:
child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
size = child!.size;
Basically, it is a very simple process that says "set its own size to the size obtained from the child's layout calculation," but what we want to pay attention to is the content of the BoxConstraints passed as the first argument of child.layout().
Since it is written as _additionalConstraints.enforce(constraints), let's check what _additionalConstraints, constraints, and enforce() are respectively.
_additionalConstraints
Following the code, _additionalConstraints is the object passed to the constructor when creating RenderConstrainedBox in the createRenderObject() method of SizedBox.
@override
RenderConstrainedBox createRenderObject(BuildContext context) {
return RenderConstrainedBox(
additionalConstraints: _additionalConstraints,
);
}
BoxConstraints get _additionalConstraints {
return BoxConstraints.tightFor(width: width, height: height);
}
width and height are the exact values passed to the constructor when placing the SizedBox. In this case, we specified width: 100 and height: 200.
It seems that the BoxConstraints object is generated from the received width and height by calling the BoxConstraints.tightFor() constructor. Let's check the implementation of .tightFor() as well.
const BoxConstraints.tightFor({
double? width,
double? height,
}) : minWidth = width ?? 0.0,
maxWidth = width ?? double.infinity,
minHeight = height ?? 0.0,
maxHeight = height ?? double.infinity;
When both width and height are specified as in this case, it seems both max and min will have the provided values. In BoxConstraints, "tight" refers to a state where "the minimum and maximum values are the same." The opposite is "loose."
When the minimum constraints and the maximum constraint in an axis are the
same, that axis is tightly constrained. See: [
BoxConstraints.tightFor], [BoxConstraints.tightForFinite], [tighten],
[hasTightWidth], [hasTightHeight], [isTight].An axis with a minimum constraint of 0.0 is loose (regardless of the
maximum constraint; if it is also 0.0, then the axis is simultaneously tight
and loose!). See: [BoxConstraints.loose], [loosen].
https://api.flutter.dev/flutter/rendering/BoxConstraints-class.html
In summary, _additionalConstraints is a "BoxConstraints object that holds the provided width and height as both minimum and maximum values." [1]
constraints
constraints is this.constraints, as mentioned in the first step of performLayout(). If you jump to the definition, it is written as follows:
/// The box constraints most recently received from the parent.
@override
BoxConstraints get constraints => super.constraints as BoxConstraints;
From the comment, we can see that it is the "BoxConstraints object received from the parent."
In this case, the parent is the SizedBox that specified 300x500, so (omitting various details) it is a "BoxConstraints object where both minimum and maximum values are set to 300x500."
.enforce()
Finally, there is the .enforce() method. This is a method of the _additionalConstraints object, and the constraints received from the parent are passed as an argument.
The implementation is as follows:
/// Returns new box constraints that respect the given constraints while being
/// as close as possible to the original constraints.
BoxConstraints enforce(BoxConstraints constraints) {
return BoxConstraints(
minWidth: clampDouble(minWidth, constraints.minWidth, constraints.maxWidth),
maxWidth: clampDouble(maxWidth, constraints.minWidth, constraints.maxWidth),
minHeight: clampDouble(minHeight, constraints.minHeight, constraints.maxHeight),
maxHeight: clampDouble(maxHeight, constraints.minHeight, constraints.maxHeight),
);
}
Note that clampDouble is a method that performs the same process as num.clamp. If the value received as the first argument is within the range of the minimum value in the second argument and the maximum value in the third argument, it returns the value as is; otherwise, it returns the value of the minimum or maximum it was closest to.
double clampDouble(double x, double min, double max) {
assert(min <= max && !max.isNaN && !min.isNaN);
if (x < min) {
return min;
}
if (x > max) {
return max;
}
if (x.isNaN) {
return max;
}
return x;
}
In other words, applying our values results in the following:
return BoxConstraints(
minWidth: clampDouble(100, 300, 300), // -> 300
maxWidth: clampDouble(100, 300, 300), // -> 300
minHeight: clampDouble(200, 500, 500), // -> 500
maxHeight: clampDouble(200, 500, 500), // -> 500
);
As you can see, both the minimum and maximum values have been overwritten(!) by the 300x500 specified by the parent SizedBox.
Returning to performLayout(), the BoxConstraints generated here are passed to ColoredBox, and a box of that size is painted blue. Since these BoxConstraints are configured as "minimum 300x500, maximum 300x500—basically, make it 300x500 no matter what!", the result is the 300x500 blue box we saw at the beginning.
Summary so far
So, by following the implementation of SizedBox and its RenderObject, RenderConstrainedBox, we have confirmed why the initial code does not render the 100x200 blue box as specified.
In practice, you likely wouldn't build a layout where you pass a SizedBox with one size as a child to another SizedBox with a different size. However, you can now see how a layout that defies intuition can be created depending on the BoxConstraints passed and the implementation of performLayout().
In Flutter, keeping in mind that the size of a Widget is determined by the Constraints passed by the parent and the implementation of the RenderObject that receives them will be helpful when building layouts with Widgets (especially when the layout doesn't turn out as you imagined).
Just having the perspective of passing the size from the parent instead of trying to specify the size on the Widget itself can significantly change how you write the build() method.
Additionally, we've experienced that we can confirm "why such a layout is created" by following the framework's code when necessary.
Extra
Once you understand the discussion so far, you can apply it to understand and solve several problems.
Problem 1: How can I display the 100x200 blue box from the code at the beginning?
Insert a Widget that passes BoxConstraints with a minimum of 0x0 and a maximum of 300x500 to the child. For example, RenderPositionedBox generated by Align passes loose BoxConstraints to the child's RenderObject in performLayout() as follows:
class RenderPositionedBox extends RenderAligningShiftedBox {
@override
void performLayout() {
// Various parts omitted
child!.layout(constraints.loosen(), parentUsesSize: true);
}
Using this:
void main() => runApp(
const MaterialApp(
home: Scaffold(
body: SizedBox(
// Inside a box with width 300 and height 500
width: 300,
height: 500,
child: Align(
child: SizedBox(
// A box with width 100 and height 200
width: 100,
height: 200,
// Place a blue box
child: ColoredBox(color: Colors.blue),
),
),
),
),
),
);

Now, a blue box that appears to be 100x200 in size is displayed. However, since the location defaults to the "center of the parent" (the default value for Align), you will likely need to specify a value for alignment as appropriate.
Problem 2: Doesn't LayoutBuilder Tell You the Widget's Size?
LayoutBuilder does not tell you the size of the Widget. As we have seen so far, it only provides the BoxConstraints that serve as the basis for layout calculations. Therefore, what kind of layout calculation the child performs using those BoxConstraints, and what size it results in, is up to the child.
It is good to keep in mind that LayoutBuilder is not a Widget designed to achieve "building a layout using the size of a specific Widget."
Problem 3: Can I Detect Device Orientation Using OrientationBuilder?
No, you cannot. This is mentioned in the documentation, and the reason is obvious if you look at what OrientationBuilder is doing based on what we've discussed so far.
class OrientationBuilder extends StatelessWidget {
Widget _buildWithConstraints(BuildContext context, BoxConstraints constraints) {
final Orientation orientation = constraints.maxWidth > constraints.maxHeight ? Orientation.landscape : Orientation.portrait;
return builder(context, orientation);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: _buildWithConstraints);
}
}
OrientationBuilder internally uses LayoutBuilder to obtain BoxConstraints and then simply determines whether it is Orientation.landscape or Orientation.portrait based on which is larger: maxHeight or maxWidth.
It makes its determination based solely on the BoxConstraints passed to the OrientationBuilder, with no relation to the actual device orientation, and it ignores minWidth and minHeight entirely. Be sure to use it with this understanding.
That's all.
At first, one of the strengths of Flutter is that its APIs are so well-designed that you can build UIs to a certain extent without knowing these things. However, when trying to implement slightly complex layouts, dealing with Widgets overflowing the screen, or designing reusable Widgets, the presence or absence of this knowledge can lead to a lot of wasted trial and error or "magic code."
Once you get used to Flutter to some degree, it is a good idea to deepen your understanding of these internal layout calculation logics.
-
As you can see by reading the implementation, if no values are specified, the minimum is set to
0and the maximum todouble.infinity, resulting in a configuration that expands or contracts depending on the child's layout. ↩︎
Discussion