Flutter Widget Keys Explained: ValueKey, ObjectKey and GlobalKey

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.