This reverts commit 340b193f20.
This commit is contained in:
@@ -469,7 +469,6 @@ class _CupertinoAlertRenderElement extends RenderObjectElement {
|
||||
} else if (_actionsElement == child) {
|
||||
_actionsElement = null;
|
||||
}
|
||||
super.forgetChild(child);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -464,7 +464,6 @@ class _CupertinoDialogRenderElement extends RenderObjectElement {
|
||||
assert(_actionsElement == child);
|
||||
_actionsElement = null;
|
||||
}
|
||||
super.forgetChild(child);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1016,7 +1016,6 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RootRenderObje
|
||||
void forgetChild(Element child) {
|
||||
assert(child == _child);
|
||||
_child = null;
|
||||
super.forgetChild(child);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -132,20 +132,7 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
|
||||
|
||||
static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
|
||||
static final Set<Element> _debugIllFatedElements = HashSet<Element>();
|
||||
// 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<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;
|
||||
}());
|
||||
}
|
||||
static final Map<GlobalKey, Element> _debugReservations = <GlobalKey, Element>{};
|
||||
|
||||
void _register(Element element) {
|
||||
assert(() {
|
||||
@@ -173,83 +160,46 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
|
||||
_registry.remove(this);
|
||||
}
|
||||
|
||||
void _debugReserveFor(Element parent, Element child) {
|
||||
void _debugReserveFor(Element parent) {
|
||||
assert(() {
|
||||
assert(parent != null);
|
||||
assert(child != null);
|
||||
_debugReservations[parent] ??= <Element, GlobalKey>{};
|
||||
_debugReservations[parent][child] = this;
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
|
||||
static void _debugVerifyGlobalKeyReservation() {
|
||||
assert(() {
|
||||
final Map<GlobalKey, Element> keyToParent = <GlobalKey, Element>{};
|
||||
_debugReservations.forEach((Element parent, Map<Element, GlobalKey> 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(<DiagnosticsNode>[
|
||||
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(<DiagnosticsNode>[
|
||||
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(<DiagnosticsNode>[
|
||||
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(<DiagnosticsNode>[
|
||||
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<T extends State<StatefulWidget>> extends Key {
|
||||
final GlobalKey key = element.widget.key as GlobalKey;
|
||||
assert(_registry.containsKey(key));
|
||||
duplicates ??= <GlobalKey, Set<Element>>{};
|
||||
// Uses ordered set to produce consistent error message.
|
||||
final Set<Element> elements = duplicates.putIfAbsent(key, () => LinkedHashSet<Element>());
|
||||
final Set<Element> elements = duplicates.putIfAbsent(key, () => HashSet<Element>());
|
||||
elements.add(element);
|
||||
elements.add(_registry[key]);
|
||||
}
|
||||
}
|
||||
_debugIllFatedElements.clear();
|
||||
_debugReservations.clear();
|
||||
if (duplicates != null) {
|
||||
final List<DiagnosticsNode> information = <DiagnosticsNode>[];
|
||||
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<Element> _debugForgottenChildrenWithGlobalKey = HashSet<Element>();
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -60,7 +60,6 @@ class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderOb
|
||||
void forgetChild(Element child) {
|
||||
assert(child == _child);
|
||||
_child = null;
|
||||
super.forgetChild(child);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -924,7 +924,6 @@ class ListWheelElement extends RenderObjectElement implements ListWheelChildMana
|
||||
@override
|
||||
void forgetChild(Element child) {
|
||||
_childElements.remove(child.slot);
|
||||
super.forgetChild(child);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -230,7 +230,6 @@ class _SliverPersistentHeaderElement extends RenderObjectElement {
|
||||
void forgetChild(Element child) {
|
||||
assert(child == this.child);
|
||||
this.child = null;
|
||||
super.forgetChild(child);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -344,7 +344,6 @@ class _TableElement extends RenderObjectElement {
|
||||
@override
|
||||
bool forgetChild(Element child) {
|
||||
_forgottenChildren.add(child);
|
||||
super.forgetChild(child);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<StatefulWidget> {
|
||||
@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: <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 {
|
||||
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: <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.'
|
||||
),
|
||||
);
|
||||
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<DependentStatefulWidget> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user