Managing State with setState
Patterns and pitfalls for local widget state — batching updates, avoiding unnecessary rebuilds, and when setState stops being enough.
阅读需 3 分钟
setState was introduced a few lessons back as the trigger for rebuilding a StatefulWidget. This lesson goes deeper into using it correctly on real, more complex screens.
Only mutate state inside the callback
It's easy to assume setState just "means an update happened somewhere nearby," but Flutter is stricter than that: any field read by build that changes outside of a setState callback won't reliably trigger a rebuild, and any field mutated but not read by build doesn't need setState at all.
int _count = 0;
void _increment() {
setState(() {
_count++; // the mutation belongs inside the callback
});
}You can mutate multiple fields inside one setState call — Flutter only schedules a single rebuild no matter how many fields change inside the callback, which is why it's better to batch related updates into one setState rather than calling it repeatedly:
void _submitForm() {
setState(() {
_isSubmitting = true;
_errorMessage = null;
});
}setState only affects its own widget's subtree
Calling setState inside _CounterState rebuilds Counter and everything below it — not the entire app, and not its siblings or ancestors. This is good for performance, but it's a common source of confusion when a change made in one widget doesn't seem to affect a sibling widget elsewhere on screen — because it can't, on its own.
// Cart badge and product list are siblings.
// Incrementing cart count in one won't update the other
// unless they share state from a common ancestor.
Column(
children: [
ProductList(),
CartBadge(), // has its own separate state
],
)Solving this — sharing state between widgets that aren't parent/child — is exactly what the "Lifting State Up" lesson later in this course covers. For now, the rule to internalize is: setState only reaches down, never sideways or up.
Avoid calling setState after disposal
A State object can be removed from the tree (for example, the user navigates away) while an asynchronous operation it started is still in flight. Calling setState after that point throws an error, because there's no widget left to rebuild:
Future<void> _loadData() async {
final result = await fetchSomething();
if (!mounted) return; // guard against a disposed State
setState(() {
_data = result;
});
}mounted is a property every State object has, and checking it before any setState that follows an await is standard practice — skipping this check is one of the most common runtime errors in real Flutter apps.
Keep setState callbacks synchronous
The callback passed to setState should only contain simple, synchronous field assignments — never await inside it:
// Wrong: setState's callback shouldn't do async work.
setState(() async {
_data = await fetchSomething();
});
// Right: await first, then setState with the plain result.
final data = await fetchSomething();
if (!mounted) return;
setState(() {
_data = data;
});Keeping this boundary clean — do the async work, then synchronously apply the result inside setState — keeps rebuild timing predictable and avoids subtle bugs where Flutter thinks a rebuild finished before your data actually arrived.