DevAtlas

Learn

  • Dashboard
  • Learning Paths
  • All Categories
  • Topics

Practice

  • Interview Questions
  • Flashcards
  • Quizzes
  • Study Plans

Personal

  • Bookmarks
  • Notes
  • Progress
  • PDF Library
  • Settings
  • About
  1. Topics
  2. Flutter Widget Lifecycle
0%
Flutter

Flutter Widget Lifecycle

A StatefulWidget’s State object is created, initialized, built, updated, and finally disposed. Knowing the order lets you set things up and tear them down at exactly the right moment.

Intermediate25 min study11 min read
Export PDF

Prerequisites: StatelessWidget vs StatefulWidget

Learning objectives

  • List the State lifecycle callbacks in the order Flutter invokes them.
  • Choose the right callback for setup and teardown work — subscribe in initState, clean up in dispose.
  • Explain why context-dependent work belongs in didChangeDependencies, not initState.
  • Use the mounted flag and dispose correctly to avoid memory leaks and setState-after-dispose errors.

Simple explanation

Think of a StatefulWidget’s State object as an employee with a shift. When the shift starts they arrive and set up their desk (initState). Once settled, they check the building’s notice board for anything relevant to their job (didChangeDependencies). Then they do the actual work over and over throughout the day (build). If management reshuffles their assignment, they adjust (didUpdateWidget). When something changes that affects their work, they redo the task (setState → build again). At the end of the shift they pack up, return the keys, and switch off the lights (dispose). Skip the pack-up and you leave the lights burning all night — that is a memory leak.

A StatelessWidget has no shift and no desk: it is just a fixed poster on the wall. Only StatefulWidget — through its State — has this lifecycle.

Technical explanation

A StatefulWidget itself is immutable and cheap to recreate. The mutable, long-lived part is its State object, and the framework drives it through a fixed sequence of callbacks:

  1. createState() — called by the framework once when the widget is inserted into the tree. It returns your State instance. This is the only method on the StatefulWidget class itself; everything below lives on State.
  2. initState() — called exactly once, right after the State is created. Call super.initState() first. Initialize controllers, subscribe to streams, and set up listeners here. You cannot safely use InheritedWidget dependencies (e.g. Theme.of(context), MediaQuery.of(context)) yet, because they are not wired up at this point.
  3. didChangeDependencies() — called immediately after initState, and again every time an InheritedWidget this State depends on changes. This is the correct place for context-dependent initialization, such as reading an inherited value or re-subscribing when a provider higher up changes.
  4. build(BuildContext context) — called to describe the UI. It runs many times: after initState/didChangeDependencies, after every setState, after didUpdateWidget, and whenever an ancestor rebuilds. Keep it pure and fast — no side effects, no subscriptions, no async work.
  5. didUpdateWidget(covariant oldWidget) — called when the parent rebuilds and supplies a new widget configuration of the same type at the same position. Flutter reuses the same State but hands you a new widget. Compare oldWidget with widget and, if something like a controller or subscription depends on the changed config, tear down the old one and set up the new one.
  6. setState(fn) — you call this (not the framework) to signal that state has changed. It marks the element dirty and schedules a rebuild, so build runs again. Never call it inside build, and never after dispose.
  7. deactivate() — called when the State is removed from the tree, possibly temporarily (e.g. when a subtree is moved via a GlobalKey). If it is not reinserted in the same frame, dispose follows.
  8. dispose() — called once when the State is permanently removed. Clean up everything you created: dispose controllers, cancel StreamSubscriptions, remove listeners, close sinks. Call super.dispose() last.

The mounted flag is true between initState and dispose. After an async gap, guard with if (!mounted) return; before calling setState, because the widget may already be gone.

Why it matters

Getting the lifecycle right is the difference between an app that runs smoothly and one that leaks memory, throws setState() called after dispose(), or reads context before it is ready. Every animation, stream, timer, text field, and scroll controller you create has to be released at the correct time. The lifecycle tells you exactly when to create and when to release — so resources are held for precisely as long as the widget is on screen and not a millisecond longer.

How it works

For a State that lives, updates, and dies, the framework walks the callbacks like this:

  1. Parent builds → createState() → initState() → didChangeDependencies() → build() — the widget is now on screen.
  2. User interaction or new data → setState() → build() — repeated as often as needed.
  3. An inherited dependency changes → didChangeDependencies() → build().
  4. Parent rebuilds with a new config → didUpdateWidget(oldWidget) → build().
  5. Widget removed from the tree → deactivate() → dispose() — resources released, State gone.

Real-world analogy

Renting an apartment maps neatly onto the lifecycle. initState is signing the lease and moving your furniture in — you do it once. didChangeDependencies is checking the building rules, which you re-check whenever management updates them. build is decorating the room, which you might redo many times. didUpdateWidget is the landlord swapping your fridge for a new model — same apartment, new appliance to wire up. dispose is handing back the keys and cancelling the utilities. Forget to cancel the electricity and you keep paying for an empty flat — the memory-leak equivalent.

Important terminology

  • State object — the mutable, persistent companion of a StatefulWidget that survives rebuilds and owns the lifecycle callbacks.
  • mounted — a boolean on State that is true while the State is in the tree (between initState and dispose); guard setState after async work with it.
  • setState — the method that marks the State dirty and schedules a rebuild; the only sanctioned way to change UI-affecting state.
  • dispose — the final callback where you release every resource the State acquired, preventing memory leaks.

Key definitions

