Custom RenderObjects

Low PriorityAsked in ~35% of senior interviews

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:

HookPurpose
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
setupParentDataCustom 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

RenderBoxRenderSliver
Lives inMost of the treeScrollable areas (CustomScrollView)
ConstraintsBoxConstraints (min/max width & height)SliverConstraints (scroll offset, axis, viewport main extent…)
GeometrySizeSliverGeometry (paint extent, layout extent, visible state…)
Built-in examplesRenderFlex, RenderPadding, RenderImageRenderSliverList, 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

MethodWhen 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

  1. What's the difference between RenderBox and RenderSliver? RenderBox lives in the regular widget tree and uses BoxConstraints + Size. RenderSliver lives inside a scrollable viewport (CustomScrollView) and uses SliverConstraints + SliverGeometry, which carry scroll offset, axis, viewport extent — needed for lazy, scroll-aware layout.

  2. 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 calls performLayout() on just this subtree — not the whole tree. Same idea for markNeedsPaint().

  3. 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.