Flutter Stateful Widget Lifecycle

Summary: in this tutorial, you’ll explore the flutter stateful widget lifecycle and what you can do in each phase.

A StatefulWidget is a Flutter widget that can change its UI over time because it remembers some data called state.

When that state changes (for example, a counter number), Flutter automatically rebuilds the UI to show the new value.

You use a StatefulWidget when your screen needs to update in response to user actions or events.

Lifecycle flow (simplified) #


createState()
   ↓
initState()          ← called once
   ↓
didChangeDependencies()
   ↓
build()              ← called many times
   ↓
(during updates)
didUpdateWidget()
   ↓
build()
   ↓
(disposal)
dispose()

initState() #

What it is #

Called once when the State object is first created.

@override
void initState() {
  super.initState();
  // one-time initialization
}Code language: JavaScript (javascript)

When it’s called #

  • ✅ Automatically
  • ✅ Exactly one time
  • ✅ Before the first build()

What it’s for #

Use initState for one-time setup:

✅ Good uses:

  • Logging an error once
  • Initializing controllers
  • Starting timers
  • Initial API calls
  • Adding listeners
  • Reading initial widget values

❌ Bad uses:

  • Calling BuildContext APIs that depend on ancestors (Theme, MediaQuery)
  • Calling setState()

Example #

@override
void initState() {
  super.initState();
  analytics.logPageView();
}Code language: CSS (css)

didChangeDependencies() #

What it is #

Called when inherited dependencies change

@override
void didChangeDependencies() {
  super.didChangeDependencies();
}Code language: CSS (css)

When it’s called #

  • ✅ Right after initState
  • ✅ Again when an InheritedWidget changes:
    • Theme
    • MediaQuery
    • Localizations
    • Provider, Riverpod, etc.

What it’s for #

Use this if you depend on context-based data:

✅ Good uses:

  • Reading Theme.of(context)
  • Reading MediaQuery.of(context)
  • Subscribing to providers
  • Localizations

❌ Bad uses:

  • One-time logic unrelated to context
  • Heavy work (can be called multiple times)

Example #

@override

void didChangeDependencies() {
  super.didChangeDependencies();
  final theme = Theme.of(context);
}Code language: JavaScript (javascript)

build(BuildContext context) #

What it is #

Builds the UI.

@override
Widget build(BuildContext context) {
  return …
}Code language: CSS (css)

When it’s called #

  • ✅ Very often
  • ✅ After initState
  • ✅ After setState
  • ✅ When parent rebuilds
  • ✅ When dependencies change
  • ✅ During animations, rotations, etc.

Golden rule #

build() must be pure and side-effect‑free

✅ What to do #

  • Create widgets
  • Read state
  • Read immutable data
  • Choose layouts

❌ What NOT to do #

  • Logging
  • API calls
  • Timers
  • Controllers init
  • State mutations
  • Analytics

Bad example: #

@override

Widget build(BuildContext context) {
  Logger.e(error); // ❌ runs many times
  …
}Code language: JavaScript (javascript)

didUpdateWidget() #

What it is #

Called when the parent rebuilds with a new widget configuration, but State is reused.

@override
void didUpdateWidget(covariant MyWidget oldWidget) {
  super.didUpdateWidget(oldWidget);

}Code language: CSS (css)

When it’s called #

  • ✅ Parent rebuilds
  • ✅ Widget constructor args change
  • ✅ BEFORE build()

This is VERY important #

Widgets change → State does NOT reset

So Flutter gives you this hook to respond.

What it’s for #

Use didUpdateWidget to:

  • Compare old vs new values
  • React to external changes
  • Update controllers
  • Re-log errors when they change

Example (logging safely) #

@override
void didUpdateWidget(covariant PrettyError oldWidget) {
  super.didUpdateWidget(oldWidget);
  if (widget.error != oldWidget.error) {
    Logger.e('${widget.message}: ${widget.error}');
  }
}Code language: JavaScript (javascript)

✅ Only logs when the error actually changes

setState() #

What it is #

Marks the widget dirty so Flutter knows to rebuild.

setState(() {
  counter++;
});

Rules #

  • ✅ Call when state changes
  • ❌ Never in build()
  • ❌ Not in initState() without delay
  • ✅ Safe in async callbacks

dispose() #

What it is #

Called when the State is removed permanently.

@override
void dispose() {
  controller.dispose();
  super.dispose();
}Code language: CSS (css)

When it’s called #

  • ✅ Widget is removed from tree
  • ✅ Navigation pop
  • ✅ List item destroyed

What it’s for #

  • Dispose controllers
  • Cancel timers
  • Remove listeners
  • Close streams

Quick cheat sheet #

MethodCalledUse forAvoid
initStateOnceSetup, logging, controllersContext, setState
didChangeDependenciesManyTheme, MediaQuery, providersHeavy logic
buildVery oftenUI onlySide effects
didUpdateWidgetWhen props changeReact to changesIgnoring oldWidget
disposeOnceCleanupForgetting super.dispose()
Was this tutorial helpful ?