State object
The mutable, long-lived companion of a StatefulWidget that persists across rebuilds and hosts the lifecycle callbacks.
initState
Called once when the State is created; the place to initialize controllers and subscribe to streams. Call super.initState() first.
didChangeDependencies
Runs after initState and again whenever an InheritedWidget dependency changes; the right place for context-dependent setup.
dispose
The final callback where you release resources — dispose controllers, cancel subscriptions, remove listeners. Call super.dispose() last.

Must memorize

Must memorize

  • Order: createState → initState → didChangeDependencies → build → didUpdateWidget → setState → deactivate → dispose.
  • initState runs once (super.initState first); dispose runs once (super.dispose last). Subscribe in one, clean up in the other.
  • build can run many times and must stay pure — never do subscriptions or side effects there.
  • context-dependent work belongs in didChangeDependencies, not initState; guard async setState with if (!mounted) return.

Real-world example

A live chat screen with a message stream

A chat screen subscribes to a WebSocket message stream in initState and calls setState as messages arrive. When the user navigates away, dispose cancels the StreamSubscription. Without that cancellation the socket stays open and the closure keeps calling setState on a dead State, throwing errors and leaking memory.

A loading spinner driven by an AnimationController

A spinner creates an AnimationController (with a TickerProvider) in initState and repeats it. In dispose the controller is disposed so its ticker stops. Skipping dispose keeps the ticker alive, wasting frames and battery even after the screen is closed.

Code example

pulsing_dot.dart
class PulsingDot extends StatefulWidget {
  const PulsingDot({super.key});

  @override
  State<PulsingDot> createState() => _PulsingDotState();
}

class _PulsingDotState extends State<PulsingDot>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState(); // always first
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    )..repeat(reverse: true);
  }

  @override
  Widget build(BuildContext context) {
    // build stays pure — it only describes UI.
    return FadeTransition(
      opacity: _controller,
      child: const CircleAvatar(radius: 12),
    );
  }

  @override
  void dispose() {
    _controller.dispose(); // release the ticker
    super.dispose(); // always last
  }
}

An AnimationController created in initState and released in dispose.

message_list.dart
class MessageList extends StatefulWidget {
  const MessageList({super.key, required this.stream});

  final Stream<String> stream;

  @override
  State<MessageList> createState() => _MessageListState();
}

class _MessageListState extends State<MessageList> {
  final List<String> _messages = [];
  StreamSubscription<String>? _sub;

  @override
  void initState() {
    super.initState();
    _subscribe(widget.stream);
  }

  @override
  void didUpdateWidget(covariant MessageList oldWidget) {
    super.didUpdateWidget(oldWidget);
    // Parent gave us a new stream — swap the subscription.
    if (oldWidget.stream != widget.stream) {
      _sub?.cancel();
      _subscribe(widget.stream);
    }
  }

  void _subscribe(Stream<String> stream) {
    _sub = stream.listen((msg) {
      if (!mounted) return; // widget may be gone after an async event
      setState(() => _messages.add(msg));
    });
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [for (final m in _messages) Text(m)],
    );
  }

  @override
  void dispose() {
    _sub?.cancel(); // cancel the subscription to avoid a leak
    super.dispose();
  }
}

Subscribing to a stream in initState, guarding setState with mounted, and cancelling in dispose.

did_change_dependencies.dart
class ThemedBanner extends StatefulWidget {
  const ThemedBanner({super.key});

  @override
  State<ThemedBanner> createState() => _ThemedBannerState();
}

class _ThemedBannerState extends State<ThemedBanner> {
  late Color _accent;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // Safe here: inherited widgets are wired up.
    // Also re-runs automatically when the Theme changes.
    _accent = Theme.of(context).colorScheme.primary;
  }

  @override
  Widget build(BuildContext context) {
    return Container(color: _accent, height: 48);
  }
}

Reading an InheritedWidget dependency in didChangeDependencies, not initState.

Common mistakes

Mistake

Forgetting to release resources in dispose — leaving AnimationControllers, StreamSubscriptions, or listeners alive.

Do this instead

Every controller/subscription/listener created in initState must be disposed or cancelled in dispose, or it leaks memory and keeps firing callbacks.

Mistake

Calling setState after an await when the widget may be gone, or using Theme.of(context) in initState.

Do this instead

Guard async updates with if (!mounted) return; before setState, and move context-dependent reads to didChangeDependencies or build.

Interview questions

Quiz

Question 1 of 40 ✓
Which callback runs exactly once and is the right place to subscribe to a stream?
Select an answer to continue.

Flashcards

Card 1 of 3

Show answer · Space

Summary

A StatefulWidget’s State runs a fixed lifecycle: createState → initState → didChangeDependencies → build → didUpdateWidget → (setState triggers rebuilds) → deactivate → dispose. Initialize and subscribe in initState, read context-dependent values in didChangeDependencies, keep build pure, react to new configs in didUpdateWidget, and release every resource in dispose. Use the mounted flag to guard setState after async work.

References

  • Flutter API — State class
  • Flutter API — StatefulWidget class

Related topics

StatelessWidget vs StatefulWidgetBuildContext in Flutter
Next
StatelessWidget vs StatefulWidget

On this page

  • Learning objectives
  • Simple explanation
  • Technical explanation
  • Why it matters
  • How it works
  • Real-world analogy
  • Important terminology
  • Key definitions
  • Must memorize
  • Real-world example
  • Code example
  • Common mistakes
  • Interview questions
  • Quiz
  • Flashcards
  • Summary