skip to content

Keys & Identity

Flutter reuses an element when the new widget has the same runtimeType and key, so without keys state follows list position, not the item. Interviewers ask about the reordered-list bug.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Flutter, what is a widget's key for, and when do you actually need to give a widget one?

level: juniorimportance: must knowfreq 74%

answer

  1. matching old children to new
  2. runtimeType plus key
  3. same-type stateful siblings
  4. State lives in the element
  5. insert, delete, reorder

basics

~20 s

A key tells Flutter which old widget a new one replaces: an element is reused only when runtimeType and key match. Same-type stateful siblings that can be inserted, removed or reordered need one, or their state stays with the position.

solid answer

~40 s

When a parent rebuilds, Flutter matches its old child elements to the new child widgets and reuses an element whenever `Widget.canUpdate` is true: same `runtimeType` and equal `key`, where two null keys count as equal. Without keys, same-type siblings are therefore matched by position. That is harmless for purely stateless rows, but a `StatefulWidget`'s `State` belongs to the element, so after a sort, an insert or a delete the state stays at its index while the data moves. Giving each such sibling a key derived from its data, usually `ValueKey(item.id)`, makes Flutter match old and new children by key instead. A widget that is the only child of its parent, or a list whose order never changes, generally needs no key.

code

dart · 6 lines
dart
Column(
  children: [
    for (final Track track in tracks)
      TrackTile(key: ValueKey(track.id), track: track),
  ],
)

go deeper

for a junior

Recall the two things canUpdate compares, runtimeType and key, and that keyless same-type siblings are matched by position, so state stays at the index.

for a middle

Walk through a delete or reorder step by step and say which element each new widget lands on, and why the key belongs on the outermost widget of the item.

for a senior

Spot position-bound state in a code review before it ships, and prefer ids from the data model over indexes or generated keys.

for a principal

Frame keys as the identity contract between data and UI; ask whether the state should live in the model instead, so identity bugs cannot corrupt it.

## What a key is Every Flutter widget constructor accepts an optional `key` (declared on `Widget` as `final Key? key`). A **key** is not data the widget displays and it does not hold state. It is an **identity hint**: when a parent rebuilds and produces a fresh list of child widgets, the framework uses keys to decide which existing **element** each new widget should update. This matters because widgets are immutable, throwaway configuration objects, while the long-lived part of the UI is the element tree. A `StatefulWidget` keeps its `State` object inside its element, so whichever element a new widget is matched to decides which `State` that widget gets. ## The rule Flutter applies: `Widget.canUpdate` The whole decision is one static method in `framework.dart`: ```dart static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; } ``` - If it returns `true`, the old element is **kept** and simply receives the new widget as its configuration; a stateful element keeps its `State` and calls `didUpdateWidget`. - If it returns `false`, the old element is **removed** and a new one is created from the new widget, with a brand-new `State`. - Two widgets with **no key** have equal keys (`null == null`), so for keyless siblings only the type matters. For a multi-child parent such as `Column`, the matching walks the old and new child lists from the top while they keep matching, then from the bottom, and matches whatever is left in the middle by key. Keyless children in the middle are not matched to each other at all. ## The classic symptom: state follows the position Picture a playlist screen: a `Column` of `TrackTile` widgets, each a `StatefulWidget` whose `State` holds a `bool` for a "download" checkbox. The user ticks the first track, then deletes it. 1. Old children: `TrackTile(A)`, `TrackTile(B)`, `TrackTile(C)`, no keys; A's `State` has the tick. 2. New children after the delete: `TrackTile(B)`, `TrackTile(C)`. 3. Same type, both keys null, so the first old element (holding A's ticked `State`) is updated with B's widget, and the second with C's. 4. The third old element is left over and is removed. Track B now shows a tick it never had, and the `State` that really belonged to C was the one thrown away. Nothing crashed; the state simply stayed at index 0. With `key: ValueKey(track.id)` on each `TrackTile`, the top-down walk stops at the first pair whose keys differ, and the remaining children are matched by key: B's element goes to B, C's to C, and A's element is removed along with A. ## When you need a key, and when you do not | Situation | Key needed? | Why | |---|---|---| | Same-type stateful siblings that can be reordered, inserted or removed | Yes | Otherwise `State` stays at its index | | A stateless row whose subtree holds stateful widgets (a `TextField`, an `ExpansionTile`) | Yes, on the row | The descendants' state lives under the row's element | | A list that is built once and never changes order | No | Position and identity never diverge | | The only child of a parent (`Scaffold.body`, a `Padding` child) | No | There is nothing to confuse it with | | Siblings of different types | Rarely | `runtimeType` already tells them apart | ## Choosing what goes in the key - Use a value that is **stable and unique per item**, normally the id from your data model: `ValueKey(track.id)`. - Keys only need to be **unique among the children of one parent**. Two siblings with equal keys trip the debug assertion "Duplicate keys found." - The **list index is not an identity**: `ValueKey(index)` behaves exactly like no key after a reorder, because index 0 still has key 0. - A key goes on the **outermost widget of each item**, the one the parent actually sees in its child list. The practical interview answer is short: keys exist so that state follows the item rather than the slot, and they only matter where same-type siblings can change places.

  • Does a key on a StatelessWidget row ever matter?
    Yes, when the row's subtree contains stateful widgets such as a `TextField` or an `ExpansionTile`. Their `State` lives in elements under the row's element, so if the row is matched by position, that descendant state stays at the position too. Keying the row makes the whole subtree follow the item.
  • What happens if two siblings in a Flutter Column get the same ValueKey?
    In debug builds the multi-child element asserts with "Duplicate keys found.", because keys must be unique among one parent's children. The check is an assertion, so release builds do not run it; the duplicate has to be fixed at the source rather than tolerated.

