skip to content

In a custom Flutter RenderBox, what must performLayout do, and when should you set sizedByParent and implement computeDryLayout?

level: seniorimportance: should knowfreq 24%

answer

  1. size must satisfy constraints
  2. parentUsesSize: true to read child size
  3. size from constraints only: sizedByParent
  4. dry layout: size without side effects
  5. must match the real layout

basics

~20 s

performLayout lays out children, positions them via parentData and sets a size within the constraints. Set sizedByParent only when size depends on constraints alone. Implement computeDryLayout whenever a parent may ask your size without laying you out; it must match performLayout.

solid answer

~40 s

`performLayout` must lay out each child with constraints you choose, passing `parentUsesSize: true` if you read the child's `size`; write each child's position into its `parentData`; and set `size`, which must satisfy `constraints` (use `constraints.constrain`). Set **`sizedByParent`** to `true` only if the size depends on nothing but the constraints, like a box that fills whatever it is given; the framework then sizes you through `performResize`, which by default calls `computeDryLayout`, and treats you as a relayout boundary. **`computeDryLayout(constraints)`** answers "what size would you be?" without changing state; parents such as `Row`, `Wrap` or `Stack` call `getDryLayout` on children when they compute their own dry layout. Implement it for any box whose size can be computed cheaply, and keep it consistent with `performLayout`; the default asserts that it is unimplemented and returns `Size.zero`.

code

dart · 59 lines
dart
import 'package:flutter/rendering.dart';

class RenderRuler extends RenderBox {
  RenderRuler({
    required double lengthMm,
    required double pixelsPerMm,
    required Color tickColor,
  })  : _lengthMm = lengthMm,
        _pixelsPerMm = pixelsPerMm,
        _tickColor = tickColor;

  static const double _height = 32;

  double _lengthMm;
  set lengthMm(double value) {
    if (value == _lengthMm) return;
    _lengthMm = value;
    markNeedsLayout(); // changes the size
  }

  double _pixelsPerMm;
  set pixelsPerMm(double value) {
    if (value == _pixelsPerMm) return;
    _pixelsPerMm = value;
    markNeedsLayout(); // changes the size and tick spacing
  }

  Color _tickColor;
  set tickColor(Color value) {
    if (value == _tickColor) return;
    _tickColor = value;
    markNeedsPaint(); // same geometry, new pixels
  }

  @override
  Size computeDryLayout(BoxConstraints constraints) =>
      constraints.constrain(Size(_lengthMm * _pixelsPerMm, _height));

  @override
  void performLayout() {
    size = computeDryLayout(constraints);
  }

  @override
  bool hitTestSelf(Offset position) => true;

  @override
  void paint(PaintingContext context, Offset offset) {
    final Canvas canvas = context.canvas;
    final Paint tick = Paint()
      ..color = _tickColor
      ..strokeWidth = 1;
    for (var mm = 0; mm * _pixelsPerMm <= size.width; mm++) {
      final double x = offset.dx + mm * _pixelsPerMm;
      final double h = mm % 10 == 0 ? 20 : (mm % 5 == 0 ? 14 : 8);
      canvas.drawLine(Offset(x, offset.dy), Offset(x, offset.dy + h), tick);
    }
  }
}

go deeper

for a junior

Know that performLayout must set a size within the constraints the parent passed.

for a middle

Explain laying out children with parentUsesSize, writing parentData offsets, and what sizedByParent means.

for a senior

Implement computeDryLayout consistently with performLayout, keep it side-effect free, and choose sizedByParent only when size ignores everything but constraints.

for a principal

Require dry layout, intrinsics and debug checks in the test plan for shared render objects, since gaps surface only under specific parents.

