A stateful widget is not a one-shot function — it is born, it updates, and it dies, and Flutter calls a specific method at each of those moments. Knowing which method fires when is the difference between code that leaks timers and crashes with "setState called after dispose" and code that cleans up after itself. This tutorial walks the State lifecycle in order — initState, didChangeDependencies, didUpdateWidget, build, deactivate and dispose — explains exactly what belongs in each, and points out the mistakes that bite almost everyone at least once.

The Complete Flutter Guide: Build Android, iOS and Web apps
Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.
Enrol nowEvery snippet below is paste-ready against current stable Flutter. The lifecycle lives inside a State object, so this all assumes you are working with a StatefulWidget rather than a stateless one.
We'll list the lifecycle in firing order, then take each method in turn and finish with a cheat sheet of what to put where.
If you'd rather watch the lifecycle traced live with print statements as a widget mounts and unmounts, the channel covers it inside real screens.
The State lifecycle in order
When Flutter inserts a stateful widget into the tree, it runs a fixed sequence. Memorising this order makes the rest of the article obvious.
- createState — Flutter creates the
Stateobject. - initState — one-time setup, runs exactly once.
- didChangeDependencies — runs after
initState, and again when inherited dependencies change. - build — produces the widget tree; runs on every rebuild.
- didUpdateWidget — runs when the parent rebuilds with a new widget config, just before
build. - deactivate — runs when the widget is removed from the tree (possibly temporarily).
- dispose — final cleanup, runs once when the
Stateis gone for good.
Notice that build sits in the middle and runs many times, while initState and dispose bracket the whole life and run exactly once each. That symmetry — set up in initState, tear down in dispose — is the mental model to hold.
initState
initState is the very first method you control, and it runs once when the State is created. It is where you create objects the widget owns for its whole life: controllers, subscriptions, and the initial values of your state fields.
@override
void initState() {
super.initState(); // always call first
_controller = TextEditingController();
_animation = AnimationController(vsync: this, duration: _kDur);
_counter = widget.initialCount; // read the widget's config here
}
Two rules matter. Call super.initState() first, before your own code. And do not perform InheritedWidget lookups here — Theme.of(context), MediaQuery.of(context) or a Provider read can fail or return stale values, because the element is not yet fully wired into the tree. That is precisely why context is unsafe in initState; those lookups belong in the next method.
didChangeDependencies
didChangeDependencies runs immediately after initState on first build, and then again every time an InheritedWidget this State depends on changes. That makes it the correct home for reading inherited values and reacting when they change.
@override
void didChangeDependencies() {
super.didChangeDependencies();
final locale = Localizations.localeOf(context);
_formatter = DateFormat.yMMMd(locale.toString());
}
If the locale or theme later changes, this method fires again and your formatter updates. Because it can run more than once, keep it cheap and idempotent — do not kick off a fresh network request here unless you have guarded against repeats.

Master controllers, animations and cleanup
The Complete Flutter Guide builds real screens with the full lifecycle — setting up in initState and tearing down in dispose — the right way.
Enrol nowdidUpdateWidget
A State object survives when its parent rebuilds and hands it a new widget instance of the same type. When that happens, Flutter calls didUpdateWidget with the previous widget, so you can react to changed configuration — for example, restarting a subscription keyed to a value the parent passed down.
@override
void didUpdateWidget(covariant ProfileView oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.userId != widget.userId) {
_subscription.cancel();
_subscription = _listenTo(widget.userId);
}
}
Always compare oldWidget against the current widget before acting; this method runs on every parent rebuild, so unconditional work here is wasteful. This is also why you read widget.someProp in build rather than caching it in initState — the prop can change while the State lives on.
deactivate and dispose
deactivate runs when the widget is removed from the tree. It can be temporary — during a reparent, Flutter may reinsert the same State elsewhere — so it is rarely where you do cleanup. dispose is the real teardown: it runs once, when the State is permanently removed and will never build again.
@override
void dispose() {
_controller.dispose(); // free controllers
_animation.dispose();
_subscription.cancel(); // cancel streams
_timer?.cancel(); // cancel timers
super.dispose(); // always call last
}
Everything you created in initState should be released here. Forgetting to dispose a TextEditingController, an AnimationController, or a StreamSubscription is the classic Flutter memory leak, and it is also what produces "setState called after dispose" when an async callback fires after the widget is gone. Call super.dispose() last.
What to put where
Here is the cheat sheet, condensed: create owned objects and set initial field values in initState; read and react to inherited widgets in didChangeDependencies; respond to changed parent config in didUpdateWidget; describe the UI in build and keep it pure; and release everything in dispose. Keep build free of side effects — no subscriptions, no requests, no controller creation — because it runs constantly.
One more rule for after the widget is alive: any setState from an async callback should be guarded with if (!mounted) return;, because the widget may have been disposed while the work was running. The mounted flag is the lifecycle telling you whether it is still safe to call setState.
Common mistakes
- Looking up context in initState.
Theme.of,MediaQuery.ofand Provider reads belong indidChangeDependencies, notinitState. - Forgetting to dispose. Controllers, subscriptions, timers and listeners must be released in
disposeor they leak. - Doing work in build.
buildruns constantly; never start requests, subscriptions, or controllers there. - Caching widget props in initState. Props can change; read
widget.xinbuildand react indidUpdateWidget. - Calling setState after dispose. Guard async callbacks with
if (!mounted) return;before touching state.
Frequently asked questions
What is the order of the Flutter widget lifecycle methods?
On first creation: createState, then initState, didChangeDependencies, then build. On updates, didUpdateWidget or didChangeDependencies runs before build. On removal, deactivate runs, and finally dispose when the State is gone for good.
What is the difference between initState and didChangeDependencies?
initState runs once and is for setup that needs no context lookups, like creating controllers. didChangeDependencies runs after it and again whenever an inherited dependency changes, so it is where you read Theme.of, MediaQuery.of or a Provider and react to their changes.
What should I put in dispose?
Release anything that would otherwise leak: dispose controllers and FocusNodes, cancel StreamSubscriptions and Timers, and remove listeners you added. Call super.dispose() at the very end. Skipping this causes memory leaks and "setState called after dispose" errors.
Can I call setState in initState?
No, and you do not need to. initState already runs before the first build, so fields you set there are picked up automatically. If you need work after the first frame, schedule it with addPostFrameCallback and guard later setState calls with mounted.
Further reads
Keep going with the tutorials that pair with this guide:
- Flutter Development Guide 2026 — the full Flutter hub.
- Flutter setState Explained — rebuilds during the lifecycle.
- StatelessWidget vs StatefulWidget — where the lifecycle lives.
- Flutter TextField and TextEditingController — dispose controllers correctly.
- Flutter BuildContext Explained — why context is unsafe in initState.
Sources: Flutter documentation — State lifecycle: initState, didChangeDependencies, didUpdateWidget, deactivate, dispose (api.flutter.dev). Verified against current stable Flutter.