Flutter setState Explained: How StatefulWidget Rebuilds Work

Coding Liquids blog cover featuring Sagnik Bhattacharya for the Flutter setState guide, with a StatefulWidget rebuild cycle diagram, a counter incrementing, and a build method re-running.
Coding Liquids blog cover featuring Sagnik Bhattacharya for the Flutter setState guide, with a StatefulWidget rebuild cycle diagram, a counter incrementing, and a build method re-running.

setState is the first piece of state management every Flutter developer meets, and it is widely misunderstood. It does not redraw the screen, it does not update one widget, and the closure you pass it is not where the magic happens. This tutorial explains what setState actually does, walks the rebuild cycle step by step, shows what belongs inside the closure and why the whole build method re-runs, and covers the classic mistakes — calling it in build, calling it after dispose — with runnable code.

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

Understanding this one method clears up a surprising amount of confusion about why widgets do — and do not — update. It only applies to a StatefulWidget, since a stateless one has no mutable state to change.

Follow me on Instagram@sagnikteaches

We'll use the humble counter as our running example, because it shows every part of the cycle in a few lines.

Connect on LinkedInSagnik Bhattacharya

If you'd rather watch the rebuild cycle traced live with the Flutter inspector, the channel covers exactly that.

Subscribe on YouTube@codingliquids

What setState actually does

A StatefulWidget stores its mutable data in a separate State object. When that data changes, the framework needs to know so it can refresh the UI. setState is how you tell it. Concretely, it runs the closure you pass — your mutation — and then marks this State object's element as dirty by calling markNeedsBuild internally.

void _increment() {
  setState(() {
    _count++;
  });
}

That dirty mark does not redraw anything immediately. It schedules the element to be rebuilt on the next frame. So setState is best read as "I changed some state; please rebuild this widget when you get a chance" — not "redraw now".

A useful detail: the framework batches these requests. If you call setState several times before the next frame, you do not get several rebuilds — the element is already marked dirty, and Flutter rebuilds it once when the frame is scheduled. That is why a tight burst of state changes still produces a single, efficient repaint. The return value of your closure is ignored, which is also why the closure must not be async: an async closure returns a Future the framework never awaits, so your changes would land after the rebuild has already run.

The rebuild cycle, step by step

Here is the full sequence when you tap a button wired to the counter:

// 1. User taps -> _increment() runs.
// 2. setState runs your closure: _count becomes 1.
// 3. setState calls markNeedsBuild: this element is now dirty.
// 4. On the next frame, Flutter rebuilds dirty elements.
// 5. build() re-runs and returns a new widget tree using _count.
// 6. Flutter diffs new vs old and repaints only what changed.

Step six is the important one. Even though build runs fully, Flutter compares the freshly returned widgets against the previous tree and only updates the underlying render objects that genuinely differ. That is why a counter app stays smooth even though build runs on every tap — this diffing is the same mechanism the widget lifecycle drives at every stage.

It helps to separate three trees in your head. The widget tree is the lightweight description you return from build; it is thrown away and rebuilt freely. The element tree is the longer-lived bookkeeping layer that holds your State objects and decides what changed. The render tree does the actual layout and painting. setState only marks an element dirty; the framework walks down from there, rebuilds the affected widgets, reconciles them against the elements, and updates only the render objects that need it. Understanding that the cheap part (describing widgets) is separate from the expensive part (painting pixels) is what makes the "the whole build re-runs" fact stop being alarming.

What goes inside the closure (and what does not)

The closure exists for one reason: to make sure your mutation happens before the rebuild is scheduled. Put the state change inside it. You do not need anything else there.

// Good: mutate inside, keep it tiny.
setState(() {
  _count++;
});

// Also fine: mutate first, then an empty setState to signal.
_items.add(newItem);
setState(() {});

What should not go inside the closure is slow or asynchronous work. Do the network call or computation first, await it, and only call setState with the finished result. The closure is synchronous and runs on the UI thread, so keep it to plain field assignments.

// Bad: awaiting inside setState — the closure cannot be async.
// setState(() async { _data = await fetch(); });  // don't

// Good: await first, then a quick synchronous setState.
final data = await fetch();
if (!mounted) return;
setState(() => _data = data);
The Complete Flutter Guide course thumbnail

