skip to content

Custom RenderObjects

A custom RenderBox takes over layout, paint and hit testing directly instead of composing widgets. Interviewers ask when that is worth it and what markNeedsLayout costs over markNeedsPaint.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Flutter, what does markNeedsLayout cost compared with markNeedsPaint on a RenderObject, and how do you choose between them in a setter?

level: middleimportance: should knowfreq 40%

answer

  1. geometry vs appearance
  2. layout implies paint
  3. relayout boundary stops the climb
  4. repaint boundary stops paint
  5. compare before marking

basics

~20 s

markNeedsLayout schedules layout and then paint, and can spread up to ancestors whose layout depends on this size. markNeedsPaint only repaints up to the nearest repaint boundary. Call markNeedsLayout when size or child positions change, markNeedsPaint when only pixels change.

solid answer

~50 s

`markNeedsPaint()` flags the object for the **paint** phase; the flag climbs to the nearest repaint boundary, whose layer is re-recorded, and no layout runs. `markNeedsLayout()` flags it for **layout**, and after layout the object is marked for paint as well, so it always costs more. Layout dirtiness also climbs: unless this object is a **relayout boundary** it marks its parent, and so on. An object is a relayout boundary when its parent did not ask to use its size, when it is `sizedByParent`, when its constraints are tight, or when it is the root. So a setter asks one question: does this value change the size or where children go? A ruler's length or scale does, so `markNeedsLayout`; its tick colour does not, so `markNeedsPaint`. Either way, compare with the stored value first and return early if equal.

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

Remember that layout changes size and position while paint changes pixels, and that layout is the more expensive of the two.

for a middle

Explain that layout implies paint, how layout dirtiness climbs to a relayout boundary and paint to a repaint boundary, and when setters pick each.

for a senior

Spot wrong invalidation in a custom render object: layout for paint-only changes costing frame time, or paint for geometry changes leaving stale sizes.

for a principal

Set review rules for render object setters, compare-then-mark with the narrowest invalidation, and decide where boundaries belong in shared components.

