Flutter Lesson 66 of 83 4 min read
Keys in Flutter: ValueKey, UniqueKey and GlobalKey
Understand what keys do in Flutter, when you need ValueKey, ObjectKey, UniqueKey or GlobalKey, and how they fix list state bugs.
On this page
Every widget constructor has a key parameter, and most of the time you ignore it. Keys matter in one situation: when Flutter must tell apart widgets of the same type at the same level, usually items in a list.
How Flutter matches widgets #
When a widget rebuilds, Flutter compares the new widgets with the old ones to decide what to keep. Without keys it matches by position and type: “the first child was a TodoTile and still is, so reuse it”.
That is fine for stateless widgets, since they are fully described by their parameters. For stateful widgets, the State object is kept at its position. If the items move, the state stays where it was and ends up attached to the wrong item.
The bug #
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: TodoPage()));
class TodoPage extends StatefulWidget {
const TodoPage({super.key});
@override
State<TodoPage> createState() => _TodoPageState();
}
class _TodoPageState extends State<TodoPage> {
final _todos = ['Buy milk', 'Call Hari', 'Pay rent'];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Tick one, then delete the first')),
body: ListView(
children: [
for (final todo in _todos)
TodoTile(
// key: ValueKey(todo), // uncomment to fix the bug
title: todo,
onDelete: () => setState(() => _todos.remove(todo)),
),
],
),
);
}
}
class TodoTile extends StatefulWidget {
const TodoTile({super.key, required this.title, required this.onDelete});
final String title;
final VoidCallback onDelete;
@override
State<TodoTile> createState() => _TodoTileState();
}
class _TodoTileState extends State<TodoTile> {
bool _done = false; // state kept inside the tile
@override
Widget build(BuildContext context) {
return CheckboxListTile(
title: Text(widget.title),
value: _done,
onChanged: (v) => setState(() => _done = v ?? false),
secondary: IconButton(icon: const Icon(Icons.delete), onPressed: widget.onDelete),
);
}
}
Tick “Buy milk”, then delete it. The tick jumps to “Call Hari”. Flutter saw “the first tile is still a TodoTile” and kept its state, with the new title.
Uncomment the key line and the bug is gone. With a key, Flutter matches by key instead of position, so each state follows its own item.
The kinds of key #
| Key | Identity comes from | Use for |
|---|---|---|
ValueKey(value) | A value such as an id or a string | List items with a unique id. The usual choice |
ObjectKey(object) | The object’s identity | Items that are distinct objects with no single id field |
UniqueKey() | Itself. Never equal to any other key | Forcing a widget to be recreated |
GlobalKey() | Unique across the whole app | Reaching a widget’s state from outside |
PageStorageKey(value) | A value | Remembering scroll position across tab switches |
ListView(
children: [
for (final product in products)
ProductTile(key: ValueKey(product.id), product: product),
],
)
When you need a key #
- A list of stateful widgets that can be reordered, inserted into or removed from.
DismissibleandReorderableListViewrequire one on each item.AnimatedSwitcher: the key tells it the child has changed.AnimatedListand other widgets that animate individual items.
When you do not #
- Stateless children.
- A list that only grows at the end and never reorders.
- Widgets that are not siblings of the same type.
Rules #
- Keys must be unique among siblings. Two children with the same key cause an error.
- Do not use the index as a key. An index is the position, which is what Flutter uses anyway, so it fixes nothing.
- Do not use a random value or
UniqueKey()created insidebuildfor list items. A new key on every build throws the state away each time. - Put the key on the outermost widget of the item, the one that is the direct child of the list.
Forcing a rebuild from scratch #
Occasionally you want to discard state on purpose, such as resetting a form or restarting an animation.
Key _formKey = UniqueKey();
void _reset() => setState(() => _formKey = UniqueKey());
// A new key means Flutter creates a fresh state.
MyForm(key: _formKey)
GlobalKey #
A GlobalKey gives access to a widget’s state, context or size from anywhere. You have used one already:
final _formKey = GlobalKey<FormState>();
Form(key: _formKey, child: ...)
_formKey.currentState!.validate();
Other uses include ScaffoldMessengerState and NavigatorState for showing snackbars or navigating without a context.
Global keys are more expensive than other keys and make widgets depend on each other from a distance. Use them for forms and a few app-level cases, not as a general way to share state.
What super.key is for #
const TodoTile({super.key, required this.title});
This lets whoever uses your widget pass a key, forwarding it to the base class. Include it in every widget constructor. The linter insists.
Try it yourself #
Make a list of five colour swatches, each a stateful widget that counts its own taps. Add a “Shuffle” button that reorders the list. Watch the counts stay in place without keys, then add ValueKeys and watch them follow their swatches.