Keys - wurzelsand/flutter-memos GitHub Wiki

Keys

Watch the video

  • «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 of children 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 a StatefulElement that is directly linked to the associated State object. So the State object is not directly connected to the StatefulWidget, but to the StatefulElement.
  • 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 and key properties of the original widgets with those of the current widgets. If they match, the elements just update their references. If an Element tries to update the reference to its widget and detects that the key property of the new widget is different from the old one, Flutter checks if the new widget Widget is part of a collection Iterable<Widget>. If it is, then it searches only in this collection for a matching widget. If the widget is not part of a collection and key is not a GlobalKey, Flutter will not search for a matching widget. But if a matching key was found, the Element (and its subtree if exists) changes its position to match the position of its corresponding widget again. If no matching key was found, a new corresponding element (plus a new State 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 the performRebuild method of the appropriate Element subclass. But if a widget has a key 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));

Stateless widgets without keys

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.

Stateless widgets with keys

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'.

Stateful widgets 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.

Stateful widgets with keys

@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:

Stateful widgets with padding

@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(),
    ),
  ];
}

Keys to completely rebuild State.

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 in build of _LoginTextState, because then we would not need the variable _loggedIn nor would we need to set the key property in LoginText. But by setting the key property we can force State to be completely rebuilt.
⚠️ **GitHub.com Fallback** ⚠️