In Flutter, how do ValueKey, ObjectKey and UniqueKey differ in what they compare, and which fits items loaded from an API?
answer
- what does each == compare
- ValueKey delegates to value ==
- ObjectKey uses identical()
- UniqueKey equals only itself
- runtimeType checked first
basics
~10 sValueKey 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.
solid answer
~40 sAll three are `LocalKey`s, so they only need to be unique among one parent's children. `ValueKey<T>` delegates equality to its value's `==`; `Key('x')` is simply a const factory for `ValueKey<String>`. `ObjectKey` compares its value with `identical()`, so it matches only the same instance. `UniqueKey` is equal to nothing but itself and has no const constructor. Every one of them also checks `runtimeType` first, so `ValueKey('a')` never equals `PageStorageKey('a')`. For items parsed from an API, each refresh creates new instances, so `ObjectKey(item)` changes on every fetch while `ValueKey(item.id)` stays equal: that is the default choice. `UniqueKey` created inside `build()` produces a new identity every rebuild and resets the subtree's state each time; use it only deliberately, created once and replaced when you want a reset.
code
dart · 7 linesListView(
children: [
for (final Track track in tracks)
// New Track instances arrive on every refresh; the id stays equal.
TrackTile(key: ValueKey(track.id), track: track),
],
)go deeper
Remember one line per key: ValueKey compares values, ObjectKey compares identity, UniqueKey matches only itself.
Explain what happens to each key after a refetch that creates new model objects, and why runtimeType is part of key equality.
Catch UniqueKey in build and ObjectKey on parsed data in review, and choose the id that expresses real identity rather than whole-object equality.
Treat stable ids in the data layer as a UI requirement too; a model without identity forces weaker keys everywhere it is rendered.
## Local keys and their scope A Flutter **key** decides which existing element a new widget updates: `Widget.canUpdate` reuses an element only when the old and new widgets have the same `runtimeType` and keys that compare equal with `==`. So the kind of key you pick is really a choice of **equality rule**. `ValueKey`, `ObjectKey` and `UniqueKey` all extend **`LocalKey`**. A local key only has to be unique among the children of a single parent; the same `ValueKey(1)` can appear in two different lists on one screen without conflict. (`GlobalKey`, the other branch of the hierarchy, is unique across the whole app and is a different tool.) ## The three equality rules side by side | Key | `==` is true when | Const? | Typical use | |---|---|---|---| | `ValueKey<T>(value)` | same runtime type and `value == other.value` | yes | an id or other stable value from the data model | | `ObjectKey(value)` | same runtime type and `identical(value, other.value)` | yes | the object instance *is* the identity | | `UniqueKey()` | only when compared with itself | no | forcing a fresh element on purpose | Two details from the source are worth knowing: - `Key('track-42')` is a const factory that builds a `ValueKey<String>`; it is not a separate kind of key. - Every `==` implementation starts with `other.runtimeType != runtimeType`. `ValueKey<int>(1)` and `ValueKey<num>(1)` are therefore different keys, and a `PageStorageKey` (a `ValueKey` subclass) never equals a plain `ValueKey` with the same value. The `ValueKey` docs even suggest a private subclass as a way to make keys that cannot collide with anyone else's. ## Why `ValueKey(item.id)` is the default for fetched data Consider a playlist loaded from a backend and refreshed with pull-to-refresh: 1. The first fetch parses JSON into `Track` objects and builds a `TrackTile` per track. 2. The refresh parses the response again, producing **new `Track` instances**, even for tracks that did not change. 3. With `ObjectKey(track)`, every new key wraps a different instance, `identical()` fails, and every stateful tile is torn down and rebuilt with a fresh `State`. 4. With `ValueKey(track.id)`, the ids are equal strings or ints, so each existing element and its `State` is reused. If `Track` overrides `==` by value (with a package such as `equatable`, or by hand), `ValueKey(track)` also works, but then every field participates: a changed title makes the key unequal and resets that tile. Keying by the id alone expresses the real identity. ## `ObjectKey`: when identity is the right answer `ObjectKey` fits when the object instance genuinely is the identity and survives rebuilds: an in-memory list of mutable view-model objects held in a `State`, or objects with no natural id. The docs describe it as tying "the identity of a widget to the identity of an object used to generate that widget". The shopping-list example in Flutter's own UI introduction keys its items with `ObjectKey(product)` for exactly this reason. ## `UniqueKey`: forcing a fresh subtree A `UniqueKey` never equals any other key, so a widget carrying a new `UniqueKey` can never reuse an old element. - **Created inside `build()`**: every rebuild of the parent produces a new key, so the child's `State` is disposed and recreated each time. Controllers restart, text fields lose their text and animations jump back. This is a bug, not a way to "make keys unique". - **Deliberate reset**: store a `UniqueKey` in the parent's `State` and replace it inside `setState` when you want the subtree to start over, for example restarting a track-preview widget. Incrementing a counter used as `ValueKey(counter)` achieves the same effect. ## Common mistakes to avoid - `ValueKey(index)` is not an identity. After a reorder, position 0 still has key 0, so matching is exactly as positional as with no key at all. - Keys must be unique among siblings; equal keys under one multi-child parent trigger the debug assertion "Duplicate keys found." - Putting the key on a widget nested inside each item instead of on the item's outermost widget means the parent never sees it.
- When is a UniqueKey what you actually want in Flutter?When you intend to throw a subtree's state away. Keep the `UniqueKey` in the parent's `State` and assign a new one inside `setState` when the user asks for a reset; the child then gets a new element and runs `initState` again. Creating it inline in `build()` does the same thing on every rebuild, which is almost never intended.
- Why is ValueKey(index) no better than no key at all?Because the index describes a position, not an item. After a sort, the widget at position 0 still carries `ValueKey(0)`, so `canUpdate` pairs it with the element that was at position 0, exactly as keyless matching would. It merely satisfies APIs that demand a key, such as `ReorderableListView`.
- Does ValueKey<int>(1) equal PageStorageKey<int>(1) in Flutter?No. `ValueKey.==` returns false when `runtimeType` differs, and `PageStorageKey` is a subclass with its own runtime type. Subclassing `ValueKey`, especially privately, is the documented way to make keys that cannot collide with keys from other code that happen to wrap the same value.
saying these in an interview costs you the question
- ObjectKey compares its object with ==, so equal data gives equal keys.
- A UniqueKey created in build keeps state stable because each key is unique.
- ValueKey(index) keeps each item's state attached after a reorder.
- Local keys must be unique across the whole app, not just among siblings.
- Key('x') is a separate key type that compares by identity.