skip to content

In Flutter, when would you use CustomMultiChildLayout or CustomSingleChildLayout, and what must a MultiChildLayoutDelegate's performLayout do?

level: seniorimportance: nice to knowfreq 15%

answer

  1. position depends on a sibling's size
  2. LayoutId tags each child
  3. layoutChild exactly once each
  4. getSize cannot use children
  5. shouldRelayout or a relayout Listenable

basics

~20 s

Use them when a child's constraints or position depend on measured sizes that Row, Column, Stack and Align cannot express. performLayout must call layoutChild exactly once for every child, identified by its LayoutId, and position each with positionChild.

solid answer

~40 s

`CustomSingleChildLayout` takes a `SingleChildLayoutDelegate` with four hooks: `getSize` (default `constraints.biggest`), `getConstraintsForChild`, `getPositionForChild(size, childSize)` and the required `shouldRelayout`. It is how Flutter positions popup menus against their anchor. `CustomMultiChildLayout` takes a `MultiChildLayoutDelegate`; each child is wrapped in `LayoutId(id: ...)`. In `performLayout(Size size)` you call `layoutChild(id, constraints)` **exactly once per child** — it returns that child's size — and `positionChild(id, offset)`; `hasChild(id)` handles optional children. `Scaffold` itself is laid out this way. The key limit: the widget's own size comes from `getSize(constraints)` and **cannot depend on the children**. If you need that, you are writing a custom render object. Implement `shouldRelayout` to compare fields, or pass a `relayout` `Listenable` to re-lay out without rebuilding.

code

dart · 47 lines
dart
import 'package:flutter/material.dart';

enum _ResumeSlot { track, label }

class _ResumeLayout extends MultiChildLayoutDelegate {
  _ResumeLayout(this.progress);

  final double progress;

  @override
  void performLayout(Size size) {
    const double trackHeight = 4;
    final Size label = layoutChild(_ResumeSlot.label, BoxConstraints.loose(size));
    layoutChild(_ResumeSlot.track, BoxConstraints.tightFor(width: size.width, height: trackHeight));
    positionChild(_ResumeSlot.track, Offset(0, size.height - trackHeight));
    final double markerX = size.width * progress;
    final double left = (markerX - label.width / 2).clamp(0.0, size.width - label.width);
    positionChild(_ResumeSlot.label, Offset(left, 0));
  }

  @override
  bool shouldRelayout(_ResumeLayout oldDelegate) => oldDelegate.progress != progress;
}

class ResumeMarker extends StatelessWidget {
  const ResumeMarker({super.key, required this.progress, required this.label});

  final double progress;
  final String label;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 32,
      child: CustomMultiChildLayout(
        delegate: _ResumeLayout(progress),
        children: <Widget>[
          LayoutId(
            id: _ResumeSlot.track,
            child: ColoredBox(color: Theme.of(context).colorScheme.outlineVariant),
          ),
          LayoutId(id: _ResumeSlot.label, child: Text(label)),
        ],
      ),
    );
  }
}

go deeper

for a junior

Know that these widgets exist for layouts Row, Column and Stack cannot express, and that children are tagged with LayoutId.

for a middle

Name the delegate methods, the once-per-child rule for layoutChild, and the shouldRelayout contract.

for a senior

Recognise when measured-size rules need a delegate, explain the getSize limitation, and use relayout Listenables to avoid rebuilds.

for a principal

Decide when a repeated layout pattern deserves a shared delegate or a render object instead of ad hoc nesting of Stack and Align.

## The gap they fill `Row`, `Column`, `Stack`, `Align` and friends cover most layouts. They struggle when **one child's position depends on another child's measured size** in a custom way — for example, a label centred over a marker but kept inside the bounds, where the clamp depends on the label's own width. Custom layout delegates let you write that rule in plain Dart without creating a render object. ## CustomSingleChildLayout One child, one `SingleChildLayoutDelegate`: | Method | Default | Job | |---|---|---| | `getSize(constraints)` | `constraints.biggest` | the widget's own size | | `getConstraintsForChild(constraints)` | pass-through | constraints for the child | | `getPositionForChild(size, childSize)` | `Offset.zero` | where to place the child | | `shouldRelayout(oldDelegate)` | required | whether a new delegate changes layout | Flutter uses it for popup-menu and dropdown routes: the menu's position depends on both the anchor rectangle and the menu's measured size. ## CustomMultiChildLayout Many children, one `MultiChildLayoutDelegate`. The rules: 1. Wrap each child in `LayoutId(id: someId)`. Ids must be unique; an `enum` is the usual choice. 2. Override `performLayout(Size size)`. It receives the size already chosen by `getSize`. 3. Call `layoutChild(id, constraints)` **exactly once for every child** each time; it returns the child's `Size`. Skipping a child or laying one out twice is reported as an error. 4. Call `positionChild(id, offset)` for each child; unpositioned children stay at `(0, 0)`. 5. Use `hasChild(id)` for optional slots. 6. Implement `shouldRelayout(oldDelegate)` by comparing the delegate's fields. Children paint in list order, whatever order you lay them out in. `Scaffold` is implemented with a `MultiChildLayoutDelegate`, which is why its body, app bar, floating action button and bottom bar can depend on each other's sizes. ## The main limitation `getSize(constraints)` runs **before** any child is laid out, and its docs state that the size cannot reflect the children's sizes. So a delegate layout cannot "shrink-wrap" its children. Give it bounded constraints, or a fixed size in `getSize`. If the widget must size itself from its children, or answer baselines and intrinsic sizes precisely, the next step is a custom `RenderBox`. ## Relayout without rebuild Both delegate base classes accept a `relayout` `Listenable`. Pass an `Animation` or a `ValueNotifier`, and the layout re-runs when it notifies, **skipping the build phase**. That is cheaper than rebuilding the widget with a new delegate on every animation tick. ## A worked case In a lecture-archive player, a "resume here" label should sit centred above a marker at the watched fraction of the progress track, but never hang off either edge: - lay out the label loosely and read its width; - lay out the track at full width and a fixed height; - compute `markerX = size.width * progress` and clamp `markerX - labelWidth / 2` between 0 and `size.width - labelWidth`. The clamp needs the label's measured width, which `Align` cannot express. ## Choosing - Can `Row`, `Column`, `Stack` or `Align` express it? Use them. - Needs measured sizes but a fixed outer size? Use a delegate. - Must size itself from children or be reused in hot paths? Write a render object.

  • Why can't a CustomMultiChildLayout size itself to fit its children?
    Its size comes from the delegate's `getSize(constraints)`, which runs before `performLayout` lays out any child, so no child sizes exist yet. The default returns `constraints.biggest`. To size from children you need a custom `RenderBox`.
  • How do you animate a delegate layout without rebuilding the widget each frame?
    Pass a `Listenable`, such as an `Animation`, as the delegate's `relayout` argument and read its value in `performLayout`. The render object listens and marks itself for layout when it notifies, skipping the build phase.

saying these in an interview costs you the question

  • A MultiChildLayoutDelegate can size itself from its children.
  • Children can be laid out any number of times in performLayout.
  • Unpositioned children are centred automatically.
  • Children paint in the order layoutChild is called.
  • Every custom layout needs a custom RenderBox.