Widget keys solve a narrow but real problem: helping Flutter tell widgets of the same type apart when a list changes shape — items get reordered, inserted, or removed. Without the right key, you get a specific, recognizable bug class: state (like a checkbox’s checked value, or scroll position) sticking to the wrong list item after a reorder.
Why Flutter Needs Keys at All
By default, Flutter matches widgets between rebuilds by their type and position in the widget list. If a list of five ListTile widgets becomes four because you removed the second one, Flutter’s default matching says “position 2 used to hold item B, now it holds item C” — and if ListTile at position 2 was a StatefulWidget with its own state, that state stays attached to position 2, now showing item C’s data with item B’s old state.
ValueKey: Identify by a Simple Value
The most common fix: give each item a key based on something that uniquely identifies it, not its position.
ListView(
children: tasks.map((task) {
return Dismissible(
key: ValueKey(task.id),
onDismissed: (_) => removeTask(task),
child: TaskTile(task: task),
);
}).toList(),
)
Now Flutter matches by task.id instead of position, so removing task B leaves task C’s widget — and any state attached to it — correctly matched to task C after the rebuild. Dismissible specifically requires a key for exactly this reason; without one, swiping to dismiss an item can visually dismiss the wrong row.
ObjectKey: Identify by Object Equality
ObjectKey is for when you don’t have a simple primitive id to key on and want to key by an object’s own == equality instead:
ListView(
children: users.map((user) => UserTile(
key: ObjectKey(user),
user: user,
)).toList(),
)
This only works well if the object’s == is meaningful (a data class with value equality, for example) — for a plain class using reference equality, two logically-equal-but-different instances would be treated as different items.
GlobalKey: Identify Across the Entire Tree
GlobalKey is a different tool for a different problem: it lets you access a widget’s State (or its layout information) from anywhere else in the app, not just within a list. It’s commonly used to trigger form validation from outside the form:
final formKey = GlobalKey<FormState>();
Form(
key: formKey,
child: TextFormField(/* ... */),
)
// Elsewhere, e.g. in a submit button's onPressed:
if (formKey.currentState!.validate()) {
// proceed
}
GlobalKeys are more expensive than ValueKey/ObjectKey and should be used sparingly — only when you genuinely need to reach into a widget’s state from outside its own build method, not as a general substitute for list keys.
Quick Rule
Building a list that can be reordered, filtered, or have items removed? Add a ValueKey (or ObjectKey) based on a stable identifier, not the index. Need to reach a widget’s state from somewhere else entirely? That’s what GlobalKey is for — and nothing else needs it.