In Flutter, in what order does the framework call a State object's lifecycle methods, from createState to dispose?
answer
- created, then attached to a context
- initState exactly once
- didChangeDependencies right after it
- didUpdateWidget on a new configuration
- deactivate, then dispose at frame end
basics
~20 screateState, 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 sThe 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 linesimport '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
Recall the order: createState, initState, didChangeDependencies, build, then didUpdateWidget and setState rebuilds, and finally deactivate and dispose.
Explain what each callback is for, where the super call goes, why none may be async, and why didChangeDependencies also runs right after initState.
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.
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