In Flutter, what does a GlobalKey give you that a ValueKey does not, and why do the docs call reparenting with one relatively expensive?
answer
- unique across the whole app
- currentState, currentContext, currentWidget
- moves an element to a new parent
- same frame or it is gone
- deactivate, activate, dependents rebuild
basics
~20 sA 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 sA `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 linesclass _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
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.
Explain reparenting within one frame and list what the move costs: deactivate, activate, a rebuild and inherited-dependent rebuilds.
Decide when a GlobalKey is justified versus lifting state or passing a controller, and spot keys recreated per build or duplicated across routes.
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.