In Flutter, what are the three jobs of a custom RenderBox, and which method does it override for each?
answer
- constraints down, size up
- performLayout sets size
- paint(PaintingContext, Offset)
- hitTestSelf and hitTestChildren
- setters mark what is stale
basics
~10 sA 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.
solid answer
~40 sA `RenderBox` is a render object that follows the **box protocol**: its parent hands it `BoxConstraints` and it must choose a `size` inside them. **Layout** happens in `performLayout()`, which reads `constraints`, lays out any children, positions them and sets `size`. **Painting** happens in `paint(PaintingContext context, Offset offset)`, where `offset` is the box's top-left corner in the canvas's coordinates, so everything is drawn relative to it. **Hit testing** goes through `hitTest`, which checks the point is inside `size` and then asks `hitTestChildren` and `hitTestSelf`; a leaf that should receive taps returns `true` from `hitTestSelf`. Around those, property setters call `markNeedsLayout` or `markNeedsPaint` so the pipeline knows which job to redo.
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
Recall the three jobs and their methods: performLayout sets size, paint draws at the given offset, and hitTestSelf or hitTestChildren answer taps.
Explain constraints down and size up, why paint uses the offset, and how setters choose markNeedsLayout or markNeedsPaint.
Implement all three jobs consistently, including hit-testing children with their offsets, and justify when a custom box beats composition.
Treat custom render objects as a maintained API surface: decide who owns them, how they are tested, and when composition is good enough.
## What a RenderBox is Widgets describe; **render objects** do the work. `RenderObject` is the general base class, and **`RenderBox`** is the subclass for the everyday two-dimensional **box protocol**: a parent passes `BoxConstraints` (minimum and maximum width and height), and the child picks a `Size` that satisfies them. Almost every render object behind `Padding`, `SizedBox`, `Row` or `Text` is a `RenderBox`. Writing one yourself means taking over three jobs that composed widgets normally do for you. ## Job 1: layout Override **`performLayout()`**. Inside it: 1. Read `constraints`, the `BoxConstraints` the parent gave you. 2. Lay out each child by calling `child.layout(childConstraints, parentUsesSize: true)` if you need to read its size. 3. Position each child by writing its `parentData` offset. 4. Set **`size`**, which must satisfy `constraints`; `constraints.constrain(desired)` clamps a desired size into range. If the size depends **only** on the constraints, you can instead set `sizedByParent` to `true` and compute the size in `computeDryLayout`. Forgetting to set a size is an error: a box that was never given a size cannot be painted or hit-tested. ## Job 2: painting Override **`paint(PaintingContext context, Offset offset)`**: - `context.canvas` is the `Canvas` to draw on; do not keep it across calls to other `PaintingContext` methods, since it may be replaced. - `offset` is where this box's top-left corner sits in that canvas, so every coordinate is `offset + local`. - Children are painted with `context.paintChild(child, offset + childOffset)`. The canvas is shared with other render objects, which is why drawing at `Offset.zero` instead of at `offset` paints in the wrong place. ## Job 3: hit testing When a pointer goes down, the framework calls **`hitTest(result, position: ...)`** from the root down. `RenderBox.hitTest` already checks that `position` is inside `size` and then calls: - **`hitTestChildren(result, position: ...)`**, which should forward to children, adjusting the position by each child's offset; - **`hitTestSelf(position)`**, which defaults to `false`; return `true` if this box itself should receive the event. If either returns `true`, the box adds itself to the result and the event is delivered to it and its ancestors along the path. ## How the jobs depend on each other The three jobs are not independent, and the order matters: 1. **Layout comes first.** Paint needs `size` and child positions, so paint runs only after layout is clean. 2. **Hit testing needs layout, not paint.** The framework documents that hit testing may run on a box that was never painted: a child of a fully transparent `Opacity` is still hit-tested even though it is not painted. So hit-test logic must use layout results, never values computed during `paint`. 3. **Paint must not change layout.** Setting `size` or calling `markNeedsLayout` from `paint` is an error; paint only records drawing commands. A beginner's ruler that computes its tick positions inside `paint` and then uses them in `hitTestSelf` breaks rule 2: before the first paint, or when painting is skipped, the tap test reads stale or empty data. Compute geometry in `performLayout` and let both paint and hit testing read it. ## Keeping the jobs up to date A render object keeps its own copy of every value it uses. Setters compare the new value, store it, and mark the job that is now stale: | Property affects | Setter calls | Next frame redoes | |---|---|---| | Size or child positions | `markNeedsLayout()` | layout, then paint | | Only appearance | `markNeedsPaint()` | paint only | ## The woodworking ruler A woodworking app draws an on-screen ruler with millimetre, half-centimetre and centimetre ticks. As a `RenderRuler`: - `performLayout` sets `size` to the ruler length times pixels per millimetre, clamped to the constraints; - `paint` draws each tick at `offset.dx + mm * pixelsPerMm`; - `hitTestSelf` returns `true`, so a tap anywhere on the ruler reaches the gesture handling above it; - `lengthMm` and `pixelsPerMm` setters call `markNeedsLayout`, the `tickColor` setter calls `markNeedsPaint`. The widget that creates and updates `RenderRuler`, and the drawing API on `Canvas` itself, are covered in neighbouring topics.
- Why must a RenderBox's paint method draw relative to the offset argument?Because the canvas belongs to a layer shared by many render objects, and `offset` is where this box's top-left corner lies in that canvas. Drawing at local coordinates without adding `offset` would paint the ruler at the layer's origin, on top of whatever else lives there.
- What does RenderBox.hitTest already do before it calls hitTestChildren and hitTestSelf?It checks that the position lies inside the box's `size`; outside, it returns `false` without asking either method. Inside, if `hitTestChildren` or `hitTestSelf` returns `true`, it adds a `BoxHitTestEntry` for this box to the result and returns `true`.
A carpenter fitting a shelf into an alcove: the alcove's size range is given (constraints), the carpenter decides the shelf size and where brackets go (layout), finishes and varnishes it (paint), and answers whether a pressed point is on the shelf (hit test).
saying these in an interview costs you the question
- A RenderBox sets its size in paint once it knows what it drew.
- The paint offset can be ignored because each box has its own canvas.
- hitTestSelf returns true by default, so every box is tappable.
- A RenderBox may choose any size regardless of its constraints.
- Changing a colour should call markNeedsLayout.