updateChildren method
Reconcile old children with new widgets using Flutter's algorithm.
Matching strategy:
- Walk from the top matching by Widget.canUpdate (runtimeType + key).
- Walk from the bottom matching the same way.
- For the middle section, build a keyed map from old children that have explicit keys. Unkeyed old children in the middle are unmounted.
- Walk the middle of the new list, looking up matches by key.
- Unmount any remaining unmatched keyed old children.
Implementation
void updateChildren(List<Widget> newWidgets) {
if (_children.isEmpty && newWidgets.isEmpty) return;
var structureChanged = false;
int newChildrenTop = 0;
int oldChildrenTop = 0;
int newChildrenBottom = newWidgets.length - 1;
int oldChildrenBottom = _children.length - 1;
final newChildren = List<Element?>.filled(newWidgets.length, null);
// Scan from the top: match while canUpdate holds.
while (oldChildrenTop <= oldChildrenBottom &&
newChildrenTop <= newChildrenBottom) {
final oldChild = _children[oldChildrenTop];
final newWidget = newWidgets[newChildrenTop];
if (!Widget.canUpdate(oldChild.widget, newWidget)) break;
oldChild.update(newWidget);
newChildren[newChildrenTop] = oldChild;
newChildrenTop++;
oldChildrenTop++;
}
// Scan from the bottom: match while canUpdate holds.
while (oldChildrenTop <= oldChildrenBottom &&
newChildrenTop <= newChildrenBottom) {
final oldChild = _children[oldChildrenBottom];
final newWidget = newWidgets[newChildrenBottom];
if (!Widget.canUpdate(oldChild.widget, newWidget)) break;
// Don't update yet — just narrow the window. We update bottom matches
// after the middle is resolved so indices are stable.
oldChildrenBottom--;
newChildrenBottom--;
}
// Build a keyed map of the remaining old children in the middle.
final haveOldChildren = oldChildrenTop <= oldChildrenBottom;
Map<Key, Element>? oldKeyedChildren;
if (haveOldChildren) {
oldKeyedChildren = <Key, Element>{};
while (oldChildrenTop <= oldChildrenBottom) {
final oldChild = _children[oldChildrenTop];
if (oldChild.widget.key != null) {
oldKeyedChildren[oldChild.widget.key!] = oldChild;
} else {
// Unkeyed old children in the middle cannot be matched — unmount.
oldChild.unmount();
structureChanged = true;
}
oldChildrenTop++;
}
}
// Walk the middle of the new list, matching by key.
while (newChildrenTop <= newChildrenBottom) {
Element? oldChild;
final newWidget = newWidgets[newChildrenTop];
if (newWidget.key != null && oldKeyedChildren != null) {
oldChild = oldKeyedChildren[newWidget.key!];
if (oldChild != null) {
if (Widget.canUpdate(oldChild.widget, newWidget)) {
oldKeyedChildren.remove(newWidget.key!);
} else {
oldChild = null;
}
}
}
if (oldChild != null) {
oldChild.update(newWidget);
newChildren[newChildrenTop] = oldChild;
} else {
final element = createElement(newWidget);
element._attachOwner(_owner);
element.mount(this);
newChildren[newChildrenTop] = element;
structureChanged = true;
}
newChildrenTop++;
}
// Now update the bottom matches (we deferred these earlier).
assert(
newWidgets.length - newChildrenTop == _children.length - oldChildrenTop,
);
newChildrenBottom = newWidgets.length - 1;
oldChildrenBottom = _children.length - 1;
while (newChildrenTop <= newChildrenBottom) {
final oldChild = _children[oldChildrenTop];
final newWidget = newWidgets[newChildrenTop];
assert(Widget.canUpdate(oldChild.widget, newWidget));
oldChild.update(newWidget);
newChildren[newChildrenTop] = oldChild;
newChildrenTop++;
oldChildrenTop++;
}
// Unmount leftover keyed old children that were not reused.
if (oldKeyedChildren != null && oldKeyedChildren.isNotEmpty) {
for (final oldChild in oldKeyedChildren.values) {
oldChild.unmount();
structureChanged = true;
}
}
_children
..clear()
..addAll(newChildren.cast<Element>());
if (structureChanged) {
final host = _renderObjectHost();
if (host != null && !host._isRebuilding) {
host._markDirty();
}
}
}