skip to content

In Flutter, what is the difference between a State's deactivate and dispose callbacks, and when does activate run?

level: middleimportance: nice to knowfreq 24%

answer

  1. removed versus gone for good
  2. reinsertion before the frame ends
  3. GlobalKey subtree grafting
  4. activate, then build again
  5. no ancestor lookups in dispose

basics

~20 s

deactivate runs when a State's subtree is removed from the tree; if it is reinserted elsewhere before the frame ends, typically a GlobalKey graft, activate and build follow. Otherwise dispose runs at the frame's end and the State can never build again.

solid answer

~40 s

Removal is two-stage. `deactivate` is called whenever the subtree containing the State leaves the tree, for example because the parent built a different type or key; override it only to unlink from other elements, and end with `super.deactivate()`. The framework may then reinsert the subtree at another location before the end of that same frame, which happens when a widget with a `GlobalKey` moves; it then calls `activate`, never on first insertion, and rebuilds the State. If no reinsertion happens by the end of the frame, it calls `dispose`: `mounted` becomes false, and the State is finished for good. Because reinsertion is possible, most resources should be released in `dispose`, not `deactivate`. Ancestor lookups are unsafe in `dispose`; save the reference in `didChangeDependencies` instead.

code

dart · 16 lines
dart
// Inside _BarometerState
late ScaffoldMessengerState _messenger;

@override
void didChangeDependencies() {
  super.didChangeDependencies();
  _messenger = ScaffoldMessenger.of(context); // element is active here
}

@override
void dispose() {
  // ScaffoldMessenger.of(context) here would assert:
  // "Looking up a deactivated widget's ancestor is unsafe."
  _messenger.hideCurrentSnackBar();
  super.dispose();
}

go deeper

for a junior

Recall that dispose is the final callback and that deactivate comes just before it when a widget is removed.

for a middle

Explain the two-stage removal, why activate exists for same-frame reinsertion with a GlobalKey, and why resources are released in dispose.

for a senior

Debug lifecycle surprises: States that survive a move, lookup assertions in dispose, and early releases in deactivate that break a reinserted subtree.

for a principal

Judge when reparenting live subtrees is worth its complexity versus rebuilding fresh State, given what the two-stage lifecycle demands of every widget involved.

## Why removal has two stages Flutter lets a subtree move within the tree without losing its State: a widget carrying a **GlobalKey** can be removed from one parent and inserted under another in the same frame, and the framework grafts the existing element subtree, State included, to the new position. To support that, removal is split into an **inactive** stage, which may be undone within the frame, and a **defunct** stage, which cannot. ## deactivate - Called when the subtree containing the State is removed, for example because the parent now builds a widget with a different `runtimeType` or key at that spot. - Meant for cleaning up links between this object and other elements, such as a pointer an ancestor holds to this State's render object. - Must end with `super.deactivate()`. - Should not release most resources: the framework documents that States can defer that until `dispose`, because reinsertion may follow. ## activate - Called only when a deactivated State is reinserted into the tree, which the framework does before the end of the frame in which it was removed. - Never called on first insertion; `initState` covers that. - Must start with `super.activate()`; afterwards the framework marks the element for rebuild, and if the element had inherited dependencies it also calls `didChangeDependencies`, because the new position may see different inherited values. - The place to reacquire anything `deactivate` released. ## dispose - Called when the State was not reinserted by the end of the frame. - Terminal: `mounted` becomes false, `setState` is an error, and there is no way to remount the State. - Release everything: cancel subscriptions and timers, dispose controllers, stop animations, then call `super.dispose()` last. - Not called on app shutdown or process termination. ## Ancestor lookups in dispose By the time `dispose` runs, the element is no longer active, and a debug assertion rejects lookups: "Looking up a deactivated widget's ancestor is unsafe." Its hint gives the fix: to refer to an ancestor in `dispose`, save a reference in `didChangeDependencies`. A barometer that hides its calibration snackbar when it goes away stores the `ScaffoldMessengerState` there and uses the stored reference in `dispose`. ## Side by side | | `deactivate` | `activate` | `dispose` | |---|---|---|---| | When | Subtree removed | Reinserted in the same frame | Not reinserted by frame end | | Can be undone | Yes | Not applicable | No | | `mounted` afterwards | Still true | True | False | | Typical work | Unlink from other elements | Reacquire what deactivate released | Release all resources | | `super` call | Last | First | Last | ## In practice Most widgets override neither `deactivate` nor `activate`; they subscribe in `initState` and release in `dispose`. The two-stage model matters when you debug a State that survived a move, when you see a lookup assertion in `dispose`, or when someone proposes releasing resources early in `deactivate`.

  • How do you use an ancestor such as a ScaffoldMessenger safely in dispose?
    Look it up in `didChangeDependencies`, while the element is active, and store it in a field, for example `_messenger = ScaffoldMessenger.of(context)`. In `dispose`, use the stored reference. Calling `ScaffoldMessenger.of(context)` in `dispose` trips the debug assertion "Looking up a deactivated widget's ancestor is unsafe.", whose hint recommends exactly this pattern.

saying these in an interview costs you the question

  • deactivate is the last callback a State ever receives
  • activate runs on first insertion, just before initState
  • A disposed State is cached and reused by a later identical widget
  • Ancestor lookups are safe in dispose because context still exists