Writing in the front
Key is an identifier for widgets, Elements, and SemanticsNodes. Key is an abstract class that comes in two types: LocalKey and GlobalKey.
Their more detailed classification is roughly as follows:
graph LR
A[Key] --> B(LocalKey)
A --> C(GlobalKey)
B-->D(ValueKey)
D-->E(PageStorageKey)
B-->F(ObjectKey)
B-->G(UniqueKey)
C-->H(LabeledGlobalKey)
C-->I(GlobalObjectKey)
content
Create a StatefulWidget for MyBox to demonstrate:
class MyBox extends StatefulWidget {
final Color color;
final Key key;
MyBox({this.color, this.key}) : super(key: key);
@override
_MyBoxState createState() => _MyBoxState();
}
class _MyBoxState extends State<MyBox> {
num number = 0;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
number++;
});
},
child: Container(
alignment: Alignment.center,
width: 60,
height: 60,
color: widget.color,
child: Text(
number.toString(),
style: TextStyle(fontSize: 20),),),); }}Copy the code
Then create three out and click change some data:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
returnMaterialApp( home: Scaffold( appBar: AppBar(), body: Center( child: Column( children: [ MyBox(color: Colors.yellow), MyBox(color: Colors.blue), MyBox(color: Colors.green) ], ), ), ), ); }}Copy the code
Now switch the first and second positions and hit Hot Reload. This will look like this:You can see that the colors have been switched, but the numbers haven’t changed.
There is a canUpdate() method in the Widget that determines whether to update the Widget
@immutable
abstract class Widget extends DiagnosticableTree {...static bool canUpdate(Widget oldWidget, Widget newWidget) {
returnoldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; }... }Copy the code
In our scenario, the Key condition can be ignored because there is no Key before and after the Flutter. Then, because of several myboxes, the Flutter can only be seen at that position of the Element Tree no matter how you change the position. Their runtimeType is the same before and after the Flutter. So for it, it’s really the same Widget, because we don’t give it a Key for further identification.
graph LR
A[MyBox] --> B(MyBoxElement)
C[MyBox] --> D(MyBoxElement)
E[MyBox] --> F(MyBoxElement)
In other words, if you switch the position of the first and the second, it is the same as if you do not change the position and then change their color values respectively.
Under HotReload, the State in the StatefulWidget is not re-created because it has already been created. Instead, it goes directly to the Build () method, and the number is initialized outside the build() method, so the number is still the same data. The color has changed because it was brought in from the outside, so the color has changed, but the number has not changed.
Of course, if MyBox were using a StatelessWidget, that would be what we’d expect, because there’s no such thing as state.
So, we add Key to each of these myBoxes to achieve the desired effect.
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold(appBar: appBar (), body: Center(Child: Column(Key: ValueKey(1)), children: [// Add key // MyBox(color: Colors.yellow, key: ValueKey(1)), // MyBox(color: Colors.blue, key: ValueKey(2)), // MyBox(color: Colors.green, key: MyBox(color: color.blue, key: ValueKey(2)), MyBox(color: color.yellow, key: ValueKey(1)), MyBox(color: Colors.green, key: ValueKey(3)) ], ), ), ), ); }}Copy the code
Now that you know what a Key does, let’s look at what each Key does.
LocalKey
LocalKey is relative to GlobalKey, which needs to be unique throughout the app, and LocalKey only needs to be unique within the Element of the same parent.
Because LocalKey is an abstract class, we’ll just use one of its implementation classes as an example, and everything else is the same.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
body: Column(
key: ValueKey(1),
children: [
Text("hello", key: ValueKey(1)),
Text("hello", key: ValueKey(2)),
Text("hello", key: ValueKey(3() [() [() [() [() }}Copy the code
There are three valueKeys in the children under Column, one of which is ValueKey(1), and their parent also has a ValueKey(1). This has no effect, because the uniqueness of LocalKey is only at its same level.
This is one of the reasons GlobalKey is a bit of a performance hog, because it needs to be compared to the entire app, whereas LocalKey is only at the same level.
ValueKey
For ValueKey, it compares whether the value we passed in is the same.
class ValueKey<T> extends LocalKey {
const ValueKey(this.value);
final T value;
@override
bool operator= = (Object other) {
if(other.runtimeType ! = runtimeType)return false;
return other isValueKey<T> && other.value == value; }...Copy the code
PageStorageKey
PageStorageKey is a special Key that can be used in situations where state is stored, but this is not meant to be used with PageStorage widgets, such as the PageStorageKey used in ListView, So the internal implementation will get it through PageStorage, and then use it as the Key, and the ListView scroll data as the value, and then bind them together to make it easy to recover the data later.
Read about this article about Flutter: What happens when PageStorageKey is used?
ObjectKey
With ObjectKey, we compare whether the objects we pass in are the same, that is, if the objects we pass in point to the same memory address, we consider them consistent.
class ObjectKey extends LocalKey {
const ObjectKey(this.value);
final Object? value;
@override
bool operator= = (Object other) {
if(other.runtimeType ! = runtimeType)return false;
return other isObjectKey && identical(other.value, value); }...Copy the code
Comparing objects for consistency in Dart is done using the identical() method
/// Check whether two references are to the same object.
external bool identical(Object? a, Object? b);
Copy the code
UniqueKey
UniqueKey has no comparison. It is unique as its name suggests. It can only be equal to itself.
class UniqueKey extends LocalKey {
UniqueKey();
@override
String toString() => '[#${shortHash(this)}] ';
}
Copy the code
GlobalKey
GlobalKey, as mentioned earlier, is used to identify uniqueness across the entire app. So you can’t have two widgets in a tree with the same GlobalKey.
@optionalTypeArgs
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
factory GlobalKey({ String? debugLabel }) => LabeledGlobalKey<T>(debugLabel);
const GlobalKey.constructor() : super.empty();
Element? get_currentElement => WidgetsBinding.instance! .buildOwner! ._globalKeyRegistry[this];
BuildContext? get currentContext => _currentElement;
Widget? getcurrentWidget => _currentElement? .widget; T?get currentState {
final Element? element = _currentElement;
if (element is StatefulElement) {
final StatefulElement statefulElement = element;
final State state = statefulElement.state;
if (state is T)
return state;
}
return null; }}Copy the code
GlobalKey is useful but should be used with caution.
Be useful
Through its implementation, we can see that if we use it to identify the StatefulWidget, then we can access its State and manipulate properties or methods in State, etc.
You can also get the Widget’s Context and the Widget held by Element to get more information. As we used to say:
final GlobalKey box1Key = GlobalKey();
RenderBox box = box1Key.currentContext.findRenderObject();
/ / size
Size size = box.size;
// Position on the screen
Offset offset = box.localToGlobal(Offset.zero);
Copy the code
If two pages overlap and we want the top page to call one of the GestureDetector methods on the bottom page, give the bottom GestureDetector a GlobalKey and the top page can operate on it as if it were empty:
GestureDetector gestureDetector = gestureKey.currentWidget;
gestureDetector.onTap();
Copy the code
No Context page is displayed
The GlobalKey can be used to retrieve the State of the Navigator, so you can jump to the page without Context.
class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { final GlobalKey<NavigatorState> navigatorKey = GlobalKey(); @override Widget build(BuildContext context) { return MaterialApp( navigatorKey: navigatorKey, home: Scaffold( appBar: AppBar(), body: Center( child: Column( children: [ TextButton( onPressed: () { navigatorKey.currentState.pushNamed("routeName"); }, child: Text("press")), ], ), ), ), ); }}Copy the code
careful
If a GlobalKey is recreated at each build, its global uniqueness means that it will discard the subtree state held by the old Key and create a new subtree for the new Key.
Performance losses are one thing, and sometimes there are unintended effects. For example, with GestureDetector, if it is given a new GlobalKey every time it builds, it might not be able to track the gesture being performed.
So it’s better to let State hold it and initialize it outside the build() method, for example in state.initState ().
About the only one in the app
GlobalKey is used as a unique identifier within the app scope. Is it true that a GlobalKey given to one Widget cannot be given to another?
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final GlobalKey boxGlobalKey = GlobalKey();
bool isChanged = false;
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
children: [
TextButton(
onPressed: () {
setState(() {
isChanged = !isChanged;
});
},
child: Text("press")), isChanged ? MyBox(color: Colors.red, key: boxGlobalKey) : MyBox(color: Colors.blue, key: boxGlobalKey) ], ), ), ), ); }}Copy the code
When we click the button, the switch works without an error, and the boxGlobalKey can be given to another Widget.
That is, it’s not unique throughout the life of the app, it’s unique within the same frame of the tree.
When we use GlobalKey, there is a mechanism to manage it.
BuildOwner’s _registerGlobalKey() method is called when the Widget’s Element is mounted to the tree by calling the mount method:
abstract class Element extends DiagnosticableTree implements BuildContext { ... void mount(Element? parent, Object? newSlot) { ... final Key? key = widget.key; if (key is GlobalKey) { owner! ._registerGlobalKey(key, this); }... }... }Copy the code
class BuildOwner { final Map<GlobalKey, Element> _globalKeyRegistry = <GlobalKey, Element>{}; void _registerGlobalKey(GlobalKey key, Element element) { ... _globalKeyRegistry[key] = element; }}Copy the code
The GlobalKey is added to _globalKeyRegistry as the Key and the current Element as the value.
On removal from the tree, Element’s unmount method is called and BuildOwner’s _unregisterGlobalKey() method is called for removal.
abstract class Element extends DiagnosticableTree implements BuildContext { ... @mustCallSuper void unmount() { ... final Key? key = _widget.key; if (key is GlobalKey) { owner! ._unregisterGlobalKey(key, this); }}... }Copy the code
class BuildOwner { final Map<GlobalKey, Element> _globalKeyRegistry = <GlobalKey, Element>{}; void _unregisterGlobalKey(GlobalKey key, Element element) { .... if (_globalKeyRegistry[key] == element) _globalKeyRegistry.remove(key); }}Copy the code
So where do you check? BuildOwner’s finalizeTree() method is called when the WidgetsBinding drawFrame method is called. In Debug mode, this method checks for duplicate GlobalKeys.
mixin WidgetsBinding{
@override
void drawFrame() {
try{... buildOwner! .finalizeTree(); }... }}Copy the code
class BuildOwner {
void finalizeTree() {
Timeline.startSync('Finalize tree', arguments: timelineArgumentsIndicatingLandmarkEvent);
try {
lockState(() {
_inactiveElements._unmountAll(); // this unregisters the GlobalKeys
});
assert(() {
try {
_debugVerifyGlobalKeyReservation();
_debugVerifyIllFatedPopulation();
if (_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans != null &&
_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans!.isNotEmpty) {
final Set<GlobalKey> keys = HashSet<GlobalKey>();
for (final Element element in _debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans!.keys) {
if (element._lifecycleState != _ElementLifecycle.defunct)
keys.addAll(_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans![element]!);
}
if (keys.isNotEmpty) {
final Map<String, int> keyStringCount = HashMap<String, int>();
for (final String key in keys.map<String>((GlobalKey key) => key.toString())) {
if (keyStringCount.containsKey(key)) {
keyStringCount.update(key, (int value) => value + 1);
} else {
keyStringCount[key] = 1;
}
}
final List<String> keyLabels = <String>[];
keyStringCount.forEach((String key, int count) {
if (count == 1) {
keyLabels.add(key);
} else {
keyLabels.add('$key ($count different affected keys had this toString representation)');
}
});
final Iterable<Element> elements = _debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans!.keys;
final Map<String, int> elementStringCount = HashMap<String, int>();
for (final String element in elements.map<String>((Element element) => element.toString())) {
if (elementStringCount.containsKey(element)) {
elementStringCount.update(element, (int value) => value + 1);
} else {
elementStringCount[element] = 1;
}
}
final List<String> elementLabels = <String>[];
elementStringCount.forEach((String element, int count) {
if (count == 1) {
elementLabels.add(element);
} else {
elementLabels.add('$element ($count different affected elements had this toString representation)');
}
});
assert(keyLabels.isNotEmpty);
final String the = keys.length == 1 ? ' the' : '';
final String s = keys.length == 1 ? '' : 's';
final String were = keys.length == 1 ? 'was' : 'were';
final String their = keys.length == 1 ? 'its' : 'their';
final String respective = elementLabels.length == 1 ? '' : ' respective';
final String those = keys.length == 1 ? 'that' : 'those';
final String s2 = elementLabels.length == 1 ? '' : 's';
final String those2 = elementLabels.length == 1 ? 'that' : 'those';
final String they = elementLabels.length == 1 ? 'it' : 'they';
final String think = elementLabels.length == 1 ? 'thinks' : 'think';
final String are = elementLabels.length == 1 ? 'is' : 'are';
// TODO(jacobr): make this error more structured to better expose which widgets had problems.
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Duplicate GlobalKey$s detected in widget tree.'),
// TODO(jacobr): refactor this code so the elements are clickable
// in GUI debug tools.
ErrorDescription(
'The following GlobalKey$s $were 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$s $were:\n'
'- ${keyLabels.join("\n ")}\n'
'This was determined by noticing that after$the widget$s with the above global key$s $were moved '
'out of $their$respective previous parent$s2, $those2 previous parent$s2 never updated during this frame, meaning '
'that $they either did not update at all or updated before the widget$s $were moved, in either case '
'implying that $they still $think that $they should have a child with $those global key$s.\n'
'The specific parent$s2 that did not update after having one or more children forcibly removed '
'due to GlobalKey reparenting $are:\n'
'- ${elementLabels.join("\n ")}'
'\nA GlobalKey can only be specified on one widget at a time in the widget tree.',
),
]);
}
}
} finally {
_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans?.clear();
}
return true;
}());
} catch (e, stack) {
// Catching the exception directly to avoid activating the ErrorWidget.
// Since the tree is in a broken state, adding the ErrorWidget would
// cause more exceptions.
_debugReportException(ErrorSummary('while finalizing the widget tree'), e, stack);
} finally {
Timeline.finishSync();
}
}
void _debugVerifyGlobalKeyReservation() {
assert(() {
final Map<GlobalKey, Element> keyToParent = <GlobalKey, Element>{};
_debugGlobalKeyReservations.forEach((Element parent, Map<Element, GlobalKey> childToKey) {
// We ignore parent that are unmounted or detached.
if (parent._lifecycleState == _ElementLifecycle.defunct || parent.renderObject?.attached == false)
return;
childToKey.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;
final 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;
}
});
});
_debugGlobalKeyReservations.clear();
return true;
}());
}
void _debugVerifyIllFatedPopulation() {
assert(() {
Map<GlobalKey, Set<Element>>? duplicates;
for (final Element element in _debugIllFatedElements) {
if (element._lifecycleState != _ElementLifecycle.defunct) {
assert(element != null);
assert(element.widget != null);
assert(element.widget.key != null);
final GlobalKey key = element.widget.key! as GlobalKey;
assert(_globalKeyRegistry.containsKey(key));
duplicates ??= <GlobalKey, Set<Element>>{};
// Uses ordered set to produce consistent error message.
final Set<Element> elements = duplicates.putIfAbsent(key, () => LinkedHashSet<Element>());
elements.add(element);
elements.add(_globalKeyRegistry[key]!);
}
}
_debugIllFatedElements.clear();
if (duplicates != null) {
final List<DiagnosticsNode> information = <DiagnosticsNode>[];
information.add(ErrorSummary('Multiple widgets used the same GlobalKey.'));
for (final GlobalKey key in duplicates.keys) {
final Set<Element> elements = duplicates[key]!;
// TODO(jacobr): this will omit the '- ' before each widget name and
// use the more standard whitespace style instead. Please let me know
// if the '- ' style is a feature we want to maintain and we can add
// another tree style that supports it. I also see '* ' in some places
// so it would be nice to unify and normalize.
information.add(Element.describeElements('The key $key was used by ${elements.length} widgets', elements));
}
information.add(ErrorDescription('A GlobalKey can only be specified on one widget at a time in the widget tree.'));
throw FlutterError.fromParts(information);
}
return true;
}());
}
}
Copy the code