All I Know about Layout Calculation

All I Know about Layout Calculation

First of all, I must note that reading the official document of "Understanding constraints" is a best choice to understand this topic in most cases.

https://docs.flutter.dev/ui/layout/constraints

This document introduces the important concept of layout calculation.

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

Knowing this concept is crucial to understanding what is happening when you compose your widgets. In other words, it may enable you to predict what UI will be shown on the screen before you run your app.

So, I strongly recommend you read the document first. Then, this article will clarify how and where the concept is implemented by diving into the source code of the Flutter framework.

So, let’s get started!

Let's start by distinguishing Flutter’s widgets to make sure we are on the same page.

Briefly speaking, there are 3 major types of widgets in the Flutter framework:

  1. StatelessWidget/StatefulWidget

  2. InheritedWidget

  3. RenderObjectWidget

StatelessWidget and StatefulWidget, which are the most commonly used widgets, composes the widgets by build() method. InheritedWidget is used to pass the data to the descendant widgets.

RenderObjectWidget is the very widget that creates RenderObjects which:

  • calculate the layout

  • paint the result of the layout calculation

  • manage touch events, semantics, focus, or other interactive behaviors

In other words, the entire UI of the Flutter app is achieved by RenderObjectWidget, while all the business for StatelessWidget, StatefulWidget, and InheritedWidget is just composing the RenderObject tree, I would say.

Thus, if we want to investigate the layout calculation, we must keep in mind to focus on RenderObjectWidget and created RenderObjects.

As the concept says, Constraints, Sizes, and Positions are the keywords to understand the layout calculation. Though they are explained in the official document in detail, let me summarize them here.

Constraints is a condition for the direct child RenderObject to calculate its layout, which consists of minimum/maximum width and height.

Because it's a condition in the end, the result is not necessarily the size indicated by the given constraints.

For example, if the parent RenderObject passes a constraint of minimum width/height of 0 and maximum width/height of 100 to its child, the child RenderObject can be the size of 50x50, 100x100, or even 0x0. It's totally up to the child RenderObject to decide its size.

I must mention here that Constraints has two variations: BoxConstraints and SliverConstraints. However, as SliverConstraints, which is used for ListView and its family, is a little bit more complex than BoxConstraints, we will focus on BoxConstraints in this article.

Once the child RenderObjects calculate their layout, they decide their Sizes. The size is returned to the parent as a result of layout calculation.

Parent RenderObject doesn't know what is exactly painted inside the child RenderObject, but it is usually enough if it knows the size of its child. If the parent RenderObject has multiple children, it also knows each size of them.

Now, the parent RenderObject knows the size of each child RenderObject. The last task for the parent RenderObject is to decide "where to place each child RenderObject", which means calculating the Positions.

For example, if the parent Align provides a constraint of 100x100 to its child, and the child RenderObject decides its size of 50x50, the parent Align will place the child at the position according to its alignment property.


So far, we have seen the word of Constraints, Sizes, and Positions as well as how parent RenderObject and child RenderObject communicate with each other to decide the layout.

This communication is done recursively from ancestor RenderObject to descendant RenderObject. This means a certain child RenderObject also creates its own constraints and passes it to its child RenderObject. This process continues until the "leaf" RenderObject tree.

Now, we are ready to dive into the concrete source code. Let's start with SizedBox.

SizedBox is a simple widget that creates a RenderConstrainedBox with the given width and height. The brief implementation is as follows:

class SizedBox extends SingleChildRenderObjectWidget {
  const SizedBox({ super.key, this.width, this.height, super.child });

  final double? width;
  final double? height;

  @override
  RenderConstrainedBox createRenderObject(BuildContext context) {
    return RenderConstrainedBox(
      additionalConstraints: _additionalConstraints,
    );
  }

  BoxConstraints get _additionalConstraints {
    return BoxConstraints.tightFor(width: width, height: height);
  }
}

As we can see, what SizedBox does is creating a RenderConstrainedBox which passes a BoxConstraints made from the given width and height to its child.

