Keys - wurzelsand/flutter-memos GitHub Wiki
-
«Generally, a widget that is the only child of another widget does not need an explicit key.» [Flutter documentation]
Keysmay be necessary for a collection ofchildrenof the same type. - On the way to program execution, Flutter creates an element tree that is finally rendered on the screen. In this context, the widget tree can be seen as a blueprint for the element tree. Each widget has the
createElementmethod, through which it creates elements for the element tree. Each element is linked by a reference to the widget it created. - A
StatefulWidgetcreates aStatefulElementthat is directly linked to the associatedStateobject. So theStateobject is not directly connected to theStatefulWidget, but to theStatefulElement. - When a widget tree is updated, Flutter compares the elements of the element tree with the corresponding widgets of the widget tree. The element tree still stores the references to the widgets as they were before the update. Flutter now compares the
runtimeTypeandkeyproperties of the original widgets with those of the current widgets. If they match, the elements just update their references. If anElementtries to update the reference to its widget and detects that thekeyproperty of the new widget is different from the old one, Flutter checks if the new widgetWidgetis part of a collectionIterable<Widget>. If it is, then it searches only in this collection for a matching widget. If the widget is not part of a collection andkeyis not aGlobalKey, Flutter will not search for a matching widget. But if a matchingkeywas found, theElement(and its subtree if exists) changes its position to match the position of its corresponding widget again. If no matchingkeywas found, a new corresponding element (plus a newStateif it is stateful) is created. - When widgets are compared to their corresponding elements to update their references (
Element -> Widget), the widgets are rebuilt each time, triggered by theperformRebuildmethod of the appropriateElementsubclass. But if a widget has akeyand it matches, only the reference to that widget is updated and the element changes its position if necessary, but the widget is not rebuilt. However, this is just an observation and generally you should assume that a widget could be rebuilt at any time.
I recreated the example shown in the Flutter team's video:

import 'package:flutter/material.dart';
import 'dart:math';
void main() => runApp(const MaterialApp(home: PositionedTiles()));
class PositionedTiles extends StatefulWidget {
const PositionedTiles({super.key});
@override
State<PositionedTiles> createState() => _PositionedTilesState();
}
class _PositionedTilesState extends State<PositionedTiles> {
late List<Widget> tiles;
@override
void initState() {
super.initState();
tiles = [
StatelessColorfulTile(),
StatelessColorfulTile(),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(child: Row(children: tiles)),
floatingActionButton: FloatingActionButton(
onPressed: swapTiles,
child: const Icon(Icons.sentiment_very_satisfied),
),
);
}
void swapTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
}
}
class StatelessColorfulTile extends StatelessWidget {
StatelessColorfulTile({super.key});
final Color myColor = getRandomColor();
@override
Widget build(BuildContext context) {
return Container(
color: myColor,
child: const Padding(padding: EdgeInsets.all(70)),
);
}
}
class StatefulColorfulTile extends StatefulWidget {
const StatefulColorfulTile({super.key});
@override
State<StatefulColorfulTile> createState() => _StatefulColorfulTileState();
}
class _StatefulColorfulTileState extends State<StatefulColorfulTile> {
final myColor = getRandomColor();
@override
Widget build(BuildContext context) {
return Container(
color: myColor,
child: const Padding(padding: EdgeInsets.all(70)),
);
}
}
Color getRandomColor() => Color(0xFF000000 | Random().nextInt(0xFFFFFF));Assuming the first two colors generated are red and blue, the Row widget is composed something like this:
After swapping the tiles:
The elements from the element tree are compared with the widgets from the widget tree. They match in type. Therefore, the references are simply updated so that the first element no longer references the red widget but the blue one.
Now we explicitly set the key property of the two StatelessColorfulTile objects:
@override
void initState() {
super.initState();
tiles = [
StatelessColorfulTile(key: UniqueKey()),
StatelessColorfulTile(key: UniqueKey()),
];
}
This time the key properties of the two StatelessColorfulTile objects do not match their corresponding elements. Therefore, Flutter searches within the StatelessElement collection for matching elements, finds them and reorders the elements:
The result is the same: The first element displays the blue widget, the second the red one. The additional swapping of the elements was not necessary, but at least the widgets possibly do not have to be rebuilt, as they do without 'keys'.
@override
void initState() {
super.initState();
tiles = [
StatefulColorfulTile(),
StatefulColorfulTile(),
];
}Now when we press the floatingActionButton, nothing happens: The left square remains red, the right one remains blue:

The reason we see no change is that the myColor property is no longer stored in the widget, but in the State object, which is directly connected to the StatefulElement. And when comparing the StatefulElements with their corresponding Widgets no differences are found in runtimeType and key. So the references are only updated and the first element in the collection remains the StatefulElement with the State myColor = red.
@override
void initState() {
super.initState();
tiles = [
StatefulColorfulTile(key: UniqueKey()),
StatefulColorfulTile(key: UniqueKey()),
];
}This time everything works: With every button press the two squares swap their places.
The two keys no longer match. So the two StatefulElements swap their order so that the keys match again:
@override
void initState() {
super.initState();
tiles = [
Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(key: UniqueKey()),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(key: UniqueKey()),
),
];
}
UniqueKey is a LocalKey. Once the two Padding widgets and their StatefulColorfulTiles have been swapped, the two StatefulElements recognize that the keys of their corresponding StatefulColorfulTiles have changed. But each of the two StatefulColorfulTiles is the only child of its parent (Padding). So on the same level there are no other children to search for. Therefore, completely new StatefulElements along with new States are created.
A solution, yet not performant, would be to replace the LocalKeys with GlobalKeys:
@override
void initState() {
super.initState();
tiles = [
Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(key: GlobalKey()),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(key: GlobalKey()),
),
];
}«Reparenting an Element using a global key is relatively expensive, as this operation will trigger a call to State.deactivate on the associated State and all of its descendants; then force all widgets that depends on an InheritedWidget to rebuild.» [Flutter documentation]
Therefore:
@override
void initState() {
super.initState();
tiles = [
Padding(
key: UniqueKey(),
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(),
),
Padding(
key: UniqueKey(),
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(),
),
];
}

Variables of State objects are sometimes initialized in initState. When a StatefulWidget is updated, createState (and therefore initState) is not called each time. It will only be called again if its key has changed in the meantime. Therefore key is required in the following example, otherwise the text of LoginText would not change:
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: MyApp()));
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool loggedIn = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
onPressed: () => setState(() => loggedIn = !loggedIn),
child: Text(loggedIn ? 'log out' : 'log in'),
),
LoginText(
// required >>>
key: ValueKey(loggedIn),
// <<< required
loggedIn: loggedIn,
),
],
);
}
}
class LoginText extends StatefulWidget {
const LoginText({super.key, required this.loggedIn});
final bool loggedIn;
@override
State<LoginText> createState() => _LoginTextState();
}
class _LoginTextState extends State<LoginText> {
late bool _loggedIn;
@override
void initState() {
super.initState();
_loggedIn = widget.loggedIn;
}
@override
Widget build(BuildContext context) {
return Text(_loggedIn ? 'logged in' : 'logged out');
}
}- One could have simply called
widget.loggedIninbuildof_LoginTextState, because then we would not need the variable_loggedInnor would we need to set thekeyproperty inLoginText. But by setting thekeyproperty we can forceStateto be completely rebuilt.
