Flutter Keys Explained: ValueKey, GlobalKey, and When to Use Them

Coding Liquids blog cover featuring Sagnik Bhattacharya for the Flutter keys guide, with a reordered list preserving each item's state thanks to ValueKey.
Coding Liquids blog cover featuring Sagnik Bhattacharya for the Flutter keys guide, with a reordered list preserving each item's state thanks to ValueKey.

Keys are one of those Flutter features you can ignore for months until a list reorders and the state lands on the wrong row. At that point the standard fix — sprinkle a ValueKey on each item — works, but it helps to know why. Keys are how Flutter decides which widget in a new build corresponds to which element from the last build. Most of the time it gets this right from type and position alone; keys are the override you reach for when same-typed widgets move around. This guide explains how that matching works, when ValueKey and ObjectKey earn their place, what GlobalKey is really for, and the costs, with runnable code.

The Complete Flutter Guide course thumbnail

The Complete Flutter Guide: Build Android, iOS and Web apps

Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.

Enrol now

Every snippet below is paste-ready against current stable Flutter. If you are comfortable with how stateful widgets hold their state, you already have the mental model keys build on.

Follow me on Instagram@sagnikteaches

We'll start with the problem keys solve, then walk the matching algorithm, the local key types, the classic reorder bug, and finally GlobalKey.

Connect on LinkedInSagnik Bhattacharya

If you prefer to see the reorder bug reproduced and fixed on screen, the channel walks through these widget-tree internals with live examples.

Subscribe on YouTube@codingliquids

Why keys exist

Flutter keeps three trees in step: the widgets you describe, the elements that hold state and bridge to render objects, and the render objects that paint. Widgets are cheap and rebuilt constantly; elements are the persistent layer that remembers a State object, a scroll position, or an in-flight animation. On every rebuild Flutter must decide, for each new widget, whether to reuse the existing element in that slot or create a fresh one.

That decision is what a key influences. A key is an identity tag you attach to a widget so Flutter can recognise it across rebuilds even if it has moved. Without one, Flutter falls back to a simpler rule, which is fine until widgets of the same type swap places.

How Flutter matches widgets without keys

When a parent rebuilds, Flutter walks the old and new child lists together and asks, for each position, whether the old element can be updated to the new widget. The test is Widget.canUpdate: same runtimeType and same key. With no keys, the key on both sides is null, so the match comes down to type and position.

// Conceptually, for siblings at index i:
bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}

For a static column — a title, then an image, then a button — this is perfect and free. Each child stays in its slot, so position is a reliable identity and you never touch a key. The trouble only begins when same-typed children at the same level can move relative to one another.

ValueKey and ObjectKey for lists

For a list of identical widget types, you want Flutter to match by the data each row represents, not by slot. That is exactly what a local key does. The two you reach for most are ValueKey and ObjectKey.

ListView(
  children: [
    for (final task in tasks)
      TaskTile(
        key: ValueKey(task.id),   // stable id, equality by value
        task: task,
      ),
  ],
)

A ValueKey compares by ==, so use it with a stable, unique value such as a database id or a UUID — never the list index, which changes when items move. An ObjectKey compares by identity (identical) and is handy when you have the model object itself but no single id field. There is also UniqueKey, which is never equal to anything, including itself on the next build; that forces a brand-new element every rebuild, so reserve it for the rare case where you deliberately want to discard and recreate state.

The Complete Flutter Guide course thumbnail

Build lists that behave correctly

The Complete Flutter Guide covers keys, builders, and stateful list items inside real, shipped apps.

Enrol now

The state-stuck-to-position bug

Here is the classic reproduction. Two stateful tiles, each holding its own toggle, sitting in a row you can swap.

class ColourTile extends StatefulWidget {
  const ColourTile({super.key, required this.label});
  final String label;
  @override
  State<ColourTile> createState() => _ColourTileState();
}

class _ColourTileState extends State<ColourTile> {
  bool _on = false;     // this lives in the element, not the widget
  @override
  Widget build(BuildContext context) => SwitchListTile(
        title: Text(widget.label),
        value: _on,
        onChanged: (v) => setState(() => _on = v),
      );
}

Toggle the first tile on, then swap the two. Without keys, the _on = true state stays on the first slot, so it appears to jump to whichever tile is now on top — the labels swap but the toggles do not follow. Give each tile a ValueKey(label) and the state follows the data as expected.

children: [
  ColourTile(key: ValueKey(items[0]), label: items[0]),
  ColourTile(key: ValueKey(items[1]), label: items[1]),
]

The same fix applies to ListView.builder output, dismissible rows, and anywhere a StatefulWidget can change position. Stateless rows that hold no state will not show the bug, which is why keys feel optional until you add state to an item.

GlobalKey and when to use it

A GlobalKey is a different tool. Where a ValueKey only distinguishes siblings under one parent, a GlobalKey is unique across the whole app and gives you a handle to reach a widget's element or state from anywhere — to validate a form, open a drawer, or measure a widget's size.

final _formKey = GlobalKey<FormState>();

Form(
  key: _formKey,
  child: /* fields */,
);

// later, from a button callback:
if (_formKey.currentState!.validate()) {
  _formKey.currentState!.save();
}

That is the legitimate use: an imperative handle into a subtree you cannot reach by passing data down. A GlobalKey also lets you move a subtree to a new place in the tree while keeping its state alive. What it is not is a list-item key — reaching for GlobalKey on every row is overkill and slow.

Costs and mistakes

Keys are not free. Adding a GlobalKey registers the element in a global table, and re-parenting via a global key triggers extra work as Flutter detaches and re-attaches the subtree. Local keys are cheap, but a wrong key — an index, or a UniqueKey created in build — can be worse than no key, because it tells Flutter to throw away and rebuild state on every frame. The rule of thumb: prefer no key, use a ValueKey or ObjectKey with a stable id for movable stateful lists, and keep GlobalKey for the handful of places you genuinely need an imperative handle.

Common mistakes

  • Keying on the list index. Indices shift when items move, defeating the purpose; key on a stable id.
  • Creating a UniqueKey in build. It is never equal across builds, so state is rebuilt every frame.
  • Using GlobalKey for list items. It is heavier and unnecessary; use ValueKey or ObjectKey.
  • Adding keys to static, non-moving children. They add noise with no benefit; reserve keys for movable widgets.
  • Putting the key on the wrong widget. Key the item whose identity moves — usually the top widget of each row, not an inner child.

Frequently asked questions

What are keys in Flutter?

Keys are identifiers Flutter uses to match a new widget with an existing element so it can preserve that element's state across rebuilds. Most widgets need no key; they matter when same-typed widgets move within a collection.

When do I actually need a key?

When stateful widgets of the same type change position — reorder, insert, remove, or filter — so each item's state follows its data. Give each one a ValueKey or ObjectKey from a stable id.

What is the difference between ValueKey and GlobalKey?

A ValueKey is local — it distinguishes siblings under one parent, ideal for lists. A GlobalKey is app-wide and lets you reach a widget's state from elsewhere, but it is heavier, so reserve it for that need.

Why does my list state jump to the wrong item after reordering?

Without keys, Flutter matches by type and position, so state stays pinned to the slot rather than the data. A ValueKey tied to a stable id makes it match by identity, so state moves with the item.

Further reads

Keep going with the tutorials that pair with this guide:

Sources: Flutter documentation — Key, ValueKey, ObjectKey, UniqueKey, and GlobalKey; the 'When to use keys' guidance (api.flutter.dev, docs.flutter.dev). Verified against current stable Flutter.