Implementing a Custom List-Based Multi-Child Element and Render Object#
This guide walks through implementing a custom widget whose API takes a flat list of children — a List<Widget>
parameter where each entry is one child, and the parent treats all children uniformly except for whatever per-child information their parent data carries. Rows, columns, stacks, wraps, and flow layouts all fit this pattern.
For this list-based model, the Flutter framework provides nearly everything you need. MultiChildRenderObjectWidget
and ContainerRenderObjectMixin handle the reconciliation and render-tree wiring; your job is to extend them correctly and provide the per-child layout state your render object needs. We'll focus entirely on child management; layout and paint are out of scope.
What This Guide Covers#
-
When the List-Based Model Fits — Recognizing widget APIs that match what
MultiChildRenderObjectWidgetprovides. - When You Need a Different Approach — Pointers to where the list-based model breaks down.
- The Pieces Involved — The widget, element, parent data, and render object, and which the framework provides.
- Extending
MultiChildRenderObjectWidget— Exposing the children list. - Defining Parent Data for Children — The per-child state that lives on each child render object.
-
Mixing In
ContainerRenderObjectMixin— Giving your render object a linked list of children. - List Order vs Visual Order — Why the children list isn't always the same as the layout's visual sequence.
- How the Pieces Connect — Where the framework hands off between widget, element, parent data, and render object.
- Keys and Reconciliation — Why keyed children preserve state across reorderings.
-
ParentDataWidgets — How widgets attach per-child layout information from the widget tree. - What You Get For Free — Reconciliation, mounting, updating, render-tree attachment, reordering, and reparenting.
- Common Pitfalls — The traps that list-based multi-child setups produce.
When the List-Based Model Fits#
MultiChildRenderObjectWidget is designed for widgets whose API takes a flat List<Widget>
of children. The defining trait is the API shape: a single children parameter typed as
List<Widget>, with optional ParentDataWidgets wrapping individual children to attach per-child layout instructions.
Some canonical examples:
-
RowandColumn. Children are ordered along a single axis. List position is the position in the layout. -
Stack. Children are layered on top of each other. List order is paint order;Positionedparent data optionally specifies absolute placement. -
WrapandFlow. Children are arranged by the parent's algorithm. List order is iteration order; placement is computed during layout. -
ListBody. Children are stacked along an axis with their natural intrinsic sizes.
The common thread is that the children list is flat. Each entry represents one child, and the framework's reconciliation algorithm works directly against that list — matching new and old entries by position and key, deciding which to update, reorder, insert, or remove.
When You Need a Different Approach#
If your widget's API takes anything other than a flat List<Widget> of children, the standard model doesn't fit. Some examples that need a custom element instead:
- A grid that takes
List<List<Widget>>(rows of cells). - A sparse layout that takes
Map<GridCoord, Widget>. - A widget with a custom child data structure that exposes per-coordinate or per-key access.
-
A widget that mixes child models — for example, one
headerslot and aList<Widget>for items.
For those cases, you need a custom RenderObjectElement that knows how to walk your specific structure and reconcile children from it. That's covered in a separate guide on custom structured children. The rest of this guide assumes your widget's API is a flat list.
The Pieces Involved#
A list-based multi-child setup has four parts:
-
A widget that exposes a
childrenlist of typeList<Widget>. - An element that reconciles the list — matching keys, handling reordering, insertion, and removal.
- A parent data type that holds per-child layout state on each child render object.
- A render object that holds a linked list of children and provides methods to insert, move, and remove them.
The framework provides the element for free: MultiChildRenderObjectWidget.createElement returns a
MultiChildRenderObjectElement, which implements the reconciliation logic correctly, including the keyed-matching algorithm. You'll almost never subclass it.
The framework also provides ContainerRenderObjectMixin and ContainerParentDataMixin
for the render-object side. Together they handle the linked-list bookkeeping, child adoption and dropping, and visitation.
What you write is: the widget (extending a framework base class), a parent data type (extending ContainerBoxParentData), and the render object (mixing in
ContainerRenderObjectMixin and RenderBoxContainerDefaultsMixin).
Extending MultiChildRenderObjectWidget#
MultiChildRenderObjectWidget is the framework's base class for list-based multi-child widgets. It already declares the
children field:
class MyStack extends MultiChildRenderObjectWidget {
const MyStack({
super.key,
super.children,
this.alignment = Alignment.topLeft,
});
final Alignment alignment;
@override
RenderMyStack createRenderObject(BuildContext context) {
return RenderMyStack(alignment: alignment);
}
@override
void updateRenderObject(BuildContext context, RenderMyStack renderObject) {
renderObject.alignment = alignment;
}
}
A few notes:
-
super.childrenforwards the children list to the base class. You don't redeclare it. -
createRenderObjectandupdateRenderObjecthandle non-child widget properties (alignmenthere). Child management is not your concern in these methods — the element handles it separately. -
createElementis inherited from the base class and returns aMultiChildRenderObjectElement. You don't override it.
Defining Parent Data for Children#
Multi-child render objects use parent data for two things: linked-list bookkeeping (the previousSibling
and nextSibling pointers that ContainerParentDataMixin adds) and per-child layout state.
For an ordered layout like a row or column, the existing ContainerBoxParentData<RenderBox>
is enough — it gives you offset plus sibling pointers, which is everything you need.
For layouts where each child needs additional layout information, you subclass ContainerBoxParentData
and add fields. For a stack with positioned children:
class MyStackParentData extends ContainerBoxParentData<RenderBox> {
double? top;
double? right;
double? bottom;
double? left;
double? width;
double? height;
}
For a flex layout:
class MyFlexParentData extends ContainerBoxParentData<RenderBox> {
int flex = 0;
FlexFit fit = FlexFit.loose;
}
The pattern is the same across both: subclass ContainerBoxParentData<RenderBox> and add fields for the per-child layout state your render object needs. These fields are usually populated by a
ParentDataWidget (covered below), and the render object reads them during layout.
You'll install this parent data type on children via setupParentData on the render object, covered in the next section.
Mixing In ContainerRenderObjectMixin#
Your render object holds children in a linked list and provides methods to insert, move, and remove them. Two mixins work together:
-
ContainerRenderObjectMixin<ChildType, ParentDataType>— The linked-list bookkeeping. ProvidesfirstChild,lastChild,childCount,insert,move, andremove. -
RenderBoxContainerDefaultsMixin<ChildType, ParentDataType>— Convenient defaults for common box operations likedefaultPaint,defaultHitTestChildren, anddefaultComputeDistanceToFirstActualBaseline.
class RenderMyStack extends RenderBox
with ContainerRenderObjectMixin<RenderBox, MyStackParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, MyStackParentData> {
RenderMyStack({Alignment alignment = Alignment.topLeft})
: _alignment = alignment;
Alignment _alignment;
Alignment get alignment => _alignment;
set alignment(Alignment value) {
if (_alignment == value) return;
_alignment = value;
markNeedsLayout();
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! MyStackParentData) {
child.parentData = MyStackParentData();
}
}
// Layout, paint, etc. go here. They iterate via firstChild + childAfter.
}
The important pieces:
-
The type parameters on both mixins —
RenderBoxfor the child type,MyStackParentDatafor the parent data type. They must match. -
setupParentDatainstalls your parent data subclass on each child as it's adopted. Theis!check preserves existing parent data if it's already the right type, which matters for keyed reparenting and for parent data that was already populated by aParentDataWidget.
The mixin gives you:
-
A linked list of children accessible via
firstChild,lastChild,childBefore(child),childAfter(child), andchildCount. - Methods to insert, move, and remove children that handle adoption, dropping, and parent-data setup automatically.
- A default
visitChildrenthat walks children in linked-list order. - Attach/detach propagation to children.
- Render-tree adoption and dropping when children are inserted or removed.
You don't write any of those yourself. Your layout, paint, and hit-test methods iterate over the linked list and read each child's parent data to decide placement.
List Order vs Visual Order#
A point worth being explicit about: the order of the children list in your widget is the order the framework uses for
reconciliation and linked-list storage. It's not necessarily the order children appear visually.
For an ordered layout like a row, the two coincide: index 0 in the list is the leftmost child visually, index 1 is next, and so on. The render object iterates
firstChild → lastChild and lays them out in that order.
For a stack with Positioned children, list order doesn't determine where children appear — parent data does. The
children list might be [a, b, c], but each child's top, left, etc. determine its actual placement. What the list order does control is:
- Which child element corresponds to which widget during reconciliation (matched by position and key).
-
The order in which children appear in the render-object's linked list, which affects paint order (and therefore z-order in case of overlap), hit-test order, and the order of
visitChildren.
The practical implication is that you can use the list-based model for any layout whose API is a flat list of children, regardless of whether visual placement comes from list position or from parent data. The list is for identity and reconciliation; visual layout is up to your render object.
How the Pieces Connect#
When your widget first appears in the tree:
- Flutter creates a
MyStackwidget instance. -
The framework calls
createElement, which returns a newMultiChildRenderObjectElement. -
The element mounts and calls
createRenderObject, which returns aRenderMyStack. -
The element calls its internal
updateChildrenhelper with the new children list. This creates a child element for each widget in the list, in order. -
Each child element is mounted, and (if it owns a render object) the framework calls
insertRenderObjectChildon the parent element with the new render object and anIndexedSlotidentifying its position. -
The element's
insertRenderObjectChildimplementation callsrenderObject.insert(child, after: ...)to add the child to the linked list at the right place. -
As each child is adopted, the render object's
setupParentDataruns and installs the right parent data type.
On rebuilds:
- The framework hands the existing element a new
MyStackwidget. -
The element calls
updateRenderObject, forwarding non-child properties likealignment. -
The element calls
updateChildrenwith the new children list. The reconciliation algorithm walks the old and new lists together, matching by runtime type and key:- Children that match are updated in place.
- Children that moved (matched by key but at different positions) trigger
moveRenderObjectChildcalls, which callrenderObject.move(child, after: ...). - Children that no longer appear trigger
removeRenderObjectChild, which callsrenderObject.remove(child). - New children trigger
insertRenderObjectChild.
The framework handles all of this. You don't write any reconciliation logic — you just provide the storage shape via the mixins.
Keys and Reconciliation#
The reconciliation algorithm distinguishes between children based on a combination of runtime type and key. Two scenarios highlight why this matters:
- No keys. When children don't have keys, the algorithm matches them positionally. If the first child in the new list has the same runtime type as the first child in the old list, they're considered the same element and updated in place. If a child is inserted in the middle, every child after the insertion point shifts and is treated as a new element — even if it represents the "same" widget logically. Stateful children at shifted positions lose their state.
-
With keys. When children have keys, the algorithm matches them across positions. A keyed child that moves from index 3 to index 1 is recognized as the same element and preserved, with its render object simply moved within the linked list (via
moveRenderObjectChild). State, animation progress, scroll position — all preserved.
For lists where children might be inserted or reordered (a sortable list, a reorderable column), keys are essential. For lists where the set of children never changes, keys are optional but cost very little.
The framework handles all of this for you through the reconciliation algorithm. Your job is to ensure your widget's
children list reflects the intended logical structure, including keys for anything that should preserve identity across reorderings.
ParentDataWidgets#
When per-child layout state needs to come from the widget tree rather than from the parent computing it, the standard mechanism is a
ParentDataWidget. These wrap a child and write values to its parent data when the element tree updates.
The familiar examples are Positioned (writes to StackParentData), Expanded
and Flexible (write to FlexParentData), and LayoutId (writes to
MultiChildLayoutParentData).
For your custom stack, you'd provide a ParentDataWidget that writes positioning to each child:
class MyPositioned extends ParentDataWidget<MyStackParentData> {
const MyPositioned({
super.key,
this.top,
this.right,
this.bottom,
this.left,
this.width,
this.height,
required super.child,
});
final double? top;
final double? right;
final double? bottom;
final double? left;
final double? width;
final double? height;
@override
void applyParentData(RenderObject renderObject) {
final parentData = renderObject.parentData! as MyStackParentData;
bool needsLayout = false;
if (parentData.top != top) { parentData.top = top; needsLayout = true; }
if (parentData.right != right) { parentData.right = right; needsLayout = true; }
if (parentData.bottom != bottom) { parentData.bottom = bottom; needsLayout = true; }
if (parentData.left != left) { parentData.left = left; needsLayout = true; }
if (parentData.width != width) { parentData.width = width; needsLayout = true; }
if (parentData.height != height) { parentData.height = height; needsLayout = true; }
if (needsLayout) {
renderObject.parent?.markNeedsLayout();
}
}
@override
Type get debugTypicalAncestorWidgetClass => MyStack;
}
Users of your widget can then wrap children that need positioning:
MyStack(
children: [
Container(color: Colors.red),
MyPositioned(
top: 20,
left: 20,
child: Text('Overlay'),
),
],
)
This is how positioning information flows from the widget API to the render object's per-child state without forcing the parent's widget to know each child's specific layout. The parent data guide covers
ParentDataWidget in more detail.
What You Get For Free#
Because you're extending the framework's base classes, you don't write:
-
Reconciliation logic — The element's
updateChildrenalready handles the keyed-matching algorithm, including reordering, insertion, and removal. - Mount and update logic — The element handles both, calling reconciliation at the right times.
-
insertRenderObjectChild/moveRenderObjectChild/removeRenderObjectChild— Already wired to call the render-object mixin'sinsert,move, andremovemethods. -
visitChildren/forgetChild— Already implemented correctly. - Linked-list bookkeeping — Handled by
ContainerRenderObjectMixin. -
Adoption and dropping — Also handled by the mixin, including
setupParentDatacalls. - Render-tree attach/detach propagation — Handled by the mixin.
- GlobalKey reparenting — Handled by the framework's element implementation.
What you provide is:
-
A widget that extends
MultiChildRenderObjectWidgetand forwardschildrenviasuper.children. -
A parent data type (often
ContainerBoxParentData<RenderBox>directly, or a subclass with extra fields). -
A render object that uses
ContainerRenderObjectMixinandRenderBoxContainerDefaultsMixin, with asetupParentDataoverride. createRenderObjectandupdateRenderObjectfor non-child widget properties.-
Optionally, a
ParentDataWidgetfor users to attach per-child layout state from the widget tree.
Common Pitfalls#
Redeclaring children on your widget. MultiChildRenderObjectWidget
already declares final List<Widget> children. If you redeclare it, you'll shadow the base-class field and break reconciliation. Always forward via
super.children.
Mismatched type parameters on the mixins. ContainerRenderObjectMixin<RenderBox, MyStackParentData>
must match RenderBoxContainerDefaultsMixin<RenderBox, MyStackParentData>. If they don't agree, the mixin's helper methods won't compile.
Forgetting the is! check in setupParentData. Unconditionally creating a new parent data instance discards values that may have been just written by a
ParentDataWidget. The check ensures you only replace parent data when the type is actually wrong.
Using positional reconciliation when keys are needed. If children can be reordered (sortable lists, filterable lists, anything that changes the order in the
children list across rebuilds), they need keys. Without keys, reordering looks to the framework like deletion and insertion at every shifted position, and stateful children lose their state.
Assuming list order equals visual order. This is true for ordered layouts (rows, columns), but not for stacks with positioned children or any layout where placement comes from parent data. The list order determines reconciliation order and paint order, not visual placement.
Marking the wrong render object dirty in applyParentData. When a ParentDataWidget
writes to a child's parent data, it's the parent's layout that needs to re-run — not the child's. Use
renderObject.parent?.markNeedsLayout(), not renderObject.markNeedsLayout().
Trying to manage children manually. It's tempting to override mount, update, or the insert/move/remove callbacks. Almost always, the default
MultiChildRenderObjectElement does exactly what you want. If you find yourself reaching for a custom element, ask whether your widget's API could be reshaped as a flat list with parent data — and if it genuinely can't, you're in the territory of the custom-structured-children guide rather than this one.
That's the complete list-based multi-child setup. A widget that extends MultiChildRenderObjectWidget, a parent data type for per-child layout state, and a render object that mixes in
ContainerRenderObjectMixin and RenderBoxContainerDefaultsMixin. The framework handles everything related to reconciliation, attachment, and lifecycle.