← Flutter & Dart

Flutter Fundamentals

What are the lifecycle methods of a `State`, and what is each one for?

Show answer — try answering out loud first

The main ones are: initState (called once when the state is created), didChangeDependencies (when inherited dependencies change), build (every time something needs to be drawn), didUpdateWidget (when the widget is reconfigured), setState (to request a rebuild), and dispose (called once on destruction, to release resources).

The typical lifecycle order of a State:

  1. initState(): runs only once, when it's inserted into the tree. Here you initialize controllers, subscriptions, or initial requests. You can't yet safely use context for InheritedWidget.
  2. didChangeDependencies(): right after initState and every time an InheritedWidget you depend on changes. A good place to read a Provider that can change.
  3. build(): builds the UI. It's called many times; it should be fast and free of side effects.
  4. didUpdateWidget(old): when the parent widget rebuilds and passes a new configuration. Here you compare with oldWidget to react to parameter changes.
  5. setState(): marks the state as "dirty" and schedules a build.
  6. dispose(): runs once, when the widget is removed from the tree for good. Here you release resources: cancel streams, dispose() controllers, etc.
import 'package:flutter/material.dart';

class Clock extends StatefulWidget {
  const Clock({super.key});
  @override
  State<Clock> createState() => _ClockState();
}

class _ClockState extends State<Clock> {
  late final TextEditingController _controller;

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController(); // initialize resources
    print("initState: only once");
  }

  @override
  void didUpdateWidget(covariant Clock oldWidget) {
    super.didUpdateWidget(oldWidget);
    print("The widget was reconfigured");
  }

  @override
  void dispose() {
    _controller.dispose(); // ALWAYS release resources
    print("dispose: final cleanup");
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TextField(controller: _controller);
  }
}

Not releasing resources in dispose(). If you create a TextEditingController, an AnimationController, a Timer, or a subscription to a Stream and don't release them, you create memory leaks and warnings in the console. Rule: everything you initialize in initState (or that has a dispose) must be cleaned up in dispose.

"The cycle starts with initState, which runs only once and where I initialize controllers and subscriptions. Then didChangeDependencies when inherited dependencies change. build is called many times to draw and must be pure and fast. didUpdateWidget runs when the parent passes me a new configuration. setState schedules a rebuild. And dispose runs once at the end, where I release everything I opened to avoid memory leaks. The key rule is that everything I open in initState I close in dispose."

Quick challenge

How many times does initState run during the life of a State?

See answer

Exactly once, when the State is first inserted into the tree. Even if the widget rebuilds many times (calling build repeatedly), initState doesn't run again. Its counterpart, dispose, also runs only once, when the widget is removed from the tree.