BoxConstraints.tightFor here is a factory constructor that creates a BoxConstraints applying the given width and height to both the minimum and maximum constraints.

const BoxConstraints.tightFor({
  double? width,
  double? height,
}) : minWidth = width ?? 0.0,
     maxWidth = width ?? double.infinity,
     minHeight = height ?? 0.0,
     maxHeight = height ?? double.infinity;

As RenderConstrainedBox just passes the given BoxConstraints to its child RenderObject, and the constraints are always "tight", the direct child's size always becomes the same as the given width and height regardless of the child's given size.

SizedBox(
  width: 100,
  height: 100,
  child: SizedBox(
    width: 50,
    height: 50,
    child: ColoredBox(color: Colors.red),
  ),
)

So interestingly, in this case, the child SizedBox will be painted with the size of 100x100, not 50x50 like the screenshot below.

This is because the parent RenderConstrainedBox, which is created by the parent SizedBox, passes the tight constraints to its child RenderConstrainedBox, which is created by the child SizedBox, saying the minimum width/height is 100.

So how can we display a 50x50 red box then? The point is that the constraint is only applied to its direct child.

In the case above, the tight constraint of 100x100 is only applied to the child RenderObject and not applied to the grand-child RenderObject.

Thus, placing another RenderObject which provides a "not tight" constraint to its child RenderObject between the two SizedBoxs would be the straightforward solution.

Though we have several options for widgets to achieve that, let's see the example of using Align here.

SizedBox(
  width: 100,
  height: 100,
  child: Align(
    child: SizedBox(
      width: 50,
      height: 50,
      child: ColoredBox(color: Colors.red),
    ),
  ),
)

Align creates RenderPositionedBox as its corresponding RenderObject.

RenderPositionedBox passes "loosen" constraints to its child looking at performLayout() method like below.

@override
void performLayout() {
  // obtain the given constraints from its parent
  final BoxConstraints constraints = this.constraints;

  // passing "loosen" constraints to its child
  child!.layout(constraints.loosen(), parentUsesSize: true);
  size = constraints.constrain(Size(
    shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
    shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity,
  ));
  alignChild();
}

"loosen" means creating a new BoxConstraints that respects the maximum width/height of the original constraints while the minimum width/height is ignored.

BoxConstraints loosen() {
  // respect maxWidth and maxHeight
  // but ignore minWidth and minHeight
  return BoxConstraints(
    maxWidth: maxWidth,
    maxHeight: maxHeight,
  );
}

In this case, the RenderConstrainedBox associated to the child SizedBox (the one showing a red box) will receive another BoxConstraints preserving minimum size of 0x0 and maximum size of 100x100. As the RenderConstrainedBox is not forced to be a size of 100x100 anymore, this now can return the size of 50x50 as desired to its parent resulting in displaying a 50x50 red box.

Every RenderBox has its "logic" in how it layouts its child(ren) in performLayout() method. Let's take a look at another example of Stack.

Stack creates a RenderStack object as its corresponding RenderObject, and RenderStack calculates its layout in performLayout() method like below.

@override
void performLayout() {
  final BoxConstraints constraints = this.constraints;

  // calculate sizes of non-positioned children first
  size = _computeSize(
    constraints: constraints,
    layoutChild: ChildLayoutHelper.layoutChild,
  );

  RenderBox? child = firstChild;
  while (child != null) {
    final StackParentData childParentData = child.parentData! as StackParentData;

    if (!childParentData.isPositioned) {
      childParentData.offset = resolvedAlignment.alongOffset(size - child.size as Offset);
    } else {
      // then, calculate positioned children based on the constraints made from the size of non-positioned children
      _hasVisualOverflow = layoutPositionedChild(child, childParentData, size, resolvedAlignment) || _hasVisualOverflow;
    }

    child = childParentData.nextSibling;
  }
}

