skip to content

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%

answer

  1. unique across the whole app
  2. currentState, currentContext, currentWidget
  3. moves an element to a new parent
  4. same frame or it is gone
  5. deactivate, activate, dependents rebuild

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.

solid answer

~50 s

A `ValueKey` only distinguishes siblings. A `GlobalKey` is registered with the build owner, so it must be unique in the whole tree, and it gives you a handle to the element: `currentContext`, `currentWidget` and, for a `StatefulWidget`, a typed `currentState`, all `null` when nothing with that key is mounted. It also enables **reparenting**: if a widget with the key appears under a new parent in the same frame it left the old one, Flutter moves the existing element instead of building a new one, so its `State` survives. The docs call this relatively expensive because the move calls `deactivate` on that `State` and its descendants, then `activate` and a rebuild, and forces every widget in the subtree that depends on an `InheritedWidget` to rebuild. Create the key once in a `State`, never in `build()`, and prefer callbacks or controllers over reaching into `currentState`.

code

dart · 13 lines
dart
class _LibraryScreenState extends State<LibraryScreen> {
  // Created once, owned by the State, never inside build().
  final GlobalKey<NowPlayingState> _playerKey = GlobalKey<NowPlayingState>();

  @override
  Widget build(BuildContext context) {
    final bool wide = MediaQuery.sizeOf(context).width > 700;
    final Widget player = NowPlaying(key: _playerKey);
    return wide
        ? Row(children: [const Expanded(child: TrackList()), SizedBox(width: 320, child: player)])
        : Column(children: [const Expanded(child: TrackList()), player]);
  }
}

go deeper

for a junior

Recall that a GlobalKey is unique in the whole app and gives access to currentState and currentContext, and that it must not be created in build.

for a middle

Explain reparenting within one frame and list what the move costs: deactivate, activate, a rebuild and inherited-dependent rebuilds.

for a senior

Decide when a GlobalKey is justified versus lifting state or passing a controller, and spot keys recreated per build or duplicated across routes.

for a principal

Weigh the coupling a GlobalKey creates between distant widgets against its convenience, and set guidance on where the codebase allows one.

## Two branches of the key hierarchy Flutter's `Key` class has two families: - **`LocalKey`** (`ValueKey`, `ObjectKey`, `UniqueKey`, `PageStorageKey`): only compared among the children of one parent, used to keep element identity stable in lists. - **`GlobalKey<T extends State<StatefulWidget>>`**: registered in a global registry owned by the `BuildOwner`, so it must be **unique across the entire widget tree**. `GlobalKey()` is a factory that creates a `LabeledGlobalKey` (the optional `debugLabel` only affects `toString`); `GlobalObjectKey(value)` takes its identity from an object with `identical()`. Using the same `GlobalKey` twice in the tree fails with debug assertions such as "Multiple widgets used the same GlobalKey." or "Duplicate GlobalKey detected in widget tree." ## What the handle gives you Because the registry maps each key to its mounted element, a `GlobalKey` exposes three getters: | Getter | Returns | `null` when | |---|---|---| | `currentContext` | the element, as a `BuildContext` | nothing with the key is mounted | | `currentWidget` | the widget currently using the key | nothing with the key is mounted | | `currentState` | the typed `State` | not mounted, not a `StatefulWidget`, or the `State` is not a `T` | Typical legitimate uses: - Calling a method on a framework widget's state, for example `GlobalKey<AnimatedListState>` and `listKey.currentState!.insertItem(index)` to animate a new track into a playlist. - Reading layout after a frame: `key.currentContext?.findRenderObject()` to measure a widget. - Keeping a subtree alive while it moves, as described next. Reaching into another widget's `State` couples the two tightly; callbacks, controllers or shared state are usually cleaner, and the key is best reserved for cases the framework itself designs around. ## Reparenting: how state survives a move Normally a widget that appears under a different parent gets a new element and a new `State`. With a `GlobalKey` the framework does more: when it inflates a widget whose global key belonged to an element elsewhere, it **retakes that element** and grafts it into the new position, as long as `Widget.canUpdate` still holds (same `runtimeType`). This has a strict timing condition. Elements removed during a frame go to an inactive list and are unmounted at the end of that frame, which is also when their global keys are unregistered. So: 1. The widget must reappear at its new location **in the same frame** it was removed from the old one. 2. If it does, `State.activate` runs instead of `dispose`, and the element is rebuilt at its new location. 3. If it does not, the `State` is disposed and the key is free; a later reappearance starts from `initState`. A realistic case: a now-playing panel sits in a `Row` beside the playlist on wide screens and in a `Column` below it on narrow ones. Switching `Row` for `Column` changes the parent's type, so without a `GlobalKey` the player's `State`, including its playback position, would be recreated on every resize across the breakpoint. ## Why the move is relatively expensive The `GlobalKey` docs spell out the cost: - `State.deactivate` runs on the moved `State` **and all of its descendants**. - Each element is reactivated; a stateful element marks itself dirty and rebuilds. - Every widget in the subtree that depends on an `InheritedWidget` is **forced to rebuild**, because its dependencies were dropped on deactivation and it now sits under different ancestors (its `didChangeDependencies` runs). For one panel this is cheap enough; for large subtrees moved often, or for keys attached to many list items, it adds up. The docs advise using a local key when you need none of these features. ## Pitfalls interviewers probe - **Creating the key in `build()`**: each rebuild creates a new key, so the old subtree is discarded and a fresh one built. The docs note that a `GestureDetector` inside cannot even track an ongoing gesture. Create the key once, as a field of a `State` or in `initState`. - **Duplicates**: two widgets sharing a key in one frame assert; this often happens when the same keyed widget is built on two routes at once. - **Assuming `currentState` is non-null**: it is `null` before the widget is mounted, after removal, and when the type argument does not match.

  • What happens if the GlobalKey-keyed widget reappears one frame after it was removed?
    It starts over. At the end of the frame in which it was removed, inactive elements are unmounted, their `State` objects disposed and their global keys unregistered. A widget with that key built in a later frame inflates a new element and runs `initState`.
  • Why does currentState sometimes return null even though the widget is on screen?
    `currentState` returns `null` if the keyed widget is not a `StatefulWidget`, or if its `State` is not an instance of the key's type argument `T`. It is also `null` before the widget is first mounted and after it leaves the tree.

saying these in an interview costs you the question

  • A GlobalKey keeps a widget's State alive after it leaves the tree, until it returns.
  • Creating a GlobalKey inside build is fine because it is global anyway.
  • GlobalKeys are just ValueKeys that are unique app-wide, with no other behaviour.
  • Reparenting with a GlobalKey is free, so key every list item with one.
  • currentState always returns the State once the widget has been built once.