diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 35358bec454e0d1cbbafd2b85b1df1a8eb9837fd..53b71628ede0442890acff16e53904cb517e48d2 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -1605,8 +1605,8 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) { Element oldChild = oldChildren[oldChildrenTop]; Widget newWidget = newWidgets[newChildrenTop]; - assert(oldChild._debugLifecycleState == _ElementLifecycle.active); - if (!Widget.canUpdate(oldChild.widget, newWidget)) + assert(oldChild == null || oldChild._debugLifecycleState == _ElementLifecycle.active); + if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget)) break; Element newChild = updateChild(oldChild, newWidget, previousChild); assert(newChild._debugLifecycleState == _ElementLifecycle.active); @@ -1620,8 +1620,8 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) { Element oldChild = oldChildren[oldChildrenBottom]; Widget newWidget = newWidgets[newChildrenBottom]; - assert(oldChild._debugLifecycleState == _ElementLifecycle.active); - if (!Widget.canUpdate(oldChild.widget, newWidget)) + assert(oldChild == null || oldChild._debugLifecycleState == _ElementLifecycle.active); + if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget)) break; oldChildrenBottom -= 1; newChildrenBottom -= 1; @@ -1634,11 +1634,13 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab oldKeyedChildren = new Map<Key, Element>(); while (oldChildrenTop <= oldChildrenBottom) { Element oldChild = oldChildren[oldChildrenTop]; - assert(oldChild._debugLifecycleState == _ElementLifecycle.active); - if (oldChild.widget.key != null) - oldKeyedChildren[oldChild.widget.key] = oldChild; - else - _deactivateChild(oldChild); + assert(oldChild == null || oldChild._debugLifecycleState == _ElementLifecycle.active); + if (oldChild != null) { + if (oldChild.widget.key != null) + oldKeyedChildren[oldChild.widget.key] = oldChild; + else + _deactivateChild(oldChild); + } oldChildrenTop += 1; } } @@ -1682,6 +1684,7 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab // Update the bottom of the list. while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) { Element oldChild = oldChildren[oldChildrenTop]; + assert(oldChild != null); assert(oldChild._debugLifecycleState == _ElementLifecycle.active); Widget newWidget = newWidgets[newChildrenTop]; assert(Widget.canUpdate(oldChild.widget, newWidget)); @@ -1826,6 +1829,19 @@ class MultiChildRenderObjectElement<T extends MultiChildRenderObjectWidget> exte } List<Element> _children; + // We null out detached children lazily to avoid O(n^2) work walking _children + // repeatedly to remove children. + Set<Element> _detachedChildren; + + void _replaceDetachedChildrenWithNull() { + if (_detachedChildren != null && _detachedChildren.isNotEmpty) { + for (int i = 0; i < _children.length; ++i) { + if (_detachedChildren.contains(_children[i])) + _children[i] = null; + } + _detachedChildren.clear(); + } + } void insertChildRenderObject(RenderObject child, Element slot) { final ContainerRenderObjectMixin renderObject = this.renderObject; @@ -1865,8 +1881,18 @@ class MultiChildRenderObjectElement<T extends MultiChildRenderObjectWidget> exte } void visitChildren(ElementVisitor visitor) { - for (Element child in _children) - visitor(child); + _replaceDetachedChildrenWithNull(); + for (Element child in _children) { + if (child != null) + visitor(child); + } + } + + bool detachChild(Element child) { + _detachedChildren ??= new Set<Element>(); + _detachedChildren.add(child); + _deactivateChild(child); + return true; } void mount(Element parent, dynamic newSlot) { @@ -1883,6 +1909,7 @@ class MultiChildRenderObjectElement<T extends MultiChildRenderObjectWidget> exte void update(T newWidget) { super.update(newWidget); assert(widget == newWidget); + _replaceDetachedChildrenWithNull(); _children = updateChildren(_children, widget.children); } } diff --git a/packages/flutter/test/widget/reparent_state_test.dart b/packages/flutter/test/widget/reparent_state_test.dart index 024e34c1c392b0bad19e3956e9bcb6cbb17cde72..4577d9ae763e3f4b971778f6e05863c185e2ac07 100644 --- a/packages/flutter/test/widget/reparent_state_test.dart +++ b/packages/flutter/test/widget/reparent_state_test.dart @@ -95,6 +95,68 @@ void main() { }); }); + test('can reparent state with multichild widgets', () { + testWidgets((WidgetTester tester) { + GlobalKey left = new GlobalKey(); + GlobalKey right = new GlobalKey(); + + StateMarker grandchild = new StateMarker(); + tester.pumpWidget( + new Stack( + children: <Widget>[ + new StateMarker(key: left), + new StateMarker( + key: right, + child: grandchild + ) + ] + ) + ); + + (left.currentState as StateMarkerState).marker = "left"; + (right.currentState as StateMarkerState).marker = "right"; + + StateMarkerState grandchildState = tester.findStateByConfig(grandchild); + expect(grandchildState, isNotNull); + grandchildState.marker = "grandchild"; + + StateMarker newGrandchild = new StateMarker(); + tester.pumpWidget( + new Stack( + children: <Widget>[ + new StateMarker( + key: right, + child: newGrandchild + ), + new StateMarker(key: left) + ] + ) + ); + + expect((left.currentState as StateMarkerState).marker, equals("left")); + expect((right.currentState as StateMarkerState).marker, equals("right")); + + StateMarkerState newGrandchildState = tester.findStateByConfig(newGrandchild); + expect(newGrandchildState, isNotNull); + expect(newGrandchildState, equals(grandchildState)); + expect(newGrandchildState.marker, equals("grandchild")); + + tester.pumpWidget( + new Center( + child: new Container( + child: new StateMarker( + key: left, + child: new Container() + ) + ) + ) + ); + + expect((left.currentState as StateMarkerState).marker, equals("left")); + expect(right.currentState, isNull); + }); + }); + test('can with scrollable list', () { testWidgets((WidgetTester tester) { GlobalKey key = new GlobalKey();