Flutter Widget Lifecycle: initState, didChangeDependencies, dispose

Coding Liquids blog cover featuring Sagnik Bhattacharya for the Flutter widget lifecycle guide, with a vertical timeline of State methods from initState to dispose.
Coding Liquids blog cover featuring Sagnik Bhattacharya for the Flutter widget lifecycle guide, with a vertical timeline of State methods from initState to dispose.

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 course thumbnail

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 now

Every 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.

Follow me on Instagram@sagnikteaches

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.

Connect on LinkedInSagnik Bhattacharya

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.

Subscribe on YouTube@codingliquids

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 State object.
  • 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 State is 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.

The Complete Flutter Guide course thumbnail

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 now

didUpdateWidget

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.of and Provider reads belong in didChangeDependencies, not initState.
  • Forgetting to dispose. Controllers, subscriptions, timers and listeners must be released in dispose or they leak.
  • Doing work in build. build runs constantly; never start requests, subscriptions, or controllers there.
  • Caching widget props in initState. Props can change; read widget.x in build and react in didUpdateWidget.
  • 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:

Sources: Flutter documentation — State lifecycle: initState, didChangeDependencies, didUpdateWidget, deactivate, dispose (api.flutter.dev). Verified against current stable Flutter.