diff --git a/packages/flutter/lib/src/cupertino/action_sheet.dart b/packages/flutter/lib/src/cupertino/action_sheet.dart index 49f4c533d6..719c6fb117 100644 --- a/packages/flutter/lib/src/cupertino/action_sheet.dart +++ b/packages/flutter/lib/src/cupertino/action_sheet.dart @@ -469,7 +469,6 @@ class _CupertinoAlertRenderElement extends RenderObjectElement { } else if (_actionsElement == child) { _actionsElement = null; } - super.forgetChild(child); } @override diff --git a/packages/flutter/lib/src/cupertino/dialog.dart b/packages/flutter/lib/src/cupertino/dialog.dart index 305bba5d55..b91b8fbb99 100644 --- a/packages/flutter/lib/src/cupertino/dialog.dart +++ b/packages/flutter/lib/src/cupertino/dialog.dart @@ -464,7 +464,6 @@ class _CupertinoDialogRenderElement extends RenderObjectElement { assert(_actionsElement == child); _actionsElement = null; } - super.forgetChild(child); } @override diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart index 6e5db33da8..cdcb0d4c6f 100644 --- a/packages/flutter/lib/src/material/chip.dart +++ b/packages/flutter/lib/src/material/chip.dart @@ -2072,7 +2072,6 @@ class _RenderChipElement extends RenderObjectElement { final _ChipSlot slot = childToSlot[child]; childToSlot.remove(child); slotToChild.remove(slot); - super.forgetChild(child); } void _mountChild(Widget widget, _ChipSlot slot) { diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index b77267ffa4..00cbb800b1 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -1511,7 +1511,6 @@ class _RenderDecorationElement extends RenderObjectElement { final _DecorationSlot slot = childToSlot[child]; childToSlot.remove(child); slotToChild.remove(slot); - super.forgetChild(child); } void _mountChild(Widget widget, _DecorationSlot slot) { diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart index 8f05df4363..dba76f30a9 100644 --- a/packages/flutter/lib/src/material/list_tile.dart +++ b/packages/flutter/lib/src/material/list_tile.dart @@ -992,7 +992,6 @@ class _ListTileElement extends RenderObjectElement { final _ListTileSlot slot = childToSlot[child]; childToSlot.remove(child); slotToChild.remove(slot); - super.forgetChild(child); } void _mountChild(Widget widget, _ListTileSlot slot) { diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index c6232ed59a..8e3d813065 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -1016,7 +1016,6 @@ class RenderObjectToWidgetElement extends RootRenderObje void forgetChild(Element child) { assert(child == _child); _child = null; - super.forgetChild(child); } @override diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index eeeff9d933..d2a53d36a3 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -132,20 +132,7 @@ abstract class GlobalKey> extends Key { static final Map _registry = {}; static final Set _debugIllFatedElements = HashSet(); - // This map keeps track which child reserve the global key with the parent. - // Parent, child -> global key. - // This provides us a way to remove old reservation while parent rebuilds the - // child in the same slot. - static final Map> _debugReservations = >{}; - - static void _debugRemoveReservationFor(Element parent, Element child) { - assert(() { - assert(parent != null); - assert(child != null); - _debugReservations[parent]?.remove(child); - return true; - }()); - } + static final Map _debugReservations = {}; void _register(Element element) { assert(() { @@ -173,83 +160,46 @@ abstract class GlobalKey> extends Key { _registry.remove(this); } - void _debugReserveFor(Element parent, Element child) { + void _debugReserveFor(Element parent) { assert(() { assert(parent != null); - assert(child != null); - _debugReservations[parent] ??= {}; - _debugReservations[parent][child] = this; - return true; - }()); - } - - static void _debugVerifyGlobalKeyReservation() { - assert(() { - final Map keyToParent = {}; - _debugReservations.forEach((Element parent, Map chidToKey) { - // We ignore parent that are detached. - if (parent.renderObject?.attached == false) - return; - chidToKey.forEach((Element child, GlobalKey key) { - // If parent = null, the node is deactivated by its parent and is - // not re-attached to other part of the tree. We should ignore this - // node. - if (child._parent == null) - return; - // It is possible the same key registers to the same parent twice - // with different children. That is illegal, but it is not in the - // scope of this check. Such error will be detected in - // _debugVerifyIllFatedPopulation or - // _debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans. - if (keyToParent.containsKey(key) && keyToParent[key] != parent) { - // We have duplication reservations for the same global key. - final Element older = keyToParent[key]; - final Element newer = parent; - FlutterError error; - if (older.toString() != newer.toString()) { - error = FlutterError.fromParts([ - ErrorSummary('Multiple widgets used the same GlobalKey.'), - ErrorDescription( - 'The key $key was used by multiple widgets. The parents of those widgets were:\n' - '- ${older.toString()}\n' - '- ${newer.toString()}\n' - 'A GlobalKey can only be specified on one widget at a time in the widget tree.' - ), - ]); - } else { - error = FlutterError.fromParts([ - ErrorSummary('Multiple widgets used the same GlobalKey.'), - ErrorDescription( - 'The key $key was used by multiple widgets. The parents of those widgets were ' - 'different widgets that both had the following description:\n' - ' ${parent.toString()}\n' - 'A GlobalKey can only be specified on one widget at a time in the widget tree.' - ), - ]); - } - // Fix the tree by removing the duplicated child from one of its - // parents to resolve the duplicated key issue. This allows us to - // tear down the tree during testing without producing additional - // misleading exceptions. - if (child._parent != older) { - older.visitChildren((Element currentChild) { - if (currentChild == child) - older.forgetChild(child); - }); - } - if (child._parent != newer) { - newer.visitChildren((Element currentChild) { - if (currentChild == child) - newer.forgetChild(child); - }); - } - throw error; - } else { - keyToParent[key] = parent; - } - }); - }); - _debugReservations.clear(); + if (_debugReservations.containsKey(this) && _debugReservations[this] != parent) { + // Reserving a new parent while the old parent is not attached is ok. + // This can happen when a renderObject detaches and re-attaches to rendering + // tree multiple times. + if (_debugReservations[this].renderObject?.attached == false) { + _debugReservations[this] = parent; + return true; + } + // It's possible for an element to get built multiple times in one + // frame, in which case it'll reserve the same child's key multiple + // times. We catch multiple children of one widget having the same key + // by verifying that an element never steals elements from itself, so we + // don't care to verify that here as well. + final String older = _debugReservations[this].toString(); + final String newer = parent.toString(); + if (older != newer) { + throw FlutterError.fromParts([ + ErrorSummary('Multiple widgets used the same GlobalKey.'), + ErrorDescription( + 'The key $this was used by multiple widgets. The parents of those widgets were:\n' + '- $older\n' + '- $newer\n' + 'A GlobalKey can only be specified on one widget at a time in the widget tree.' + ), + ]); + } + throw FlutterError.fromParts([ + ErrorSummary('Multiple widgets used the same GlobalKey.'), + ErrorDescription( + 'The key $this was used by multiple widgets. The parents of those widgets were ' + 'different widgets that both had the following description:\n' + ' $parent\n' + 'A GlobalKey can only be specified on one widget at a time in the widget tree.' + ), + ]); + } + _debugReservations[this] = parent; return true; }()); } @@ -265,13 +215,13 @@ abstract class GlobalKey> extends Key { final GlobalKey key = element.widget.key as GlobalKey; assert(_registry.containsKey(key)); duplicates ??= >{}; - // Uses ordered set to produce consistent error message. - final Set elements = duplicates.putIfAbsent(key, () => LinkedHashSet()); + final Set elements = duplicates.putIfAbsent(key, () => HashSet()); elements.add(element); elements.add(_registry[key]); } } _debugIllFatedElements.clear(); + _debugReservations.clear(); if (duplicates != null) { final List information = []; information.add(ErrorSummary('Multiple widgets used the same GlobalKey.')); @@ -2689,7 +2639,6 @@ class BuildOwner { }); assert(() { try { - GlobalKey._debugVerifyGlobalKeyReservation(); GlobalKey._debugVerifyIllFatedPopulation(); if (_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans != null && _debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans.isNotEmpty) { @@ -3144,12 +3093,18 @@ abstract class Element extends DiagnosticableTree implements BuildContext { /// | **child != null** | Old child is removed, returns null. | Old child updated if possible, returns child or new [Element]. | @protected Element updateChild(Element child, Widget newWidget, dynamic newSlot) { + assert(() { + final Key key = newWidget?.key; + if (key is GlobalKey) { + key._debugReserveFor(this); + } + return true; + }()); if (newWidget == null) { if (child != null) deactivateChild(child); return null; } - Element newChild; if (child != null) { bool hasSameSuperclass = true; // When the type of a widget is changed between Stateful and Stateless via @@ -3173,40 +3128,28 @@ abstract class Element extends DiagnosticableTree implements BuildContext { hasSameSuperclass = oldElementClass == newWidgetClass; return true; }()); - if (child.widget == newWidget && hasSameSuperclass) { - if (child.slot != newSlot) - updateSlotForChild(child, newSlot); - newChild = child; - } else if (Widget.canUpdate(child.widget, newWidget) && hasSameSuperclass) { - if (child.slot != newSlot) - updateSlotForChild(child, newSlot); - child.update(newWidget); - assert(child.widget == newWidget); - assert(() { - child.owner._debugElementWasRebuilt(child); - return true; - }()); - newChild = child; - } else { - deactivateChild(child); - assert(child._parent == null); - newChild = inflateWidget(newWidget, newSlot); + if (hasSameSuperclass) { + if (child.widget == newWidget) { + if (child.slot != newSlot) + updateSlotForChild(child, newSlot); + return child; + } + if (Widget.canUpdate(child.widget, newWidget)) { + if (child.slot != newSlot) + updateSlotForChild(child, newSlot); + child.update(newWidget); + assert(child.widget == newWidget); + assert(() { + child.owner._debugElementWasRebuilt(child); + return true; + }()); + return child; + } } - } else { - newChild = inflateWidget(newWidget, newSlot); + deactivateChild(child); + assert(child._parent == null); } - - assert(() { - if (child != null) - _debugRemoveGlobalKeyReservation(child); - final Key key = newWidget?.key; - if (key is GlobalKey) { - key._debugReserveFor(this, newChild); - } - return true; - }()); - - return newChild; + return inflateWidget(newWidget, newSlot); } /// Add this element to the tree in the given slot of the given parent. @@ -3244,9 +3187,6 @@ abstract class Element extends DiagnosticableTree implements BuildContext { }()); } - void _debugRemoveGlobalKeyReservation(Element child) { - GlobalKey._debugRemoveReservationFor(this, child); - } /// Change the widget used to configure this element. /// /// The framework calls this function when the parent wishes to use a @@ -3265,16 +3205,6 @@ abstract class Element extends DiagnosticableTree implements BuildContext { && depth != null && _active && Widget.canUpdate(widget, newWidget)); - // This Element was told to update and we can now release all the global key - // reservations of forgotten children. We cannot do this earlier because the - // forgotten children still represent global key duplications if the element - // never updates (the forgotten children are not removed from the tree - // until the call to update happens) - assert(() { - _debugForgottenChildrenWithGlobalKey.forEach(_debugRemoveGlobalKeyReservation); - _debugForgottenChildrenWithGlobalKey.clear(); - return true; - }()); _widget = newWidget; } @@ -3471,10 +3401,6 @@ abstract class Element extends DiagnosticableTree implements BuildContext { }()); } - // The children that have been forgotten by forgetChild. This will be used in - // [update] to remove the global key reservations of forgotten children. - final Set _debugForgottenChildrenWithGlobalKey = HashSet(); - /// Remove the given child from the element's child list, in preparation for /// the child being reused elsewhere in the element tree. /// @@ -3483,23 +3409,12 @@ abstract class Element extends DiagnosticableTree implements BuildContext { /// /// The element will still have a valid parent when this is called. After this /// is called, [deactivateChild] is called to sever the link to this object. - /// - /// The [update] is responsible for updating or creating the new child that - /// will replace this [child]. @protected - @mustCallSuper void forgetChild(Element child) { - // This method is called on the old parent when the given child (with a - // global key) is given a new parent. We cannot remove the global key - // reservation directly in this method because the forgotten child is not - // removed from the tree until this Element is updated in [update]. If - // [update] is never called, the forgotten child still represents a global - // key duplication that we need to catch. - assert(() { - if (child.widget.key is GlobalKey) - _debugForgottenChildrenWithGlobalKey.add(child); - return true; - }()); + // TODO(chunhtai): Creates empty body for subclass to call super. This will + // enable us to fix internal tests pro-actively for upcoming breaking + // change. + // https://github.com/flutter/flutter/issues/43780. } void _activateWithParent(Element parent, dynamic newSlot) { @@ -4529,7 +4444,6 @@ abstract class ComponentElement extends Element { void forgetChild(Element child) { assert(child == _child); _child = null; - super.forgetChild(child); } } @@ -5683,7 +5597,6 @@ class LeafRenderObjectElement extends RenderObjectElement { @override void forgetChild(Element child) { assert(false); - super.forgetChild(child); } @override @@ -5733,7 +5646,6 @@ class SingleChildRenderObjectElement extends RenderObjectElement { void forgetChild(Element child) { assert(child == _child); _child = null; - super.forgetChild(child); } @override @@ -5840,7 +5752,6 @@ class MultiChildRenderObjectElement extends RenderObjectElement { assert(_children.contains(child)); assert(!_forgottenChildren.contains(child)); _forgottenChildren.add(child); - super.forgetChild(child); } @override diff --git a/packages/flutter/lib/src/widgets/layout_builder.dart b/packages/flutter/lib/src/widgets/layout_builder.dart index 77e6399eec..71382f24d1 100644 --- a/packages/flutter/lib/src/widgets/layout_builder.dart +++ b/packages/flutter/lib/src/widgets/layout_builder.dart @@ -60,7 +60,6 @@ class _LayoutBuilderElement extends RenderOb void forgetChild(Element child) { assert(child == _child); _child = null; - super.forgetChild(child); } @override diff --git a/packages/flutter/lib/src/widgets/list_wheel_scroll_view.dart b/packages/flutter/lib/src/widgets/list_wheel_scroll_view.dart index 40fdc2b0a2..bced3d08ac 100644 --- a/packages/flutter/lib/src/widgets/list_wheel_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/list_wheel_scroll_view.dart @@ -924,7 +924,6 @@ class ListWheelElement extends RenderObjectElement implements ListWheelChildMana @override void forgetChild(Element child) { _childElements.remove(child.slot); - super.forgetChild(child); } } diff --git a/packages/flutter/lib/src/widgets/sliver.dart b/packages/flutter/lib/src/widgets/sliver.dart index 8dd706cf48..2f9a8e4aa6 100644 --- a/packages/flutter/lib/src/widgets/sliver.dart +++ b/packages/flutter/lib/src/widgets/sliver.dart @@ -1160,7 +1160,6 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render assert(child.slot != null); assert(_childElements.containsKey(child.slot)); _childElements.remove(child.slot); - super.forgetChild(child); } @override diff --git a/packages/flutter/lib/src/widgets/sliver_persistent_header.dart b/packages/flutter/lib/src/widgets/sliver_persistent_header.dart index 2be682eb96..97f1a3d385 100644 --- a/packages/flutter/lib/src/widgets/sliver_persistent_header.dart +++ b/packages/flutter/lib/src/widgets/sliver_persistent_header.dart @@ -230,7 +230,6 @@ class _SliverPersistentHeaderElement extends RenderObjectElement { void forgetChild(Element child) { assert(child == this.child); this.child = null; - super.forgetChild(child); } @override diff --git a/packages/flutter/lib/src/widgets/table.dart b/packages/flutter/lib/src/widgets/table.dart index a6340505b8..8f9f160c08 100644 --- a/packages/flutter/lib/src/widgets/table.dart +++ b/packages/flutter/lib/src/widgets/table.dart @@ -344,7 +344,6 @@ class _TableElement extends RenderObjectElement { @override bool forgetChild(Element child) { _forgottenChildren.add(child); - super.forgetChild(child); return true; } } diff --git a/packages/flutter/test/foundation/diagnostics_json_test.dart b/packages/flutter/test/foundation/diagnostics_json_test.dart index b173f1cf54..4a52f77cd8 100644 --- a/packages/flutter/test/foundation/diagnostics_json_test.dart +++ b/packages/flutter/test/foundation/diagnostics_json_test.dart @@ -221,6 +221,11 @@ void main() { class _TestElement extends Element { _TestElement() : super(const Placeholder()); + @override + void forgetChild(Element child) { + // Intentionally left empty. + } + @override void performRebuild() { // Intentionally left empty. diff --git a/packages/flutter/test/widgets/framework_test.dart b/packages/flutter/test/widgets/framework_test.dart index 1a5fba15c8..d70ede2ceb 100644 --- a/packages/flutter/test/widgets/framework_test.dart +++ b/packages/flutter/test/widgets/framework_test.dart @@ -6,8 +6,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -typedef ElementRebuildCallback = void Function(StatefulElement element); - class TestState extends State { @override Widget build(BuildContext context) => null; @@ -63,358 +61,6 @@ void main() { expect(keyA, isNot(equals(keyB))); }); - testWidgets('GlobalKey correct case 1 - can move global key from container widget to layoutbuilder', (WidgetTester tester) async { - final Key key = GlobalKey(debugLabel: 'correct'); - await tester.pumpWidget(Stack( - textDirection: TextDirection.ltr, - children: [ - Container( - key: const ValueKey(1), - child: SizedBox(key: key), - ), - LayoutBuilder( - key: const ValueKey(2), - builder: (BuildContext context, BoxConstraints constraints) { - return const Placeholder(); - }, - ), - ], - )); - - await tester.pumpWidget(Stack( - textDirection: TextDirection.ltr, - children: [ - Container( - key: const ValueKey(1), - child: const Placeholder(), - ), - LayoutBuilder( - key: const ValueKey(2), - builder: (BuildContext context, BoxConstraints constraints) { - return SizedBox(key: key); - }, - ), - ], - )); - }); - - testWidgets('GlobalKey correct case 2 - can move global key from layoutbuilder to container widget', (WidgetTester tester) async { - final Key key = GlobalKey(debugLabel: 'correct'); - await tester.pumpWidget(Stack( - textDirection: TextDirection.ltr, - children: [ - Container( - key: const ValueKey(1), - child: const Placeholder(), - ), - LayoutBuilder( - key: const ValueKey(2), - builder: (BuildContext context, BoxConstraints constraints) { - return SizedBox(key: key); - }, - ), - ], - )); - await tester.pumpWidget(Stack( - textDirection: TextDirection.ltr, - children: [ - Container( - key: const ValueKey(1), - child: SizedBox(key: key), - ), - LayoutBuilder( - key: const ValueKey(2), - builder: (BuildContext context, BoxConstraints constraints) { - return const Placeholder(); - }, - ), - ], - )); - }); - - testWidgets('GlobalKey correct case 3 - can deal with early rebuild in layoutbuilder - move backward', (WidgetTester tester) async { - const Key key1 = GlobalObjectKey('Text1'); - const Key key2 = GlobalObjectKey('Text2'); - Key rebuiltKeyOfSecondChildBeforeLayout; - Key rebuiltKeyOfFirstChildAfterLayout; - Key rebuiltKeyOfSecondChildAfterLayout; - await tester.pumpWidget( - LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - children: [ - const _Stateful( - child: Text( - 'Text1', - textDirection: TextDirection.ltr, - key: key1, - ), - ), - _Stateful( - child: const Text( - 'Text2', - textDirection: TextDirection.ltr, - key: key2, - ), - onElementRebuild: (StatefulElement element) { - // We don't want noise to override the result; - expect(rebuiltKeyOfSecondChildBeforeLayout, isNull); - final _Stateful statefulWidget = element.widget as _Stateful; - rebuiltKeyOfSecondChildBeforeLayout = - statefulWidget.child.key; - }, - ), - ], - ); - }, - ) - ); - // Result will be written during first build and need to clear it to remove - // noise. - rebuiltKeyOfSecondChildBeforeLayout = null; - - final _StatefulState state = tester.firstState(find.byType(_Stateful).at(1)); - state.rebuild(); - // Reorders the items - await tester.pumpWidget( - LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - children: [ - _Stateful( - child: const Text( - 'Text2', - textDirection: TextDirection.ltr, - key: key2, - ), - onElementRebuild: (StatefulElement element) { - // Verifies the early rebuild happens before layout. - expect(rebuiltKeyOfSecondChildBeforeLayout, key2); - // We don't want noise to override the result; - expect(rebuiltKeyOfFirstChildAfterLayout, isNull); - final _Stateful statefulWidget = element.widget as _Stateful; - rebuiltKeyOfFirstChildAfterLayout = statefulWidget.child.key; - }, - ), - _Stateful( - child: const Text( - 'Text1', - textDirection: TextDirection.ltr, - key: key1, - ), - onElementRebuild: (StatefulElement element) { - // Verifies the early rebuild happens before layout. - expect(rebuiltKeyOfSecondChildBeforeLayout, key2); - // We don't want noise to override the result; - expect(rebuiltKeyOfSecondChildAfterLayout, isNull); - final _Stateful statefulWidget = element.widget as _Stateful; - rebuiltKeyOfSecondChildAfterLayout = statefulWidget.child.key; - }, - ), - ], - ); - }, - ) - ); - expect(rebuiltKeyOfSecondChildBeforeLayout, key2); - expect(rebuiltKeyOfFirstChildAfterLayout, key2); - expect(rebuiltKeyOfSecondChildAfterLayout, key1); - }); - - testWidgets('GlobalKey correct case 4 - can deal with early rebuild in layoutbuilder - move forward', (WidgetTester tester) async { - const Key key1 = GlobalObjectKey('Text1'); - const Key key2 = GlobalObjectKey('Text2'); - const Key key3 = GlobalObjectKey('Text3'); - Key rebuiltKeyOfSecondChildBeforeLayout; - Key rebuiltKeyOfSecondChildAfterLayout; - Key rebuiltKeyOfThirdChildAfterLayout; - await tester.pumpWidget( - LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - children: [ - const _Stateful( - child: Text( - 'Text1', - textDirection: TextDirection.ltr, - key: key1, - ), - ), - _Stateful( - child: const Text( - 'Text2', - textDirection: TextDirection.ltr, - key: key2, - ), - onElementRebuild: (StatefulElement element) { - // We don't want noise to override the result; - expect(rebuiltKeyOfSecondChildBeforeLayout, isNull); - final _Stateful statefulWidget = element.widget as _Stateful; - rebuiltKeyOfSecondChildBeforeLayout = statefulWidget.child.key; - }, - ), - const _Stateful( - child: Text( - 'Text3', - textDirection: TextDirection.ltr, - key: key3, - ), - ), - ], - ); - }, - ) - ); - // Result will be written during first build and need to clear it to remove - // noise. - rebuiltKeyOfSecondChildBeforeLayout = null; - - final _StatefulState state = tester.firstState(find.byType(_Stateful).at(1)); - state.rebuild(); - // Reorders the items - await tester.pumpWidget( - LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - children: [ - const _Stateful( - child: Text( - 'Text1', - textDirection: TextDirection.ltr, - key: key1, - ), - ), - _Stateful( - child: const Text( - 'Text3', - textDirection: TextDirection.ltr, - key: key3, - ), - onElementRebuild: (StatefulElement element) { - // Verifies the early rebuild happens before layout. - expect(rebuiltKeyOfSecondChildBeforeLayout, key2); - // We don't want noise to override the result; - expect(rebuiltKeyOfSecondChildAfterLayout, isNull); - final _Stateful statefulWidget = element.widget as _Stateful; - rebuiltKeyOfSecondChildAfterLayout = statefulWidget.child.key; - }, - ), - _Stateful( - child: const Text( - 'Text2', - textDirection: TextDirection.ltr, - key: key2, - ), - onElementRebuild: (StatefulElement element) { - // Verifies the early rebuild happens before layout. - expect(rebuiltKeyOfSecondChildBeforeLayout, key2); - // We don't want noise to override the result; - expect(rebuiltKeyOfThirdChildAfterLayout, isNull); - final _Stateful statefulWidget = element.widget as _Stateful; - rebuiltKeyOfThirdChildAfterLayout = statefulWidget.child.key; - }, - ), - ], - ); - }, - ) - ); - expect(rebuiltKeyOfSecondChildBeforeLayout, key2); - expect(rebuiltKeyOfSecondChildAfterLayout, key3); - expect(rebuiltKeyOfThirdChildAfterLayout, key2); - }); - - testWidgets('GlobalKey correct case 5 - can deal with early rebuild in layoutbuilder - only one global key', (WidgetTester tester) async { - const Key key1 = GlobalObjectKey('Text1'); - Key rebuiltKeyOfSecondChildBeforeLayout; - Key rebuiltKeyOfThirdChildAfterLayout; - await tester.pumpWidget( - LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - children: [ - const _Stateful( - child: Text( - 'Text1', - textDirection: TextDirection.ltr, - ), - ), - _Stateful( - child: const Text( - 'Text2', - textDirection: TextDirection.ltr, - key: key1, - ), - onElementRebuild: (StatefulElement element) { - // We don't want noise to override the result; - expect(rebuiltKeyOfSecondChildBeforeLayout, isNull); - final _Stateful statefulWidget = element.widget as _Stateful; - rebuiltKeyOfSecondChildBeforeLayout = statefulWidget.child.key; - }, - ), - const _Stateful( - child: Text( - 'Text3', - textDirection: TextDirection.ltr, - ), - ), - ], - ); - }, - ) - ); - // Result will be written during first build and need to clear it to remove - // noise. - rebuiltKeyOfSecondChildBeforeLayout = null; - - final _StatefulState state = tester.firstState(find.byType(_Stateful).at(1)); - state.rebuild(); - // Reorders the items - await tester.pumpWidget( - LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - children: [ - const _Stateful( - child: Text( - 'Text1', - textDirection: TextDirection.ltr, - ), - ), - _Stateful( - child: const Text( - 'Text3', - textDirection: TextDirection.ltr, - ), - onElementRebuild: (StatefulElement element) { - // Verifies the early rebuild happens before layout. - expect(rebuiltKeyOfSecondChildBeforeLayout, key1); - }, - ), - _Stateful( - child: const Text( - 'Text2', - textDirection: TextDirection.ltr, - key: key1, - ), - onElementRebuild: (StatefulElement element) { - // Verifies the early rebuild happens before layout. - expect(rebuiltKeyOfSecondChildBeforeLayout, key1); - // We don't want noise to override the result; - expect(rebuiltKeyOfThirdChildAfterLayout, isNull); - final _Stateful statefulWidget = element.widget as _Stateful; - rebuiltKeyOfThirdChildAfterLayout = statefulWidget.child.key; - }, - ), - ], - ); - }, - ) - ); - expect(rebuiltKeyOfSecondChildBeforeLayout, key1); - expect(rebuiltKeyOfThirdChildAfterLayout, key1); - }); - testWidgets('GlobalKey duplication 1 - double appearance', (WidgetTester tester) async { final Key key = GlobalKey(debugLabel: 'problematic'); await tester.pumpWidget(Stack( @@ -855,173 +501,7 @@ void main() { ], )); FlutterError.onError = oldHandler; - expect(count, 1); - }); - - testWidgets('GlobalKey duplication 18 - subtree build duplicate key with same type', (WidgetTester tester) async { - final Key key = GlobalKey(debugLabel: 'problematic'); - final Stack stack = Stack( - textDirection: TextDirection.ltr, - children: [ - const SwapKeyWidget(childKey: ValueKey(0)), - Container(key: const ValueKey(1)), - Container(key: key), - ], - ); - await tester.pumpWidget(stack); - final SwapKeyWidgetState state = tester.state(find.byType(SwapKeyWidget)); - state.swapKey(key); - await tester.pump(); - final dynamic exception = tester.takeException(); - expect(exception, isFlutterError); - expect( - exception.toString(), - equalsIgnoringHashCodes( - 'Duplicate GlobalKey detected in widget tree.\n' - 'The following GlobalKey was specified multiple times in the widget tree. This will lead ' - 'to parts of the widget tree being truncated unexpectedly, because the second time a key is seen, the ' - 'previous instance is moved to the new location. The key was:\n' - '- [GlobalKey#00000 problematic]\n' - 'This was determined by noticing that after the widget with the above global key was ' - 'moved out of its previous parent, that previous parent never updated during this frame, meaning that ' - 'it either did not update at all or updated before the widget was moved, in either case implying that ' - 'it still thinks that it should have a child with that global key.\n' - 'The specific parent that did not update after having one or more children forcibly ' - 'removed due to GlobalKey reparenting is:\n' - '- Stack(alignment: AlignmentDirectional.topStart, textDirection: ltr, fit: loose, ' - 'overflow: clip, renderObject: RenderStack#00000)\n' - 'A GlobalKey can only be specified on one widget at a time in the widget tree.' - ), - ); - }); - - testWidgets('GlobalKey duplication 19 - subtree build duplicate key with different types', (WidgetTester tester) async { - final Key key = GlobalKey(debugLabel: 'problematic'); - final Stack stack = Stack( - textDirection: TextDirection.ltr, - children: [ - const SwapKeyWidget(childKey: ValueKey(0)), - Container(key: const ValueKey(1)), - Container(child: SizedBox(key: key)), - ], - ); - await tester.pumpWidget(stack); - final SwapKeyWidgetState state = tester.state(find.byType(SwapKeyWidget)); - state.swapKey(key); - await tester.pump(); - final dynamic exception = tester.takeException(); - expect(exception, isFlutterError); - expect( - exception.toString(), - equalsIgnoringHashCodes( - 'Multiple widgets used the same GlobalKey.\n' - 'The key [GlobalKey#95367 problematic] was used by 2 widgets:\n' - ' SizedBox-[GlobalKey#00000 problematic]\n' - ' Container-[GlobalKey#00000 problematic]\n' - 'A GlobalKey can only be specified on one widget at a time in the widget tree.' - ), - ); - }); - - testWidgets('GlobalKey duplication 20 - real duplication with early rebuild in layoutbuilder will throw', (WidgetTester tester) async { - const Key key1 = GlobalObjectKey('Text1'); - const Key key2 = GlobalObjectKey('Text2'); - Key rebuiltKeyOfSecondChildBeforeLayout; - Key rebuiltKeyOfFirstChildAfterLayout; - Key rebuiltKeyOfSecondChildAfterLayout; - await tester.pumpWidget( - LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - children: [ - const _Stateful( - child: Text( - 'Text1', - textDirection: TextDirection.ltr, - key: key1, - ), - ), - _Stateful( - child: const Text( - 'Text2', - textDirection: TextDirection.ltr, - key: key2, - ), - onElementRebuild: (StatefulElement element) { - // We don't want noise to override the result; - expect(rebuiltKeyOfSecondChildBeforeLayout, isNull); - final _Stateful statefulWidget = element.widget as _Stateful; - rebuiltKeyOfSecondChildBeforeLayout = statefulWidget.child.key; - }, - ), - ], - ); - }, - ) - ); - // Result will be written during first build and need to clear it to remove - // noise. - rebuiltKeyOfSecondChildBeforeLayout = null; - - final _StatefulState state = tester.firstState(find.byType(_Stateful).at(1)); - state.rebuild(); - - await tester.pumpWidget( - LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - children: [ - _Stateful( - child: const Text( - 'Text2', - textDirection: TextDirection.ltr, - key: key2, - ), - onElementRebuild: (StatefulElement element) { - // Verifies the early rebuild happens before layout. - expect(rebuiltKeyOfSecondChildBeforeLayout, key2); - // We don't want noise to override the result; - expect(rebuiltKeyOfFirstChildAfterLayout, isNull); - final _Stateful statefulWidget = element.widget as _Stateful; - rebuiltKeyOfFirstChildAfterLayout = statefulWidget.child.key; - }, - ), - _Stateful( - child: const Text( - 'Text1', - textDirection: TextDirection.ltr, - key: key2, - ), - onElementRebuild: (StatefulElement element) { - // Verifies the early rebuild happens before layout. - expect(rebuiltKeyOfSecondChildBeforeLayout, key2); - // We don't want noise to override the result; - expect(rebuiltKeyOfSecondChildAfterLayout, isNull); - final _Stateful statefulWidget = element.widget as _Stateful; - rebuiltKeyOfSecondChildAfterLayout = statefulWidget.child.key; - }, - ), - ], - ); - }, - ) - ); - expect(rebuiltKeyOfSecondChildBeforeLayout, key2); - expect(rebuiltKeyOfFirstChildAfterLayout, key2); - expect(rebuiltKeyOfSecondChildAfterLayout, key2); - final dynamic exception = tester.takeException(); - expect(exception, isFlutterError); - expect( - exception.toString(), - equalsIgnoringHashCodes( - 'Multiple widgets used the same GlobalKey.\n' - 'The key [GlobalObjectKey String#00000] was used by multiple widgets. The ' - 'parents of those widgets were:\n' - '- _Stateful(state: _StatefulState#00000)\n' - '- _Stateful(state: _StatefulState#00000)\n' - 'A GlobalKey can only be specified on one widget at a time in the widget tree.' - ), - ); + expect(count, 2); }); testWidgets('GlobalKey - dettach and re-attach child to different parents', (WidgetTester tester) async { @@ -1278,6 +758,9 @@ class NullChildElement extends Element { visitor(null); } + @override + void forgetChild(Element child) { } + @override void performRebuild() { } } @@ -1289,6 +772,9 @@ class DirtyElementWithCustomBuildOwner extends Element { final BuildOwner _owner; + @override + void forgetChild(Element child) {} + @override void performRebuild() {} @@ -1337,66 +823,3 @@ class DependentState extends State { deactivatedCount += 1; } } - -class SwapKeyWidget extends StatefulWidget { - const SwapKeyWidget({this.childKey}): super(); - - final Key childKey; - @override - SwapKeyWidgetState createState() => SwapKeyWidgetState(); -} - -class SwapKeyWidgetState extends State { - Key key; - - @override - void initState() { - super.initState(); - key = widget.childKey; - } - - void swapKey(Key newKey) { - setState(() { - key = newKey; - }); - } - - @override - Widget build(BuildContext context) { - return Container(key: key); - } -} - -class _Stateful extends StatefulWidget { - const _Stateful({Key key, this.child, this.onElementRebuild}) : super(key: key); - final Text child; - final ElementRebuildCallback onElementRebuild; - @override - State createState() => _StatefulState(); - - @override - StatefulElement createElement() => StatefulElementSpy(this); -} - -class _StatefulState extends State<_Stateful> { - void rebuild() => setState(() {}); - - @override - Widget build(BuildContext context) { - return widget.child; - } -} - -class StatefulElementSpy extends StatefulElement { - StatefulElementSpy(StatefulWidget widget) : super(widget); - - _Stateful get _statefulWidget => widget as _Stateful; - - @override - void rebuild() { - if (_statefulWidget.onElementRebuild != null) { - _statefulWidget.onElementRebuild(this); - } - super.rebuild(); - } -}