Keep render tree and element tree in sync when re-used elements move in a MultiChildRenderObjectElement's child list (#51674)
diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart
index 5c458aa..7867c11 100644
--- a/packages/flutter/lib/src/widgets/framework.dart
+++ b/packages/flutter/lib/src/widgets/framework.dart
@@ -5433,8 +5433,35 @@
/// acts as if the child was not in `oldChildren`.
///
/// This function is a convenience wrapper around [updateChild], which updates
- /// each individual child. When calling [updateChild], this function uses the
- /// previous element as the `newSlot` argument.
+ /// each individual child. When calling [updateChild], this function uses an
+ /// [IndexedSlot<Element>] as the value for the `newSlot` argument.
+ /// [IndexedSlot.index] is set to the index that the currently processed
+ /// `child` corresponds to in the `newWidgets` list and [IndexedSlot.value] is
+ /// set to the [Element] of the previous widget in that list (or null if it is
+ /// the first child).
+ ///
+ /// When the [slot] value of an [Element] changes, its
+ /// associated [renderObject] needs to move to a new position in the child
+ /// list of its parents. If that [RenderObject] organizes its children in a
+ /// linked list (as is done by the [ContainerRenderObjectMixin]) this can
+ /// be implemented by re-inserting the child [RenderObject] into the
+ /// list after the [RenderObject] associated with the [Element] provided as
+ /// [IndexedSlot.value] in the [slot] object.
+ ///
+ /// Simply using the previous sibling as a [slot] is not enough, though, because
+ /// child [RenderObject]s are only moved around when the [slot] of their
+ /// associated [RenderObjectElement]s is updated. When the order of child
+ /// [Element]s is changed, some elements in the list may move to a new index
+ /// but still have the same previous sibling. For example, when
+ /// `[e1, e2, e3, e4]` is changed to `[e1, e3, e4, e2]` the element e4
+ /// continues to have e3 as a previous sibling even though its index in the list
+ /// has changed and its [RenderObject] needs to move to come before e2's
+ /// [RenderObject]. In order to trigger this move, a new [slot] value needs to
+ /// be assigned to its [Element] whenever its index in its
+ /// parent's child list changes. Using an [IndexedSlot<Element>] achieves
+ /// exactly that and also ensures that the underlying parent [RenderObject]
+ /// knows where a child needs to move to in a linked list by providing its new
+ /// previous sibling.
@protected
List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element> forgottenChildren }) {
assert(oldChildren != null);
@@ -5492,7 +5519,7 @@
assert(oldChild == null || oldChild._debugLifecycleState == _ElementLifecycle.active);
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
break;
- final Element newChild = updateChild(oldChild, newWidget, previousChild);
+ final Element newChild = updateChild(oldChild, newWidget, IndexedSlot<Element>(newChildrenTop, previousChild));
assert(newChild._debugLifecycleState == _ElementLifecycle.active);
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
@@ -5550,7 +5577,7 @@
}
}
assert(oldChild == null || Widget.canUpdate(oldChild.widget, newWidget));
- final Element newChild = updateChild(oldChild, newWidget, previousChild);
+ final Element newChild = updateChild(oldChild, newWidget, IndexedSlot<Element>(newChildrenTop, previousChild));
assert(newChild._debugLifecycleState == _ElementLifecycle.active);
assert(oldChild == newChild || oldChild == null || oldChild._debugLifecycleState != _ElementLifecycle.active);
newChildren[newChildrenTop] = newChild;
@@ -5572,7 +5599,7 @@
assert(oldChild._debugLifecycleState == _ElementLifecycle.active);
final Widget newWidget = newWidgets[newChildrenTop];
assert(Widget.canUpdate(oldChild.widget, newWidget));
- final Element newChild = updateChild(oldChild, newWidget, previousChild);
+ final Element newChild = updateChild(oldChild, newWidget, IndexedSlot<Element>(newChildrenTop, previousChild));
assert(newChild._debugLifecycleState == _ElementLifecycle.active);
assert(oldChild == newChild || oldChild == null || oldChild._debugLifecycleState != _ElementLifecycle.active);
newChildren[newChildrenTop] = newChild;
@@ -5666,10 +5693,12 @@
/// Insert the given child into [renderObject] at the given slot.
///
+ /// {@template flutter.widgets.slots}
/// The semantics of `slot` are determined by this element. For example, if
/// this element has a single child, the slot should always be null. If this
- /// element has a list of children, the previous sibling is a convenient value
- /// for the slot.
+ /// element has a list of children, the previous sibling element wrapped in an
+ /// [IndexedSlot] is a convenient value for the slot.
+ /// {@endtemplate}
@protected
void insertChildRenderObject(covariant RenderObject child, covariant dynamic slot);
@@ -5677,10 +5706,7 @@
///
/// The given child is guaranteed to have [renderObject] as its parent.
///
- /// The semantics of `slot` are determined by this element. For example, if
- /// this element has a single child, the slot should always be null. If this
- /// element has a list of children, the previous sibling is a convenient value
- /// for the slot.
+ /// {@macro flutter.widgets.slots}
///
/// This method is only ever called if [updateChild] can end up being called
/// with an existing [Element] child and a `slot` that differs from the slot
@@ -5840,6 +5866,13 @@
/// RenderObjects use the [ContainerRenderObjectMixin] mixin with a parent data
/// type that implements [ContainerParentDataMixin<RenderObject>]. Such widgets
/// are expected to inherit from [MultiChildRenderObjectWidget].
+///
+/// See also:
+///
+/// * [IndexedSlot], which is used as [Element.slot]s for the children of a
+/// [MultiChildRenderObjectElement].
+/// * [RenderObjectElement.updateChildren], which discusses why [IndexedSlot]
+/// is used for the slots of the children.
class MultiChildRenderObjectElement extends RenderObjectElement {
/// Creates an element that uses the given widget as its configuration.
MultiChildRenderObjectElement(MultiChildRenderObjectWidget widget)
@@ -5863,20 +5896,20 @@
final Set<Element> _forgottenChildren = HashSet<Element>();
@override
- void insertChildRenderObject(RenderObject child, Element slot) {
+ void insertChildRenderObject(RenderObject child, IndexedSlot<Element> slot) {
final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>> renderObject =
this.renderObject as ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>>;
assert(renderObject.debugValidateChild(child));
- renderObject.insert(child, after: slot?.renderObject);
+ renderObject.insert(child, after: slot?.value?.renderObject);
assert(renderObject == this.renderObject);
}
@override
- void moveChildRenderObject(RenderObject child, Element slot) {
+ void moveChildRenderObject(RenderObject child, IndexedSlot<Element> slot) {
final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>> renderObject =
this.renderObject as ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>>;
assert(child.parent == renderObject);
- renderObject.move(child, after: slot?.renderObject);
+ renderObject.move(child, after: slot?.value?.renderObject);
assert(renderObject == this.renderObject);
}
@@ -5911,7 +5944,7 @@
_children = List<Element>(widget.children.length);
Element previousChild;
for (int i = 0; i < _children.length; i += 1) {
- final Element newChild = inflateWidget(widget.children[i], previousChild);
+ final Element newChild = inflateWidget(widget.children[i], IndexedSlot<Element>(i, previousChild));
_children[i] = newChild;
previousChild = newChild;
}
@@ -5957,3 +5990,41 @@
FlutterError.reportError(details);
return details;
}
+
+/// A value for [Element.slot] used for children of
+/// [MultiChildRenderObjectElement]s.
+///
+/// A slot for a [MultiChildRenderObjectElement] consists of an [index]
+/// identifying where the child occupying this slot is located in the
+/// [MultiChildRenderObjectElement]'s child list and an arbitrary [value] that
+/// can further define where the child occupying this slot fits in its
+/// parent's child list.
+///
+/// See also:
+///
+/// * [RenderObjectElement.updateChildren], which discusses why this class is
+/// used as slot values for the children of a [MultiChildRenderObjectElement].
+@immutable
+class IndexedSlot<T> {
+ /// Creates an [IndexedSlot] with the provided [index] and slot [value].
+ const IndexedSlot(this.index, this.value);
+
+ /// Information to define where the child occupying this slot fits in its
+ /// parent's child list.
+ final T value;
+
+ /// The index of this slot in the parent's child list.
+ final int index;
+
+ @override
+ bool operator ==(Object other) {
+ if (other.runtimeType != runtimeType)
+ return false;
+ return other is IndexedSlot
+ && index == other.index
+ && value == other.value;
+ }
+
+ @override
+ int get hashCode => hashValues(index, value);
+}
diff --git a/packages/flutter/lib/src/widgets/table.dart b/packages/flutter/lib/src/widgets/table.dart
index df42cc8..0d1ea1f 100644
--- a/packages/flutter/lib/src/widgets/table.dart
+++ b/packages/flutter/lib/src/widgets/table.dart
@@ -270,7 +270,7 @@
}
@override
- void insertChildRenderObject(RenderObject child, Element slot) {
+ void insertChildRenderObject(RenderObject child, IndexedSlot<Element> slot) {
renderObject.setupParentData(child);
}
diff --git a/packages/flutter/test/widgets/multichildobject_with_keys_test.dart b/packages/flutter/test/widgets/multichildobject_with_keys_test.dart
new file mode 100644
index 0000000..186025b
--- /dev/null
+++ b/packages/flutter/test/widgets/multichildobject_with_keys_test.dart
@@ -0,0 +1,72 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/rendering.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ testWidgets('Render and element tree stay in sync when keyed children move around', (WidgetTester tester) async {
+ // Regression test for https://github.com/flutter/flutter/issues/48855.
+
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: Column(
+ children: const <Widget>[
+ Text('0', key: ValueKey<int>(0)),
+ Text('1', key: ValueKey<int>(1)),
+ Text('2', key: ValueKey<int>(2)),
+ Text('3', key: ValueKey<int>(3)),
+ Text('4', key: ValueKey<int>(4)),
+ Text('5', key: ValueKey<int>(5)),
+ Text('6', key: ValueKey<int>(6)),
+ Text('7', key: ValueKey<int>(7)),
+ Text('8', key: ValueKey<int>(8)),
+ ],
+ ),
+ ),
+ );
+
+ expect(
+ _getChildOrder(tester.renderObject<RenderFlex>(find.byType(Column))),
+ <String>['0', '1', '2', '3', '4', '5', '6', '7', '8'],
+ );
+
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: Column(
+ children: const <Widget>[
+ Text('0', key: ValueKey<int>(0)),
+ Text('6', key: ValueKey<int>(6)),
+ Text('7', key: ValueKey<int>(7)),
+ Text('8', key: ValueKey<int>(8)),
+ Text('1', key: ValueKey<int>(1)),
+ Text('2', key: ValueKey<int>(2)),
+ Text('3', key: ValueKey<int>(3)),
+ Text('4', key: ValueKey<int>(4)),
+ Text('5', key: ValueKey<int>(5)),
+ ],
+ ),
+ ),
+ );
+
+ expect(
+ _getChildOrder(tester.renderObject<RenderFlex>(find.byType(Column))),
+ <String>['0', '6', '7', '8', '1', '2', '3', '4', '5'],
+ );
+ });
+}
+
+// Do not use tester.renderObjectList(find.byType(RenderParagraph). That returns
+// the RenderObjects in the order of their associated RenderObjectWidgets. The
+// point of this test is to assert the children order in the render tree, though.
+List<String> _getChildOrder(RenderFlex flex) {
+ final List<String> childOrder = <String>[];
+ flex.visitChildren((RenderObject child) {
+ childOrder.add(((child as RenderParagraph).text as TextSpan).text);
+ });
+ return childOrder;
+}