Coats handed to a cloakroom are returned by hook number unless each carries a name tag. Without tags, whoever now stands at hook 1 gets the coat hanging there; a key is the name tag that makes the coat follow its owner.

saying these in an interview costs you the question

  • Every child of a Column or ListView must have a key or Flutter throws.
  • The key stores the widget's state, so the state is saved inside the key.
  • Without keys Flutter matches children by comparing their constructor arguments.
  • State lives in the widget object, so it moves with the item automatically.
  • Using the list index as the key fixes the reorder bug.
open as a page

In Flutter, how do ValueKey, ObjectKey and UniqueKey differ in what they compare, and which fits items loaded from an API?

level: middleimportance: must knowfreq 58%

basics

~10 s

ValueKey equals a key of the same runtime type whose value is ==; ObjectKey needs the very same object (identical); UniqueKey equals only itself. For API data, ValueKey(item.id) survives refetches that build new objects.

open as a page

In Flutter, what does a GlobalKey give you that a ValueKey does not, and why do the docs call reparenting with one relatively expensive?

level: middleimportance: should knowfreq 50%

basics

~20 s

A GlobalKey is unique app-wide, exposes currentState, currentContext and currentWidget, and lets an element keep its State while moving to a new parent within one frame. That move deactivates and reactivates the subtree and rebuilds inherited-widget dependents.

open as a page

In a Flutter playlist, sorting moves each stateful TrackTile's checkbox tick to the wrong track, and adding ValueKey(track.id) to TrackTile only made ticks vanish; what is going on?

level: seniorimportance: should knowfreq 47%

basics

~20 s

Keys are only compared among one parent's direct children. The list sees keyless wrappers matched by position, so the inner TrackTile's key mismatches and its State is recreated. Key each item's outermost widget; builder lists also need findChildIndexCallback.

open as a page

In Flutter, why does an ExpansionTile in a long ListView collapse after being scrolled away and back, and how does a PageStorageKey fix it?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

Off-screen items in a lazy ListView are disposed, so the tile's new State starts collapsed. With a unique PageStorageKey, ExpansionTile writes its expanded flag to the route's PageStorage bucket and reads it back when rebuilt.

open as a page