Controlling Widget Rebuilds with Keys
Controlling Widget Rebuilds with Keys
Flutter's reconciliation algorithm relies on two pieces of information to decide whether an existing Element can be reused for an incoming widget: the widget's runtime type and its key. When you omit a key, Flutter uses position in the widget tree as the implicit identity. This works perfectly for stateless widgets, but breaks down the moment stateful or expensive widgets are reordered, inserted, or removed — because the element subtree that holds live state gets matched to the wrong widget configuration.
How Flutter Matches Widgets to Elements
On every build() call, Flutter walks both the old and new widget subtrees in parallel. For each position it checks:
- If the new widget's runtimeType and key match the existing element, Flutter calls
update()— the element and its state are preserved. - If they do not match, Flutter deactivates the old element (potentially losing state) and inflates a new one.
- When a key is present, Flutter can also search siblings for a match rather than relying on position alone.
The Four Key Types
ValueKey
Uses a single value — typically a String, int, or enum — as the identity. Two ValueKeys are equal when their values are equal. Use this when each item in a list has a unique, stable identifier such as a database ID or a slug.
// Reorderable to-do list — each tile keeps its checkbox state
// because the key follows the item, not the position.
ListView(
children: todos.map((todo) {
return CheckboxListTile(
key: ValueKey<int>(todo.id),
value: todo.isDone,
onChanged: (v) => setState(() => todo.isDone = v!),
title: Text(todo.title),
);
}).toList(),
)
ObjectKey
Uses an entire object's identity (via identical()) rather than its equality. Use this when two objects might have the same field values but are genuinely different instances — for example, two Contact objects with the same name but different references.
// Two contacts may share a name; use object identity to distinguish them.
Column(
children: contacts.map((contact) {
return ContactCard(
key: ObjectKey(contact),
contact: contact,
);
}).toList(),
)
UniqueKey
Generates a key that is never equal to any other key, including another UniqueKey(). Because it is unique per instance, assigning a UniqueKey in build() forces a full teardown and rebuild on every render — the element is always discarded and recreated. This is intentional in rare scenarios (e.g., dismissing and reinitialising a widget), but destructive if used carelessly.
UniqueKey() inside a build() method unless you deliberately want the widget to lose all its state on every rebuild. Store the key as a field in your State class if you need to trigger a one-time reset.GlobalKey
Provides a handle to an element (and its associated State or BuildContext) that can be accessed from anywhere in the widget tree, even across routes. Common uses are accessing form state, triggering animations, or measuring a widget's size at runtime. GlobalKey is powerful but expensive — each key is registered in a global lookup table, so creating many of them degrades performance.
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
// Stored as a field — NOT recreated on every build().
final _formKey = GlobalKey<FormState>();
void _submit() {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
// proceed with login
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
validator: (v) => v!.isEmpty ? 'Required' : null,
),
ElevatedButton(
onPressed: _submit,
child: const Text('Log In'),
),
],
),
);
}
}
The Classic Keys Bug: Stateful List Items
The most instructive failure case is a reorderable list of StatefulWidget items with no keys. When the first and second items swap positions, Flutter sees the same widget types at each position, calls update() on the existing elements, and updates the configuration — but the state objects stay in place. The result: the wrong state is attached to the wrong item. Adding ValueKey (or ObjectKey) keyed to the item's identity fixes this immediately because Flutter can match elements by key across the position change.
Choosing the Right Key
- List/grid items with stable IDs →
ValueKey(item.id) - Items identified by object reference →
ObjectKey(item) - Force full reset once (store in State, flip to new UniqueKey on demand) →
UniqueKey() - Cross-tree access to State or BuildContext →
GlobalKey(use sparingly) - Stateless widgets in a stable list → no key needed
ValueKey or ObjectKey. The performance cost is negligible; the correctness benefit is significant.Summary
Keys give Flutter explicit control over element identity during reconciliation. ValueKey matches by a logical value, ObjectKey matches by reference identity, UniqueKey always forces recreation, and GlobalKey exposes a widget's internals across the tree. Using the right key in the right place prevents subtle state-misattribution bugs and gives you fine-grained control over when Flutter preserves or discards widget subtrees.