## Two different kinds of stale A render object remembers the results of two phases: - **layout**: its `size` and the positions of its children; - **paint**: the drawing commands recorded into a layer. When a property changes, the object must say which of those results is now wrong. That is what the two methods do. ## What `markNeedsPaint` does `markNeedsPaint()` sets a needs-paint flag and walks **up** to the nearest **repaint boundary**, an object with its own layer, and registers that boundary with the `PipelineOwner`. In the next frame's paint phase only that boundary's subtree is painted again. Layout is not involved at all: sizes and positions are reused. ## What `markNeedsLayout` does `markNeedsLayout()` sets a needs-layout flag. Then: 1. If this object is a **relayout boundary**, it registers itself with the `PipelineOwner` and stops. 2. Otherwise it calls its parent's `markNeedsLayout`, because the parent's own layout may depend on this child's size. In the layout phase, each registered object re-runs `performLayout`, which may lay out children again. When an object finishes layout, the framework marks it for paint. So **layout implies paint**; paint never implies layout. `RenderBox` adds one more upward path: if a parent has queried this box's intrinsic sizes, baseline or dry layout, the cached answer is cleared and the parent is marked for layout even across a relayout boundary. ## When is an object a relayout boundary? The framework decides during `layout` itself. An object is a boundary when **any** of these holds: | Condition | Why the parent is unaffected | |---|---| | The parent called `layout` without `parentUsesSize: true` | The parent never reads the size | | `sizedByParent` is `true` | The size depends only on the constraints | | The constraints are **tight** | Only one size is possible | | It is the root | There is no parent | A ruler inside a `SizedBox` with a fixed width and height gets tight constraints, so a length change relays out only the ruler. The same ruler inside a `Row` that reads its width is not a boundary, so the `Row` lays out again too. ## Choosing in a setter Ask what the value influences: - **Size, child constraints or child positions**: `markNeedsLayout()`. For the woodworking ruler: `lengthMm`, `pixelsPerMm`, a label font size. - **Only pixels**: `markNeedsPaint()`. For the ruler: `tickColor`, whether half-centimetre ticks are drawn when the tick band height is fixed. - **Neither** (a value only read in callbacks): mark nothing. And always: 1. compare with the stored value and return early if equal, because the owning widget's `updateRenderObject` assigns every property on every rebuild; 2. never call either method from inside `paint`, and do not call `markNeedsPaint` during the paint phase; the framework asserts against mutations at the wrong time. ## Tracing a pinch-to-zoom on the ruler The user pinches to zoom the ruler from 4 to 6 pixels per millimetre while a highlight colour pulses on the selected measurement: 1. Each zoom step sets `pixelsPerMm`; the setter sees a new value and calls `markNeedsLayout()`. 2. The ruler sits in a `Row` that reads its width, so it is not a relayout boundary and the `Row` is marked for layout too. 3. In the layout phase the `Row` lays out again, the ruler computes its new width, and both are then marked for paint. 4. Each pulse of the highlight sets `tickColor`; the setter calls `markNeedsPaint()` only. 5. On frames where only the colour changed, the layout phase has nothing to do for the ruler and only paint runs up to the nearest repaint boundary. If the colour setter wrongly called `markNeedsLayout`, every pulse frame would lay out the `Row` and all its children as well. ## Why the difference matters For a ruler redrawn while the user drags a measurement handle, a colour highlight that calls `markNeedsLayout` by mistake makes every frame re-run layout for the ruler and possibly its ancestors. Nothing looks wrong, but frame time goes up. Calling `markNeedsPaint` for something that does change size is worse: the ruler keeps its old size and draws ticks beyond or short of its bounds. Measuring the resulting frame cost and isolating repaints with boundaries are covered by the performance and pipeline topics.

  • A setter that changes the size calls only markNeedsPaint. What does the user see?
    The render object keeps the size computed by its last layout, because nothing asked for layout. Paint then draws using the new value inside the old bounds: on the ruler, ticks are spaced for the new scale but the box is still the old length, so ticks are clipped by siblings or stop short.
  • Why can a sizedByParent render object's markNeedsLayout stay local even when its parent reads its size?
    A `sizedByParent` object's size depends only on its constraints, and the constraints have not changed, so its size cannot change. The framework therefore treats it as a relayout boundary: it is laid out again itself, and its parent is left alone.

saying these in an interview costs you the question

  • markNeedsPaint also re-runs layout to be safe.
  • markNeedsLayout only ever affects the object it is called on.
  • Calling markNeedsLayout for a colour change is harmless.
  • Setters should always mark dirty even if the value is unchanged.
  • Every render object is a relayout boundary by default.
open as a page

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

level: seniorimportance: should knowfreq 24%

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.

open as a page

In Flutter, when is writing a custom RenderBox worth it instead of composing existing widgets or drawing with CustomPaint?

level: seniorimportance: should knowfreq 28%

basics

~10 s

Write a custom RenderBox only when composition, CustomPaint and layout delegates cannot express the layout, hit testing or performance you need. You then own layout, painting, hit testing, dry layout, intrinsics and semantics yourself.

open as a page

In Flutter, what are the three jobs of a custom RenderBox, and which method does it override for each?

level: juniorimportance: nice to knowfreq 22%

basics

~10 s

A RenderBox lays out, paints and hit-tests. It sets its size from the parent's BoxConstraints in performLayout, draws in paint(PaintingContext, Offset), and answers pointer hits through hitTestSelf and hitTestChildren.

open as a page

In a custom Flutter RenderBox with children, how do parentData offsets, paint and hitTestChildren work together?

level: seniorimportance: nice to knowfreq 16%

basics

~20 s

The parent installs a parentData object on each child in setupParentData, writes each child's position into it during performLayout, paints each child at offset plus that position, and hit-tests children in reverse paint order after subtracting it.

open as a page