setState & the Lifecycle
What setState actually does
Section titled “What setState actually does”setState does not “redraw the screen.” It does one small thing: it runs your callback (which mutates fields), then marks this widget’s element as dirty. On the next frame, Flutter rebuilds every dirty element by calling build again.
class Counter extends StatefulWidget { const Counter({super.key}); @override State<Counter> createState() => _CounterState();}
class _CounterState extends State<Counter> { int _count = 0;
void _increment() { setState(() { _count++; // mutate state INSIDE the callback }); // then Flutter marks this element dirty }
@override Widget build(BuildContext context) { return TextButton( onPressed: _increment, child: Text('count: $_count'), ); }}Two rules fall out of this: mutate state inside the setState callback (so the change and the dirty-marking happen together), and know that setState schedules a rebuild for the next frame — it is not synchronous.
The State lifecycle
Section titled “The State lifecycle”A State object has a well-defined lifecycle. Knowing which hook does what prevents a whole class of bugs.
flowchart TB create["createState()"] --> init["initState() one-time setup"] init --> deps["didChangeDependencies() after inherited deps ready"] deps --> build["build() called on every rebuild"] build --> update["didUpdateWidget() parent rebuilt with new config"] update --> build build --> dispose["dispose() cleanup"]
initState— one-time setup: create controllers, subscribe to streams. Runs once. You cannot usecontextfor inherited widgets here reliably.didChangeDependencies— runs afterinitStateand again whenever an inherited dependency changes. The safe place to readInheritedWidget/Providerdata that setup depends on.build— called on every rebuild. Must be pure and fast: no side effects, no network calls, no subscriptions. Just describe the UI from current state.didUpdateWidget— the parent rebuilt and gave this State a new widget config; react to changed props (e.g. re-subscribe if anidchanged).dispose— clean up: dispose controllers, cancel subscriptions. Forgetting this is the classic Flutter memory leak.
The scope of a rebuild
Section titled “The scope of a rebuild”setState marks this State’s element dirty, so build re-runs and produces a new subtree — but Flutter is smart about it. It diffs the new widget subtree against the old element tree and only touches what changed. Still, the whole build method runs, so a setState high in the tree that rebuilds a huge subtree is a real performance concern.
The practical lesson: push setState as low in the tree as possible. Isolate the changing bit into its own small StatefulWidget so only that tiny subtree rebuilds, not the whole page. (The performance module goes deeper on this.)