## The layout contract A **`RenderBox`** receives `BoxConstraints` from its parent and must end layout with a `size` that satisfies them. Everything else about layout is up to the box. The work happens in **`performLayout()`**, which the framework calls from `layout()` when the box is dirty or its constraints changed. ## What `performLayout` must do 1. **Lay out children.** Call `child.layout(childConstraints, parentUsesSize: true)` for each child whose size you read afterwards. Reading `child.size` without that flag trips a debug assertion, because the framework assumed you would not depend on it and may not relayout you when the child changes. 2. **Position children.** Write each child's offset into its `parentData`, typically `BoxParentData.offset`. Paint and hit testing read it later. 3. **Set `size`.** Assign a size within `constraints`. `constraints.constrain(desired)` clamps a desired size; a size that violates the constraints, or an infinite one, is reported as an error in debug builds. For the woodworking ruler, the desired width is `lengthMm * pixelsPerMm` and the height a fixed 32 pixels, so `performLayout` is one line: `size = constraints.constrain(Size(lengthMm * pixelsPerMm, 32))`. ## `sizedByParent`: size from constraints alone | Box | Size depends on | `sizedByParent` | |---|---|---| | A ruler as long as its `lengthMm` | Its own properties | `false` | | A ruler that fills the available width | Constraints only | can be `true` | | A box sized around its child | The child | `false` | With `sizedByParent` set to `true`: - `layout` calls **`performResize()`** first, and the default implementation sets `size = computeDryLayout(constraints)`; - `performLayout` still runs, to lay out children, but must **not** change the size; - the box is treated as a **relayout boundary**, so its own relayouts do not dirty the parent. If the getter's value can change at runtime, the object must call `markNeedsLayoutForSizedByParentChange()`. Most boxes never need `sizedByParent`; framework examples include error and texture boxes that simply take the size they are given. ## `computeDryLayout`: size without doing layout Some parents must know a child's size **before** committing to layout, or while computing their own dry size. They call **`getDryLayout(constraints)`**, which caches and calls your **`computeDryLayout`**. Its contract: - return exactly the size `performLayout` would produce for the same constraints; - change no state: no child `layout` calls, no `parentData` writes, no `size` assignment; - for children, use `child.getDryLayout(...)` rather than `child.layout(...)`. If you do not override it, the default implementation fails a debug assertion explaining that your class does not implement `computeDryLayout` and returns `Size.zero`. The box works until it is placed under a parent that asks, and then fails, often far from where it was written. For a leaf whose size is cheap to compute, the simplest consistent design is to compute the size once, in `computeDryLayout`, and have `performLayout` assign `size = computeDryLayout(constraints)`. Where a real answer is impossible, the documented escape hatch is to call `debugCannotComputeDryLayout` inside an assert and return `Size.zero`. ## A common failure: unbounded constraints Constraints can be **unbounded**: a horizontal scroll view gives its child a maximum width of infinity. That affects the ruler differently depending on how its size is computed: - The ruler whose width is `lengthMm * pixelsPerMm` clamps a **finite** desired width with `constraints.constrain`, so it works inside the scroll view. - A ruler that sets `size = constraints.biggest` to fill its parent asks for an infinite width there. Debug builds report that the object was given an infinite size during layout. - A `sizedByParent` ruler has the same problem, since by definition it can only use the constraints. The fix is to choose a finite fallback when a maximum is infinite, for example `constraints.hasBoundedWidth ? constraints.maxWidth : lengthMm * pixelsPerMm`, and to apply the same rule in `computeDryLayout`. Deciding which constraints a parent hands down is the layout topic's concern; the render object's job is to survive all of them. ## Checklist - Every child laid out, with `parentUsesSize: true` wherever its size is read. - Every child positioned in `parentData`. - `size` set and within `constraints`. - `computeDryLayout` implemented and consistent with `performLayout`. - `sizedByParent` only when the size truly ignores everything but constraints. The constraint rules themselves, such as tight versus loose and what an unbounded constraint means, belong to the layout topic; this contract is how a custom box obeys them.

  • Why is it a bug for computeDryLayout to call child.layout?
    Dry layout must not change state. `child.layout` lays the child out for real, overwriting its size and constraints outside the normal layout pass and possibly while the parent is only probing sizes. Use `child.getDryLayout(childConstraints)` to ask the child's size without side effects.
  • When could a ruler render object safely set sizedByParent to true?
    When its size ignores its own properties and children: for example, a ruler that always fills the maximum width it is given and has a fixed height clamped to the constraints. A ruler whose width comes from `lengthMm` depends on a property, so it must set its size in `performLayout`.

saying these in an interview costs you the question

  • A box may ignore its constraints if it clamps its paint instead.
  • sizedByParent is right for any box that has no children.
  • computeDryLayout can lay out children to find their real size.
  • Leaving computeDryLayout unimplemented is safe because it is optional.
  • Reading child.size is fine even without parentUsesSize: true.