skip to content

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

level: seniorimportance: should knowfreq 28%

answer

  1. composition first
  2. drawing only: CustomPaint
  3. custom layout: consider MultiChildLayoutDelegate
  4. layout, hit test, semantics, dry layout
  5. profile before descending

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.

solid answer

~40 s

Go down the ladder only as far as needed. **Compose widgets** first: `Row`, `Stack`, `Align` and friends cover most layouts. If you only need to **draw**, `CustomPaint` with a `CustomPainter` gives you a canvas without writing layout. If you need to **position children** by custom rules, `CustomMultiChildLayout` or a `Flow` often suffices. A custom `RenderBox` is justified when you need a layout protocol those cannot express, such as a parent whose **own size** depends on its children's sizes (a layout delegate's `getSize` sees only the constraints), precise hit-test shapes, or when profiling shows a composed subtree costs too much layout. The price is real: you implement `performLayout`, `paint`, hit testing, `computeDryLayout`, intrinsic sizes and baselines where a parent may ask, and semantics for accessibility, plus the widget that creates and updates it.

go deeper

for a junior

Remember the order to try things: compose widgets, then CustomPaint for drawing, and only then a custom render object.

for a middle

Explain what each rung provides and name layout delegates as the step before writing a render object.

for a senior

Justify a custom RenderBox with a concrete layout, hit-test or profiling reason, and list the protocols you must then implement and test.

for a principal

Weigh the long-term ownership of render-layer code, its accessibility and test burden, against the product benefit, and decide who on the team maintains it.

## The ladder of options Flutter offers several levels at which to build a piece of UI. Each lower rung gives more control and costs more code you must maintain: | Rung | Tool | You write | You get | |---|---|---|---| | 1 | Compose existing widgets | `build` methods | Layout, paint, hit testing, semantics, all tested | | 2 | `CustomPaint` + `CustomPainter` | A `paint(Canvas, Size)` method | Free-form drawing inside a box sized by its parent or `size` | | 3 | `CustomMultiChildLayout` or `Flow` | A layout or flow delegate | Custom child positions; the box's own size comes from constraints only | | 4 | Custom `RenderBox` | A render object and its widget | Full control of layout, paint and hit testing | ## What each rung is good for For the woodworking app's ruler: - **A ruler strip with ticks**: rung 2. The size can come from a `SizedBox`, and a painter draws the ticks. No render object needed. - **Labels under each centimetre**: rung 1 or 3. A `Stack` with `Positioned` labels works, or a layout delegate that positions each label from its measured size. - **A ruler whose height must grow to fit the tallest label, whose ticks must avoid labels, and whose own width must equal the measured length in real millimetres regardless of constraints**: this is where rung 4 starts to pay. A layout delegate can measure and place the labels, but its `getSize` receives only the constraints and is documented as unable to reflect the children's sizes, so the ruler could not grow to fit its tallest label; hit testing on the thin ticks also needs custom shapes. ## Reasons that justify a custom RenderBox 1. **A layout protocol the built-ins lack.** For example, a parent whose own size depends on its children's measured sizes, which no layout delegate allows, or reading children's baselines or intrinsic sizes in a specific way. 2. **Custom hit testing.** Only a specific shape, such as the tick marks, should receive pointers, or children must be hit-tested in a non-standard order. 3. **Measured performance.** A composed subtree of many widgets, rebuilt and laid out every frame of a drag, shows up in profiling, and one render object that lays out and paints directly cuts that cost. 4. **Integrating an external paint source**, such as a texture or platform view; the framework itself uses render objects there. ## What you take on Once you write a `RenderBox`, the framework's guarantees become your job: - **Layout**: `performLayout`, correct use of `parentUsesSize`, sizes within constraints. - **Dry layout**: `computeDryLayout`, or the box fails when placed under a parent that asks. - **Intrinsics and baselines**: `computeMinIntrinsicWidth` and its three siblings if the box may appear under `IntrinsicWidth`, `IntrinsicHeight` or a table, plus baseline computation if children must align by text. - **Painting and hit testing** that agree with layout. - **Semantics**: `describeSemanticsConfiguration` and friends, or the ruler is invisible to screen readers. - **Invalidation**: setters that choose `markNeedsLayout` or `markNeedsPaint` correctly. - **The widget**: a `RenderObjectWidget` whose `createRenderObject` and `updateRenderObject` stay in sync. ## Signs you went lower than needed - The render object's `performLayout` just stacks children in a row or column, which `Row`, `Column` or `Stack` already do. - Its `paint` only draws shapes with no children, which a `CustomPainter` would do with less code. - `computeDryLayout`, intrinsics and semantics are left as defaults because "it works on our screen"; the box will fail the first time someone places it under a parent that asks. - Nobody measured the composed version; the custom box was chosen for speed that was never shown to be missing. The opposite sign is just as real: a composed widget tree that needs `LayoutBuilder`s, post-frame measurement and repeated rebuilds to fake a layout rule is usually a render object trying to get out. ## How to decide 1. Build it by composition first; it is also the fastest route to a working screen. 2. If the problem is drawing, move to `CustomPaint` rather than a render object. 3. If the problem is child positioning, try a layout delegate. 4. Only then write a `RenderBox`, and treat it as a component with its own tests, including debug checks such as `debugCheckIntrinsicSizes` when you implement intrinsics. The painter API, the gesture layer and frame-time profiling each have their own topics; this decision is about when to step below them.

  • What breaks if a custom RenderBox skips semantics?
    Assistive technologies such as screen readers see nothing meaningful for it: no label, no value, no actions. A composed widget tree usually inherits semantics from the widgets it uses, but a custom render object must describe itself, for example through `describeSemanticsConfiguration`, to be accessible.
  • When is CustomPaint enough for a ruler, and when is it not?
    It is enough when the ruler is a strip of ticks whose size is set by its parent or a fixed size: the painter just draws. It stops being enough when the ruler must size itself from child widgets such as labels, lay them out and hit-test them, because a painter draws on a canvas and has no children.

saying these in an interview costs you the question

  • A custom RenderBox is the default way to build any custom-looking widget.
  • Composed widgets are always slower than a hand-written render object.
  • A custom RenderBox gets semantics and intrinsic sizing for free.
  • CustomPaint can lay out and hit-test child widgets.
  • Once performLayout and paint work, the render object is finished.