Unverified Commit d2b66dbf authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Revert "Refactors global key duplication detection (#46183)" (#49847)

This reverts commit 340b193f.
parent 340b193f
...@@ -469,7 +469,6 @@ class _CupertinoAlertRenderElement extends RenderObjectElement { ...@@ -469,7 +469,6 @@ class _CupertinoAlertRenderElement extends RenderObjectElement {
} else if (_actionsElement == child) { } else if (_actionsElement == child) {
_actionsElement = null; _actionsElement = null;
} }
super.forgetChild(child);
} }
@override @override
......
...@@ -464,7 +464,6 @@ class _CupertinoDialogRenderElement extends RenderObjectElement { ...@@ -464,7 +464,6 @@ class _CupertinoDialogRenderElement extends RenderObjectElement {
assert(_actionsElement == child); assert(_actionsElement == child);
_actionsElement = null; _actionsElement = null;
} }
super.forgetChild(child);
} }
@override @override
......
...@@ -2072,7 +2072,6 @@ class _RenderChipElement extends RenderObjectElement { ...@@ -2072,7 +2072,6 @@ class _RenderChipElement extends RenderObjectElement {
final _ChipSlot slot = childToSlot[child]; final _ChipSlot slot = childToSlot[child];
childToSlot.remove(child); childToSlot.remove(child);
slotToChild.remove(slot); slotToChild.remove(slot);
super.forgetChild(child);
} }
void _mountChild(Widget widget, _ChipSlot slot) { void _mountChild(Widget widget, _ChipSlot slot) {
......
...@@ -1511,7 +1511,6 @@ class _RenderDecorationElement extends RenderObjectElement { ...@@ -1511,7 +1511,6 @@ class _RenderDecorationElement extends RenderObjectElement {
final _DecorationSlot slot = childToSlot[child]; final _DecorationSlot slot = childToSlot[child];
childToSlot.remove(child); childToSlot.remove(child);
slotToChild.remove(slot); slotToChild.remove(slot);
super.forgetChild(child);
} }
void _mountChild(Widget widget, _DecorationSlot slot) { void _mountChild(Widget widget, _DecorationSlot slot) {
......
...@@ -992,7 +992,6 @@ class _ListTileElement extends RenderObjectElement { ...@@ -992,7 +992,6 @@ class _ListTileElement extends RenderObjectElement {
final _ListTileSlot slot = childToSlot[child]; final _ListTileSlot slot = childToSlot[child];
childToSlot.remove(child); childToSlot.remove(child);
slotToChild.remove(slot); slotToChild.remove(slot);
super.forgetChild(child);
} }
void _mountChild(Widget widget, _ListTileSlot slot) { void _mountChild(Widget widget, _ListTileSlot slot) {
......
...@@ -1016,7 +1016,6 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RootRenderObje ...@@ -1016,7 +1016,6 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RootRenderObje
void forgetChild(Element child) { void forgetChild(Element child) {
assert(child == _child); assert(child == _child);
_child = null; _child = null;
super.forgetChild(child);
} }
@override @override
......
...@@ -132,20 +132,7 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key { ...@@ -132,20 +132,7 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{}; static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
static final Set<Element> _debugIllFatedElements = HashSet<Element>(); static final Set<Element> _debugIllFatedElements = HashSet<Element>();
// This map keeps track which child reserve the global key with the parent. static final Map<GlobalKey, Element> _debugReservations = <GlobalKey, Element>{};
// 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<Element, Map<Element, GlobalKey>> _debugReservations = <Element, Map<Element, GlobalKey>>{};
static void _debugRemoveReservationFor(Element parent, Element child) {
assert(() {
assert(parent != null);
assert(child != null);
_debugReservations[parent]?.remove(child);
return true;
}());
}
void _register(Element element) { void _register(Element element) {
assert(() { assert(() {
...@@ -173,83 +160,46 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key { ...@@ -173,83 +160,46 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
_registry.remove(this); _registry.remove(this);
} }
void _debugReserveFor(Element parent, Element child) { void _debugReserveFor(Element parent) {
assert(() { assert(() {
assert(parent != null); assert(parent != null);
assert(child != null); if (_debugReservations.containsKey(this) && _debugReservations[this] != parent) {
_debugReservations[parent] ??= <Element, GlobalKey>{}; // Reserving a new parent while the old parent is not attached is ok.
_debugReservations[parent][child] = this; // 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; return true;
}());
} }
// It's possible for an element to get built multiple times in one
static void _debugVerifyGlobalKeyReservation() { // frame, in which case it'll reserve the same child's key multiple
assert(() { // times. We catch multiple children of one widget having the same key
final Map<GlobalKey, Element> keyToParent = <GlobalKey, Element>{}; // by verifying that an element never steals elements from itself, so we
_debugReservations.forEach((Element parent, Map<Element, GlobalKey> chidToKey) { // don't care to verify that here as well.
// We ignore parent that are detached. final String older = _debugReservations[this].toString();
if (parent.renderObject?.attached == false) final String newer = parent.toString();
return; if (older != newer) {
chidToKey.forEach((Element child, GlobalKey key) { throw FlutterError.fromParts(<DiagnosticsNode>[
// 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(<DiagnosticsNode>[
ErrorSummary('Multiple widgets used the same GlobalKey.'), ErrorSummary('Multiple widgets used the same GlobalKey.'),
ErrorDescription( ErrorDescription(
'The key $key was used by multiple widgets. The parents of those widgets were:\n' 'The key $this was used by multiple widgets. The parents of those widgets were:\n'
'- ${older.toString()}\n' '- $older\n'
'- ${newer.toString()}\n' '- $newer\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.' 'A GlobalKey can only be specified on one widget at a time in the widget tree.'
), ),
]); ]);
} else { }
error = FlutterError.fromParts(<DiagnosticsNode>[ throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Multiple widgets used the same GlobalKey.'), ErrorSummary('Multiple widgets used the same GlobalKey.'),
ErrorDescription( ErrorDescription(
'The key $key was used by multiple widgets. The parents of those widgets were ' 'The key $this was used by multiple widgets. The parents of those widgets were '
'different widgets that both had the following description:\n' 'different widgets that both had the following description:\n'
' ${parent.toString()}\n' ' $parent\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.' '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 _debugReservations[this] = parent;
// 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();
return true; return true;
}()); }());
} }
...@@ -265,13 +215,13 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key { ...@@ -265,13 +215,13 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
final GlobalKey key = element.widget.key as GlobalKey; final GlobalKey key = element.widget.key as GlobalKey;
assert(_registry.containsKey(key)); assert(_registry.containsKey(key));
duplicates ??= <GlobalKey, Set<Element>>{}; duplicates ??= <GlobalKey, Set<Element>>{};
// Uses ordered set to produce consistent error message. final Set<Element> elements = duplicates.putIfAbsent(key, () => HashSet<Element>());
final Set<Element> elements = duplicates.putIfAbsent(key, () => LinkedHashSet<Element>());
elements.add(element); elements.add(element);
elements.add(_registry[key]); elements.add(_registry[key]);
} }
} }
_debugIllFatedElements.clear(); _debugIllFatedElements.clear();
_debugReservations.clear();
if (duplicates != null) { if (duplicates != null) {
final List<DiagnosticsNode> information = <DiagnosticsNode>[]; final List<DiagnosticsNode> information = <DiagnosticsNode>[];
information.add(ErrorSummary('Multiple widgets used the same GlobalKey.')); information.add(ErrorSummary('Multiple widgets used the same GlobalKey.'));
...@@ -2689,7 +2639,6 @@ class BuildOwner { ...@@ -2689,7 +2639,6 @@ class BuildOwner {
}); });
assert(() { assert(() {
try { try {
GlobalKey._debugVerifyGlobalKeyReservation();
GlobalKey._debugVerifyIllFatedPopulation(); GlobalKey._debugVerifyIllFatedPopulation();
if (_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans != null && if (_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans != null &&
_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans.isNotEmpty) { _debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans.isNotEmpty) {
...@@ -3144,12 +3093,18 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -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]. | /// | **child != null** | Old child is removed, returns null. | Old child updated if possible, returns child or new [Element]. |
@protected @protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) { 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 (newWidget == null) {
if (child != null) if (child != null)
deactivateChild(child); deactivateChild(child);
return null; return null;
} }
Element newChild;
if (child != null) { if (child != null) {
bool hasSameSuperclass = true; bool hasSameSuperclass = true;
// When the type of a widget is changed between Stateful and Stateless via // When the type of a widget is changed between Stateful and Stateless via
...@@ -3173,11 +3128,13 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -3173,11 +3128,13 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
hasSameSuperclass = oldElementClass == newWidgetClass; hasSameSuperclass = oldElementClass == newWidgetClass;
return true; return true;
}()); }());
if (child.widget == newWidget && hasSameSuperclass) { if (hasSameSuperclass) {
if (child.widget == newWidget) {
if (child.slot != newSlot) if (child.slot != newSlot)
updateSlotForChild(child, newSlot); updateSlotForChild(child, newSlot);
newChild = child; return child;
} else if (Widget.canUpdate(child.widget, newWidget) && hasSameSuperclass) { }
if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot) if (child.slot != newSlot)
updateSlotForChild(child, newSlot); updateSlotForChild(child, newSlot);
child.update(newWidget); child.update(newWidget);
...@@ -3186,27 +3143,13 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -3186,27 +3143,13 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
child.owner._debugElementWasRebuilt(child); child.owner._debugElementWasRebuilt(child);
return true; return true;
}()); }());
newChild = child; return child;
} else {
deactivateChild(child);
assert(child._parent == null);
newChild = inflateWidget(newWidget, newSlot);
} }
} else {
newChild = inflateWidget(newWidget, newSlot);
} }
deactivateChild(child);
assert(() { assert(child._parent == null);
if (child != null)
_debugRemoveGlobalKeyReservation(child);
final Key key = newWidget?.key;
if (key is GlobalKey) {
key._debugReserveFor(this, newChild);
} }
return true; return inflateWidget(newWidget, newSlot);
}());
return newChild;
} }
/// Add this element to the tree in the given slot of the given parent. /// 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 { ...@@ -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. /// Change the widget used to configure this element.
/// ///
/// The framework calls this function when the parent wishes to use a /// The framework calls this function when the parent wishes to use a
...@@ -3265,16 +3205,6 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -3265,16 +3205,6 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
&& depth != null && depth != null
&& _active && _active
&& Widget.canUpdate(widget, newWidget)); && 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; _widget = newWidget;
} }
...@@ -3471,10 +3401,6 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -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<Element> _debugForgottenChildrenWithGlobalKey = HashSet<Element>();
/// Remove the given child from the element's child list, in preparation for /// Remove the given child from the element's child list, in preparation for
/// the child being reused elsewhere in the element tree. /// the child being reused elsewhere in the element tree.
/// ///
...@@ -3483,23 +3409,12 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -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 /// 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. /// 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 @protected
@mustCallSuper
void forgetChild(Element child) { void forgetChild(Element child) {
// This method is called on the old parent when the given child (with a // TODO(chunhtai): Creates empty body for subclass to call super. This will
// global key) is given a new parent. We cannot remove the global key // enable us to fix internal tests pro-actively for upcoming breaking
// reservation directly in this method because the forgotten child is not // change.
// removed from the tree until this Element is updated in [update]. If // https://github.com/flutter/flutter/issues/43780.
// [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;
}());
} }
void _activateWithParent(Element parent, dynamic newSlot) { void _activateWithParent(Element parent, dynamic newSlot) {
...@@ -4529,7 +4444,6 @@ abstract class ComponentElement extends Element { ...@@ -4529,7 +4444,6 @@ abstract class ComponentElement extends Element {
void forgetChild(Element child) { void forgetChild(Element child) {
assert(child == _child); assert(child == _child);
_child = null; _child = null;
super.forgetChild(child);
} }
} }
...@@ -5683,7 +5597,6 @@ class LeafRenderObjectElement extends RenderObjectElement { ...@@ -5683,7 +5597,6 @@ class LeafRenderObjectElement extends RenderObjectElement {
@override @override
void forgetChild(Element child) { void forgetChild(Element child) {
assert(false); assert(false);
super.forgetChild(child);
} }
@override @override
...@@ -5733,7 +5646,6 @@ class SingleChildRenderObjectElement extends RenderObjectElement { ...@@ -5733,7 +5646,6 @@ class SingleChildRenderObjectElement extends RenderObjectElement {
void forgetChild(Element child) { void forgetChild(Element child) {
assert(child == _child); assert(child == _child);
_child = null; _child = null;
super.forgetChild(child);
} }
@override @override
...@@ -5840,7 +5752,6 @@ class MultiChildRenderObjectElement extends RenderObjectElement { ...@@ -5840,7 +5752,6 @@ class MultiChildRenderObjectElement extends RenderObjectElement {
assert(_children.contains(child)); assert(_children.contains(child));
assert(!_forgottenChildren.contains(child)); assert(!_forgottenChildren.contains(child));
_forgottenChildren.add(child); _forgottenChildren.add(child);
super.forgetChild(child);
} }
@override @override
......
...@@ -60,7 +60,6 @@ class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderOb ...@@ -60,7 +60,6 @@ class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderOb
void forgetChild(Element child) { void forgetChild(Element child) {
assert(child == _child); assert(child == _child);
_child = null; _child = null;
super.forgetChild(child);
} }
@override @override
......
...@@ -924,7 +924,6 @@ class ListWheelElement extends RenderObjectElement implements ListWheelChildMana ...@@ -924,7 +924,6 @@ class ListWheelElement extends RenderObjectElement implements ListWheelChildMana
@override @override
void forgetChild(Element child) { void forgetChild(Element child) {
_childElements.remove(child.slot); _childElements.remove(child.slot);
super.forgetChild(child);
} }
} }
......
...@@ -1160,7 +1160,6 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render ...@@ -1160,7 +1160,6 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render
assert(child.slot != null); assert(child.slot != null);
assert(_childElements.containsKey(child.slot)); assert(_childElements.containsKey(child.slot));
_childElements.remove(child.slot); _childElements.remove(child.slot);
super.forgetChild(child);
} }
@override @override
......
...@@ -230,7 +230,6 @@ class _SliverPersistentHeaderElement extends RenderObjectElement { ...@@ -230,7 +230,6 @@ class _SliverPersistentHeaderElement extends RenderObjectElement {
void forgetChild(Element child) { void forgetChild(Element child) {
assert(child == this.child); assert(child == this.child);
this.child = null; this.child = null;
super.forgetChild(child);
} }
@override @override
......
...@@ -344,7 +344,6 @@ class _TableElement extends RenderObjectElement { ...@@ -344,7 +344,6 @@ class _TableElement extends RenderObjectElement {
@override @override
bool forgetChild(Element child) { bool forgetChild(Element child) {
_forgottenChildren.add(child); _forgottenChildren.add(child);
super.forgetChild(child);
return true; return true;
} }
} }
......
...@@ -221,6 +221,11 @@ void main() { ...@@ -221,6 +221,11 @@ void main() {
class _TestElement extends Element { class _TestElement extends Element {
_TestElement() : super(const Placeholder()); _TestElement() : super(const Placeholder());
@override
void forgetChild(Element child) {
// Intentionally left empty.
}
@override @override
void performRebuild() { void performRebuild() {
// Intentionally left empty. // Intentionally left empty.
......
...@@ -6,8 +6,6 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -6,8 +6,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
typedef ElementRebuildCallback = void Function(StatefulElement element);
class TestState extends State<StatefulWidget> { class TestState extends State<StatefulWidget> {
@override @override
Widget build(BuildContext context) => null; Widget build(BuildContext context) => null;
...@@ -63,358 +61,6 @@ void main() { ...@@ -63,358 +61,6 @@ void main() {
expect(keyA, isNot(equals(keyB))); 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: <Widget>[
Container(
key: const ValueKey<int>(1),
child: SizedBox(key: key),
),
LayoutBuilder(
key: const ValueKey<int>(2),
builder: (BuildContext context, BoxConstraints constraints) {
return const Placeholder();
},
),
],
));
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(
key: const ValueKey<int>(1),
child: const Placeholder(),
),
LayoutBuilder(
key: const ValueKey<int>(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: <Widget>[
Container(
key: const ValueKey<int>(1),
child: const Placeholder(),
),
LayoutBuilder(
key: const ValueKey<int>(2),
builder: (BuildContext context, BoxConstraints constraints) {
return SizedBox(key: key);
},
),
],
));
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(
key: const ValueKey<int>(1),
child: SizedBox(key: key),
),
LayoutBuilder(
key: const ValueKey<int>(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: <Widget>[
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: <Widget>[
_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: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
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 { testWidgets('GlobalKey duplication 1 - double appearance', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'problematic'); final Key key = GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(Stack( await tester.pumpWidget(Stack(
...@@ -855,173 +501,7 @@ void main() { ...@@ -855,173 +501,7 @@ void main() {
], ],
)); ));
FlutterError.onError = oldHandler; FlutterError.onError = oldHandler;
expect(count, 1); expect(count, 2);
});
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: <Widget>[
const SwapKeyWidget(childKey: ValueKey<int>(0)),
Container(key: const ValueKey<int>(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: <Widget>[
const SwapKeyWidget(childKey: ValueKey<int>(0)),
Container(key: const ValueKey<int>(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: <Widget>[
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: <Widget>[
_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.'
),
);
}); });
testWidgets('GlobalKey - dettach and re-attach child to different parents', (WidgetTester tester) async { testWidgets('GlobalKey - dettach and re-attach child to different parents', (WidgetTester tester) async {
...@@ -1278,6 +758,9 @@ class NullChildElement extends Element { ...@@ -1278,6 +758,9 @@ class NullChildElement extends Element {
visitor(null); visitor(null);
} }
@override
void forgetChild(Element child) { }
@override @override
void performRebuild() { } void performRebuild() { }
} }
...@@ -1289,6 +772,9 @@ class DirtyElementWithCustomBuildOwner extends Element { ...@@ -1289,6 +772,9 @@ class DirtyElementWithCustomBuildOwner extends Element {
final BuildOwner _owner; final BuildOwner _owner;
@override
void forgetChild(Element child) {}
@override @override
void performRebuild() {} void performRebuild() {}
...@@ -1337,66 +823,3 @@ class DependentState extends State<DependentStatefulWidget> { ...@@ -1337,66 +823,3 @@ class DependentState extends State<DependentStatefulWidget> {
deactivatedCount += 1; deactivatedCount += 1;
} }
} }
class SwapKeyWidget extends StatefulWidget {
const SwapKeyWidget({this.childKey}): super();
final Key childKey;
@override
SwapKeyWidgetState createState() => SwapKeyWidgetState();
}
class SwapKeyWidgetState extends State<SwapKeyWidget> {
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<StatefulWidget> 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();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment