In a custom Flutter RenderBox, what must performLayout do, and when should you set sizedByParent and implement computeDryLayout?
answer
- size must satisfy constraints
- parentUsesSize: true to read child size
- size from constraints only: sizedByParent
- dry layout: size without side effects
- must match the real layout
basics
~20 sperformLayout 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 linesimport '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
Know that performLayout must set a size within the constraints the parent passed.
Explain laying out children with parentUsesSize, writing parentData offsets, and what sizedByParent means.
Implement computeDryLayout consistently with performLayout, keep it side-effect free, and choose sizedByParent only when size ignores everything but constraints.
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.