Custom RenderObjects
4 min read
Flutter Internals
Existing widgets (Row, Stack, Flex, ...)
↓ doesn't work
CustomPaint / CustomSingleChildLayout / CustomMultiChildLayout
↓ doesn't work
Custom RenderObject (RenderBox or RenderSliver)
A custom RenderObject gives you:
| Hook | Purpose |
|---|---|
performLayout() | Compute this object's size from constraints; lay out children |
paint(context, offset) | Issue draw commands directly |
hitTest() / hitTestChildren() | Decide whether and where pointer events hit |
computeDryLayout, computeIntrinsic* | Support intrinsic sizing queries |
setupParentData | Custom layout-time data per child |
Use cases:
- Layout that can't be expressed with Flex/Stack (e.g., text on a curve, fan layouts, custom paged scrolls)
- Custom slivers (sticky headers, parallax effects beyond what packages offer)
- Performance-critical pickers/animations where widget wrapping is too costly
Code in action — a Gap widget
class Gap extends LeafRenderObjectWidget {
const Gap(this.size, {super.key});
final double size;
@override
RenderObject createRenderObject(BuildContext context) => _RenderGap(size);
@override
void updateRenderObject(BuildContext context, _RenderGap obj) {
obj.gapSize = size;
}
}
class _RenderGap extends RenderBox {
_RenderGap(this._gapSize);
double _gapSize;
set gapSize(double v) {
if (_gapSize == v) return;
_gapSize = v;
markNeedsLayout();
}
@override
void performLayout() {
final axis = (parent is RenderFlex)
? (parent as RenderFlex).direction
: Axis.vertical;
size = constraints.constrain(
axis == Axis.vertical ? Size(0, _gapSize) : Size(_gapSize, 0),
);
}
@override
void paint(PaintingContext context, Offset offset) {
// intentionally empty — just a gap
}
}
RenderBox vs RenderSliver
RenderBox | RenderSliver | |
|---|---|---|
| Lives in | Most of the tree | Scrollable areas (CustomScrollView) |
| Constraints | BoxConstraints (min/max width & height) | SliverConstraints (scroll offset, axis, viewport main extent…) |
| Geometry | Size | SliverGeometry (paint extent, layout extent, visible state…) |
| Built-in examples | RenderFlex, RenderPadding, RenderImage | RenderSliverList, RenderSliverPersistentHeader |
If you need a custom scrollable (sticky header that does X, parallax that does Y), you write a RenderSliver. For non-scrolling custom layouts, you write a RenderBox.
Lifecycle hooks you may need to override
| Method | When to override |
|---|---|
performLayout() | Always |
paint() | Always |
hitTestSelf() / hitTestChildren() | Custom hit testing (e.g., hit-test a circle, not a rect) |
computeDryLayout(constraints) | Allow intrinsic / DryLayout queries without doing full layout |
setupParentData(child) | Custom layout data per child (like FlexParentData) |
attach() / detach() | Manage listeners that depend on the pipeline owner |
markNeedsLayout() / markNeedsPaint() | Call when state changes affect layout / paint |
Common mistakes to avoid
❌ Reaching for custom RenderObject when CustomPaint or CustomMultiChildLayout would do
Adds maintenance cost. CustomPaint is enough 80% of the time.
❌ Mutating fields without markNeedsLayout / markNeedsPaint
Field changes; framework doesn't know to re-layout/repaint.
✅ Setters compare old vs new, then mark.
❌ Doing layout work in paint()
paint() runs AFTER layout. By contract, layout sets size; paint only draws.
Mixing them creates O(n²) work and assertion failures.
❌ Forgetting to call attach / detach for listeners
If you listen to something in initState-equivalent, you must unlisten on detach.
❌ Ignoring debugFillProperties
Custom RenderObjects show up in the inspector. Override debugFillProperties so
debugging actually works.
❌ Building a custom RenderObject without tests
You're now in framework territory. Test layout (size given constraints), paint
(golden tests), and hit testing explicitly.
Interview follow-ups
-
What's the difference between
RenderBoxandRenderSliver?RenderBoxlives in the regular widget tree and usesBoxConstraints+Size.RenderSliverlives inside a scrollable viewport (CustomScrollView) and usesSliverConstraints+SliverGeometry, which carry scroll offset, axis, viewport extent — needed for lazy, scroll-aware layout. -
How does Flutter's constraint-based layout differ from CSS flexbox? Flutter is single-pass (parent passes constraints down, child returns size up) — every widget has exactly one valid size given its constraints. CSS flexbox is multi-pass (browser may resize and reflow). Flutter's model is faster and more predictable but means widgets must opt into intrinsic sizing explicitly (it's not free).
-
What does
markNeedsLayout()do internally? It marks this RenderObject as needing layout, walks up the parent chain marking ancestors as having dirty descendants, and schedules a frame. On the next frame, Flutter's pipeline callsperformLayout()on just this subtree — not the whole tree. Same idea formarkNeedsPaint(). -
Why are most "custom widgets" actually composition widgets, not custom RenderObjects? Composition is cheaper: you reuse existing RenderObjects (
RenderPadding,RenderFlex, etc.), which are battle-tested for performance, intrinsic sizing, accessibility. Going custom means re-implementing all of that. Only do it when the existing primitives genuinely can't express your layout.
How helpful was this content?
Please sign in to rate this article.