Master Flutter state, from setState to Riverpod

The Complete Flutter Guide takes you from the rebuild cycle to production state management in real apps.

Enrol now

setState re-runs the whole build method

This trips up newcomers: setState does not update only the changed widget. It marks the entire State dirty, so the whole build method runs again from top to bottom. If your build creates ten widgets, all ten are described again on the next frame.

@override
Widget build(BuildContext context) {
  // This entire method re-runs on every setState.
  return Column(
    children: [
      Text('Count: $_count'),     // changes
      const ExpensiveHeader(),    // does not change
      ElevatedButton(
        onPressed: _increment,
        child: const Text('Add'),
      ),
    ],
  );
}

That is usually fine — describing widgets is cheap, and Flutter only repaints the differences. Marking the unchanging parts const, like ExpensiveHeader above, lets the framework skip rebuilding them entirely, because a const widget is identical across frames. The framework can compare it by identity and skip straight past it, so liberal use of const on static subtrees is one of the easiest wins in any Flutter app.

This also explains a subtle point about where you split your widgets. A const child created in the parent's build is still re-evaluated by the parent — only its instance is reused. If you want a subtree to be genuinely insulated from a parent's setState, pull it into its own widget so it sits as a separate element. The parent rebuilding will then reuse that child element untouched, because nothing about its configuration changed.

Scope rebuilds to stay fast

When a rebuild really is expensive, the answer is to shrink what setState touches. Push the changing state down into its own small widget, so only that widget rebuilds instead of a large parent. Better still, for a single changing value, use a ValueNotifier with ValueListenableBuilder so the rebuild is confined to the listener.

final _count = ValueNotifier<int>(0);

ValueListenableBuilder<int>(
  valueListenable: _count,
  builder: (context, value, _) => Text('Count: $value'),
);
// Elsewhere: _count.value++;  // rebuilds only the Text

For app-wide state shared across many screens, that is the point where you graduate from setState to a proper solution — covered in the state management guide. Start with setState, reach for more only when you feel the friction.

A practical order of escalation keeps this simple. Reach for setState first; it is built in and perfect for state that belongs to a single widget. When one value needs to update a focused part of the UI without a full rebuild, move it to a ValueNotifier. When several widgets, or whole screens, need to read and change the same state, that is the signal to lift it out into a dedicated solution like Provider, Riverpod, or BLoC. Jumping straight to a heavyweight library for a single toggle is over-engineering; clinging to setState for cross-screen state leads to tangled callback chains. The skill is matching the tool to the scope.

Common mistakes

  • Calling setState inside build. That schedules a rebuild during a rebuild and loops; mutate in callbacks or lifecycle methods, never in build.
  • Calling setState in initState. The first build already sees your initial state; just assign the value directly instead.
  • setState() called after dispose(). Guard async callbacks with if (!mounted) return; and cancel timers and streams in dispose.
  • Mutating state outside the closure. Change the field inside setState (or call an empty setState after) so the rebuild is actually scheduled.
  • Doing slow work in the closure. Keep the closure to synchronous field assignments; await network or heavy work beforehand.

Frequently asked questions

What does setState do in Flutter?

It tells the framework that this State object's data has changed and the widget must be rebuilt. It runs your closure, marks the element dirty, and schedules build to run again on the next frame — it does not redraw the screen by itself.

Why does my whole widget rebuild on setState?

setState marks the entire State dirty, so the whole build method re-runs. That is expected and usually cheap, because Flutter diffs the new tree against the old and repaints only what changed; scope rebuilds down if one is genuinely expensive.

Can I call setState in initState?

You should not, and you do not need to. initState runs before the first build, so any state you set there is already reflected. Calling setState there is redundant and asserts in debug mode — assign the value directly.

Why do I get setState() called after dispose()?

An async task, timer, or stream completed after the widget left the tree and called setState on a disposed State. Guard with if (!mounted) return; before setState, and cancel timers, streams, and subscriptions in dispose.

Further reads

Keep going with the tutorials that pair with this guide:

Sources: Flutter documentation — State.setState, StatefulWidget, and State (api.flutter.dev). Verified against current stable Flutter.