In Flutter, what does calling setState on a State object actually do, and when does build() run afterwards?
answer
- callback runs synchronously first
- then Element.markNeedsBuild
- dirty list and a scheduled frame
- idempotent within one frame
- an async callback trips an assertion
basics
~20 ssetState 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 sThe 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// 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
Recall that setState runs your callback, marks the widget dirty and that build runs in the next frame, not immediately.
Explain markNeedsBuild and scheduleBuildFor, why calls are idempotent per frame, and why an async callback or a call during build is rejected.
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.
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