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]
Keys
may be necessary for a collection ofchildren
of 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
createElement
method, through which it creates elements for the element tree. Each element is linked by a reference to the widget it created. - A
StatefulWidget
creates aStatefulElement
that is directly linked to the associatedState
object. So theState
object 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
runtimeType
andkey
properties of the original widgets with those of the current widgets. If they match, the elements just update their references. If anElement
tries to update the reference to its widget and detects that thekey
property of the new widget is different from the old one, Flutter checks if the new widgetWidget
is 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 andkey
is not aGlobalKey
, Flutter will not search for a matching widget. But if a matchingkey
was found, theElement
(and its subtree if exists) changes its position to match the position of its corresponding widget again. If no matchingkey
was found, a new corresponding element (plus a newState
if 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 theperformRebuild
method of the appropriateElement
subclass. But if a widget has akey
and 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.loggedIn
inbuild
of_LoginTextState
, because then we would not need the variable_loggedIn
nor would we need to set thekey
property inLoginText
. But by setting thekey
property we can forceState
to be completely rebuilt.