skip to content

In Flutter, in what order does the framework call a State object's lifecycle methods, from createState to dispose?

level: juniorimportance: must knowfreq 80%

answer

  1. created, then attached to a context
  2. initState exactly once
  3. didChangeDependencies right after it
  4. didUpdateWidget on a new configuration
  5. deactivate, then dispose at frame end

basics

~20 s

createState, then the State is mounted, initState runs once, didChangeDependencies follows, then build. Later builds follow setState, didUpdateWidget or a dependency change; on removal deactivate runs, and dispose ends it unless the subtree is reinserted within that frame.

solid answer

~40 s

The framework calls `createState` and attaches the State to its element, so `mounted` becomes true and `context` and `widget` are usable. Then `initState` runs exactly once, `didChangeDependencies` runs immediately after it, and the first `build` follows. From then on `build` can run any number of times: after `setState`, after `didUpdateWidget(oldWidget)` when the parent supplies a new same-type, same-key widget, after `didChangeDependencies` when an inherited widget it depends on changes, and after `reassemble` during a hot reload. When the subtree is removed, `deactivate` runs; if it is reinserted before the frame ends, `activate` and `build` follow, otherwise `dispose` runs, `mounted` becomes false and the State is finished. `initState` and `didUpdateWidget` must call `super` first, `dispose` and `deactivate` must call it last, and none of them may be `async`.

code

dart · 46 lines
dart
import 'package:flutter/material.dart';

class Barometer extends StatefulWidget {
  const Barometer({super.key});

  @override
  State<Barometer> createState() => _BarometerState();
}

class _BarometerState extends State<Barometer> {
  @override
  void initState() {
    super.initState(); // 1. once; widget and context available
    debugPrint('initState');
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies(); // 2. right after initState, then on dependency change
    debugPrint('didChangeDependencies');
  }

  @override
  void didUpdateWidget(Barometer oldWidget) {
    super.didUpdateWidget(oldWidget); // parent passed a new configuration
    debugPrint('didUpdateWidget');
  }

  @override
  void deactivate() {
    debugPrint('deactivate');
    super.deactivate(); // removed; may still be reinserted this frame
  }

  @override
  void dispose() {
    debugPrint('dispose');
    super.dispose(); // last: the State is finished
  }

  @override
  Widget build(BuildContext context) {
    debugPrint('build');
    return const Text('Waiting for sensor');
  }
}

go deeper

for a junior

Recall the order: createState, initState, didChangeDependencies, build, then didUpdateWidget and setState rebuilds, and finally deactivate and dispose.

for a middle

Explain what each callback is for, where the super call goes, why none may be async, and why didChangeDependencies also runs right after initState.

for a senior

Show production judgement: put subscriptions in initState, swap them in didUpdateWidget, release them in dispose, and never rely on dispose for work that must survive a process kill.

for a principal

Define team rules for where resources are acquired and released so lifecycle bugs are caught in review and lint rather than in crash reports.

## Where the callbacks come from A `StatefulWidget` is immutable configuration. Its mutable half, the `State<T>` object, is created by `createState` and lives as long as the widget's **element**, the framework object holding its position in the tree. The framework drives the State through a fixed set of callbacks. In debug builds it even tracks a private lifecycle enum with the stages `created`, `initialized`, `ready` and `defunct` and asserts that the calls arrive in order. ## The sequence 1. **createState**: the element's constructor calls it; the State is attached to the element, so `mounted` is true and `context` and `widget` are available. 2. **initState**: called exactly once per State. One-time setup that needs `widget` or `context`: creating controllers, subscribing to streams. Start with `super.initState()`. 3. **didChangeDependencies**: called immediately after `initState`, and again whenever an inherited widget this State depends on changes or the State moves in the tree while having dependencies. Safe place for inherited-widget lookups. 4. **build**: first build, then any number of rebuilds. 5. **didUpdateWidget(oldWidget)**: when the parent rebuilds with a new widget of the same `runtimeType` and key; the framework has already updated `widget`, receives the old one as the argument, and always calls `build` afterwards. 6. **reassemble**: during development only, on hot reload; a build is guaranteed to follow. 7. **deactivate**: the subtree was removed from the tree. End with `super.deactivate()`. 8. **activate**: only if the subtree is reinserted elsewhere before the end of the same frame, typically a `GlobalKey` graft; `build` follows. Never called on first insertion. 9. **dispose**: the subtree was not reinserted, so the State will never build again. Release resources and end with `super.dispose()`. `mounted` becomes false and the stage is terminal. ## At a glance | Callback | When | Typical work | `super` call | |---|---|---|---| | `initState` | Once, on insertion | Create controllers, subscribe | First | | `didChangeDependencies` | After `initState`; on dependency change | Inherited-widget-driven setup | First | | `didUpdateWidget` | Parent passes a new config | Compare old and new, resubscribe | First | | `reassemble` | Hot reload in development | Rarely anything | Either | | `deactivate` | Removed from the tree | Unlink from other elements | Last | | `activate` | Reinserted in the same frame | Reacquire what `deactivate` released | First | | `dispose` | Removed for good | Cancel, dispose, stop animations | Last | All of these are annotated `@mustCallSuper`, so the analyzer warns if the `super` call is missing, and a debug assertion reports a `dispose` that never reached `super.dispose()`. ## Rules the framework enforces - **No `async` callbacks.** If `initState` or `didUpdateWidget` returns a `Future`, debug builds report 'initState() returned a Future.' or its `didUpdateWidget` counterpart. Start asynchronous work from a separate method without awaiting it. - **No inherited lookups in `initState`.** A dependency-registering lookup there trips a debug assertion; use `didChangeDependencies` or `build`. - **No `setState` after `dispose`.** The `mounted` getter tells you whether the State is still attached. - **`setState` in `didUpdateWidget` is redundant**, because `build` always follows. ## What dispose does not promise The framework documents that `dispose` is **not** invoked on application shutdown or when the operating system kills the process. It runs when the element is unmounted, not when the app goes to the background. App-level pause and resume belong to the app-lifecycle APIs. ## A barometer, walked through A barometer widget shows air pressure from a sensor stream. `initState` subscribes to the stream; the first `didChangeDependencies` and `build` show 'waiting'. Each reading calls `setState`, producing a build. If the parent passes a different stream, `didUpdateWidget` cancels the old subscription and subscribes to the new one. When the user leaves the screen and the route is popped, `deactivate` and then `dispose` run, and `dispose` cancels the subscription before calling `super.dispose()`.

  • Why should super.initState() come first but super.dispose() come last?
    Setup builds on the base class, so the base runs before your code; teardown releases your resources while the State is still in its ready stage, then lets the base mark it defunct. The framework documents both orders, the methods are `@mustCallSuper`, and a debug assertion reports a `dispose` that failed to call `super.dispose()`.
  • Is dispose guaranteed to run when the user closes the app?
    No. The framework states that `dispose` is not invoked on application shutdown or when the process is terminated; it runs only when the element is unmounted. Work that must survive a kill, such as saving a draft, has to happen earlier, for example when the app-lifecycle APIs report that the app is going to the background.

saying these in an interview costs you the question

  • initState runs again every time the widget rebuilds
  • dispose always runs when the user closes the app
  • didChangeDependencies runs only when an inherited widget changes
  • Marking initState async makes the framework wait before building
  • deactivate means the State is gone for good