skip to content

In Flutter, what does calling setState on a State object actually do, and when does build() run afterwards?

level: juniorimportance: must knowfreq 76%

answer

  1. callback runs synchronously first
  2. then Element.markNeedsBuild
  3. dirty list and a scheduled frame
  4. idempotent within one frame
  5. an async callback trips an assertion

basics

~20 s

setState runs its callback synchronously, then calls markNeedsBuild on the State's element, which marks it dirty and schedules a frame. build() runs during that next frame, not inside the setState call, and several calls before the frame produce one build.

solid answer

~40 s

The implementation is small: `setState(fn)` calls `fn` synchronously and then `markNeedsBuild()` on the State's element. That marks the element dirty and hands it to the `BuildOwner` through `scheduleBuildFor`, which asks for a new frame; in that frame's build phase the framework rebuilds dirty elements, which runs this State's `build`. So the field changes immediately but the screen changes later, and three calls before a frame still yield one build, because an element that is already dirty returns early. The callback must be synchronous: if it returns a `Future`, for example an `async` closure, a debug assertion reports it, so await first and then call `setState`. Calling it after `dispose()` is an error, and so is marking an unrelated widget dirty while the framework is already building.

code

dart · 15 lines
dart
// Inside _TipCalculatorState
void _onTipChanged(double value) {
  setState(() => _tipPercent = value); // runs now; build runs next frame
}

Future<void> _loadSavedTip() async {
  final saved = await widget.settings.readTipPercent(); // async work first
  if (!mounted) return;
  setState(() => _tipPercent = saved); // then a synchronous update
}

// Wrong: the closure returns a Future, which a debug assertion reports.
// setState(() async {
//   _tipPercent = await widget.settings.readTipPercent();
// });

go deeper

for a junior

Recall that setState runs your callback, marks the widget dirty and that build runs in the next frame, not immediately.

for a middle

Explain markNeedsBuild and scheduleBuildFor, why calls are idempotent per frame, and why an async callback or a call during build is rejected.

for a senior

Diagnose setState misuse in real code: calls after dispose from timers or awaits, redundant calls in tight loops, and state updated during build instead of in handlers.

for a principal

Decide when setState's local, per-element model stops scaling for a screen and define when a team moves such state into a listenable or a state library.

## What the method does, step by step `setState` is a method on `State<T>`, the object a `StatefulWidget` creates through `createState`. The framework's own source describes its implementation as trivial, and it is: 1. In debug builds, assert that the `State` has not been disposed and is not still in its constructor. 2. Call the callback you passed, **synchronously**, right there. 3. In debug builds, assert that the callback did not return a `Future`. 4. Call `markNeedsBuild()` on the `State`'s element. There is no comparison of old and new values and no immediate rebuild. The State's fields change the moment the callback runs, but nothing on screen changes yet. ## From dirty to built `Element.markNeedsBuild` sets the element's `dirty` flag and calls `BuildOwner.scheduleBuildFor(element)`, which records the element in the list of dirty elements and, the first time in a frame, asks the binding to schedule a frame. When that frame runs, its build phase rebuilds the dirty elements, which calls the State's `build`. The resulting widgets are compared with the previous ones and the element tree below is updated; layout and paint follow in the same frame. The exact ordering inside a frame belongs to the frame-pipeline topic; for this question the key point is **one build per dirty element per frame**. Because `markNeedsBuild` returns early when the element is already dirty, the call is **idempotent within a frame**. A slider firing `onChanged` several times between two frames still costs one build. ## Rules the framework enforces | Situation | What happens | |---|---| | Callback returns a `Future` (`async` closure) | Debug assertion: 'setState() callback argument returned a Future.' | | Called after `dispose()` | Debug assertion 'setState() called after dispose()'; in release the element reference is already null, so it still fails | | Called from the `State` constructor | Debug assertion: the new State is already considered dirty | | Marks a non-descendant dirty while the tree is building | Debug assertion: 'setState() or markNeedsBuild() called during build.' | | Called several times before a frame | One build in the next frame | The async rule exists because the framework cannot wait: it marks the element dirty right after the callback returns, so an assignment made after an `await` inside the callback would land after the rebuild was already scheduled. The correct shape is to do the asynchronous work first and then call `setState` with a synchronous assignment, checking `mounted` if the widget may have left the tree while you waited. ## Inside or before the callback Since `setState` simply runs the callback and then marks the element dirty, `_tipPercent = v; setState(() {});` behaves the same as `setState(() => _tipPercent = v);`. The convention is still to mutate inside the callback. The source's design discussion explains why the API takes a callback at all: its first version was a bare `markNeedsBuild`, which early users called 'like a good luck charm' whenever unsure; asking for the change inside a callback made developers think about what actually changed. ## In a tip calculator Each tip slider's `onChanged: (v) => setState(() => _tipPercent = v)` updates one field and marks the calculator's element dirty. In the next frame the calculator's `build` recomputes the total from the new field and returns a `Text` with the new number. Nothing else in the app rebuilds unless it was also marked dirty or receives a changed widget from this build. ## Common misconceptions - `setState` does **not** call `build` before it returns. - It does **not** diff state; any call marks the element dirty, even if the value is unchanged. - It does **not** rebuild the whole app, only the calling State's element and whatever its new build output changes below it. - It is **not** a place for asynchronous work.

  • Does it matter whether you mutate the field inside the setState callback or just before calling it?
    Functionally no: `setState` only runs the callback and then calls `markNeedsBuild`, so `_x = v; setState(() {});` rebuilds exactly the same. Mutating inside the callback is the convention because it keeps the change and the notification together, which is the reason the API takes a callback rather than being a bare `markNeedsBuild`.
  • Why is calling setState from inside build an error?
    While the build phase is running, marking an element that is not a descendant of the widget currently building could be missed, so a debug assertion reports 'setState() or markNeedsBuild() called during build.' Marking a descendant is tolerated because parents build before children. Move the change into an event handler, or defer it with `WidgetsBinding.instance.addPostFrameCallback`.
  • Does setState skip the rebuild when the new value equals the old one?
    No. `setState` never compares values; any call marks the element dirty and its `build` runs in the next frame. If a handler can fire with an unchanged value, compare first and skip the call, because the framework documents that `setState` should only be called when `build` will change its result meaningfully.

saying these in an interview costs you the question

  • setState rebuilds the widget synchronously before it returns
  • setState compares old and new values and skips unchanged ones
  • Marking the setState callback async is fine for network calls
  • Each setState call produces its own separate rebuild
  • setState rebuilds the whole app from the root widget