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

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

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