What are Keys in Flutter and when should you use them?
3 min read
Flutter Internals
Theory — why keys matter
Flutter's diff algorithm compares old and new widget trees at each position. By default:
Before: [A] [B] [C] After removing B: [A] [C]
↑ ↑
pos 0 1 2 pos 0 1
Without keys, Flutter sees "position 1 used to be B, now it's C" — it doesn't drop B, it rewrites it into C, keeping B's underlying State attached to the new widget. For stateless widgets that's harmless; for stateful ones (checkboxes, text fields, animations), state ends up on the wrong row.
With keys, Flutter matches by identity:
Before: [A:1] [B:2] [C:3] After: [A:1] [C:3]
Now B is correctly disposed and C keeps its state.
The 4 key types
| Key | Identity is… | Use for |
|---|---|---|
ValueKey(x) | == on the value | Most lists — pass a stable item id |
ObjectKey(o) | identity (identical) of the object | When values can be equal but instances differ |
UniqueKey() | unique every build | Force a widget to rebuild from scratch |
GlobalKey<T> | unique app-wide | Access a child's state or RenderObject from outside |
Code in action
// 1️⃣ Stateful list items — keys preserve state on reorder/remove
ListView(
children: users.map((u) => CheckboxTile(
key: ValueKey(u.id),
user: u,
)).toList(),
);
// 2️⃣ Force a clean slate (reset a form when filters change)
RefreshableProductList(key: ValueKey(filter)); // new key → new state
// 3️⃣ Global key — read/control child state
final _formKey = GlobalKey<FormState>();
Form(key: _formKey, child: ...);
if (_formKey.currentState!.validate()) { ... }
// 4️⃣ UniqueKey — when you specifically want a remount
AnimatedSwitcher(
child: Container(key: UniqueKey(), color: bgColor),
);
When to add a key
| Situation | Key? |
|---|---|
List of stateless display items (Text) | ❌ Not needed |
| List of stateful items that can reorder/insert/remove | ✅ ValueKey(id) |
| Swapping between two widgets of the same type | ✅ Distinct keys to force a remount |
| Reading a child widget's state from outside | ✅ GlobalKey<T> |
| Forcing a remount when params change | ✅ ValueKey(param) |
| Every widget "just to be safe" | ❌ Overhead with no benefit |
Common mistakes to avoid
// ❌ No keys on stateful list items — state attaches to wrong row
Column(children: items.map((i) => CheckboxTile(item: i)).toList());
// ✅ Stable ValueKey
Column(children: items.map((i) => CheckboxTile(
key: ValueKey(i.id),
item: i,
)).toList());
// ❌ Using UniqueKey() in a hot path — forces full remount every build
return ListView(
children: items.map((i) => Tile(key: UniqueKey(), item: i)).toList(),
);
// ✅ Use stable IDs unless you actually want a remount
// ❌ Using `i` (index) as the key in a reorderable list
key: ValueKey(index), // shifts when you remove/reorder → defeats the purpose
// ❌ Over-using GlobalKey — they're more expensive and lifecycle-tricky
GlobalKey here, GlobalKey there → state lookups everywhere
// ✅ Prefer callbacks / lifting state; GlobalKey only when you truly need cross-tree access
// ❌ Putting a key on the wrong widget
// Keys go on the widget WHOSE IDENTITY matters across rebuilds —
// usually the top widget you return for each item, not the inner Text.
Interview follow-ups
-
What's the difference between
ValueKeyandObjectKey?ValueKey(v)uses==on the wrapped value — perfect for stable IDs.ObjectKey(o)usesidentical()— useful when two model instances have equal fields but aren't the same object, and you want them treated as distinct. -
When does
UniqueKey()actually make sense? When you want the widget to be torn down and recreated — e.g., resetting anAnimationControllerinside a child, or makingAnimatedSwitchertreat two same-typed widgets as different. -
What does a
GlobalKeydo, and what are the trade-offs? It gives the widget a unique identity across the whole tree, so its element can move without being recreated, and you can readcurrentState/currentContext/currentWidgetfrom anywhere. Trade-offs: more memory, gotchas around moving in/out of the tree, and easy to overuse. -
Why does using index as a key in a dynamic list break state preservation? Because the index shifts when items are added/removed/reordered. Item at index 2 might become index 1 after a delete — same key, different item, same broken matching you get with no key.
How helpful was this content?
Please sign in to rate this article.