RenderStack first calls _computeSize which calculates the size of non-positioned children. Then, it iterates over the children and calls layoutPositionedChild for positioned children passing the constraints made from the size of non-positioned children.

For example, if we have a Stack with a red box of 100x100 and a blue box of 50x50 like below:

Stack(
  children: [
    SizedBox(
      width: 100,
      height: 100,
      child: ColoredBox(color: Colors.red),
    ),
    Positioned(
      right: 0,
      bottom: 0,
      child: SizedBox(
        width: 50,
        height: 50,
        child: ColoredBox(color: Colors.blue),
      ),
    ),
  ],
)

Stack first decides its size with 100x100 SizedBox without Positioned widget, then it proceeds to calculate the layout of the 50x50 SizedBox wrapped by Positioned widget passing the constraints of maximum width/height of 100x100. And then, it places the blue box at the right bottom of the red box as Positioned widget indicates.

For your future reference, this Stack's behavior of "non-positioned children first, then positioned children" would be good information when you make some UI using Stack.

As you see above, performLayout() of RenderObject will tell you how each RenderObjectWidget calculates its and its child's layout. Fortunately, all the logic is implemented in Flutter's framework layer with Dart code, so you don't need to hesitate to read the source code when you want to know what exactly happens in layout calculations.

In the previous example, RenderStack tells its child RenderBox whether it is positioned or not by checking child.parentData like below.

final StackParentData childParentData = child.parentData! as StackParentData;

if (!childParentData.isPositioned) {
}

But what is parentData?

According to the official document, parentData is a "Data for use by the parent render object".

Usually, child RenderObject only tells its sizes to its parent RenderObject, but sometimes it needs to tell additional information to its parent RenderObject, such like "positioned or not" for RenderStack's logic.

In that case, RenderObjects uses another way for communication of parentData property.

This mechanism works like below:

  1. parent RenderObject sets its ParentData object to its child RenderObject's parentData property

  2. child RenderObject sets additional information to the given ParentData object

  3. parent RenderObject uses the information for layout calculation

For example, RenderStack sets StackParentData object to its child RenderBox's parentData property with the code below.

@override
void setupParentData(RenderBox child) {
  if (child.parentData is! StackParentData) {
    child.parentData = StackParentData();
  }
}

Then, though it's a little bit tricky, Positioned widget sets its given position data, such like right: 0, bottom: 0, to the parentData property of its closest RenderObjectWidget's RenderObject.

@override
void applyParentData(RenderObject renderObject) {
  final StackParentData parentData = renderObject.parentData! as StackParentData;

  if (parentData.right != right) {
    parentData.right = right;
    needsLayout = true;
  }

  if (parentData.bottom != bottom) {
    parentData.bottom = bottom;
    needsLayout = true;
  }

  // ... and so on

  if (needsLayout) {
    renderObject.parent?.markNeedsLayout();
  }
}

This will tell the parent RenderStack that the child RenderBox is "positioned" as .positioned getter is implemented like below in StackParentData.

bool get isPositioned => top != null || right != null || bottom != null || left != null || width != null || height != null;

Again, the fundamental idea of layout calculation in the Flutter framework is "Constraints go down. Sizes go up. Parent sets position", but "Sizes" is sometimes not enough for parent's layout calculation. To cover the case, the Flutter framework provides another way for communication of parentData property.

That's it!

We have seen how the Flutter framework calculates the layout using RenderObject.

Starting with the concept of "Constraints go down. Sizes go up. Parent sets position", we have taken a closer look at how the layout calculation is implemented.

A parent RenderObject passes its constraints to its child RenderObject. The child RenderObject calculates its size and returns it to the parent. The parent RenderObject then decides the position of the child RenderObject. Then the child RenderObject paints it.

Honestly speaking, I have some more topics about it, such as layout cache mechanism or the trick of IntrinsicWidth/Height. But I'll leave them for another article because this article is already long enough.

Thank you for reading this article, and I hope this knowledge will help you make your UI more logically predictable, where you don't wasting your time googling with vague keywords anymore.