prefer_single_setstate
Flags a method in a State subclass that calls setState() more than once. Each call schedules a rebuild, so calling it twice for one logical change rebuilds twice and briefly exposes an intermediate state.
This rule is in the opinionated preset.
See also: State.setState()
class CartView extends StatefulWidget { const CartView({super.key});
@override State<CartView> createState() => _CartViewState();}
class _CartViewState extends State<CartView> { int _itemCount = 0; String _status = 'empty';
void _addItem() { setState(() { _itemCount += 1; }); setState(() { _status = 'has items'; }); }
@override Widget build(BuildContext context) => Text('$_itemCount $_status');}class CartView extends StatefulWidget { const CartView({super.key});
@override State<CartView> createState() => _CartViewState();}
class _CartViewState extends State<CartView> { int _itemCount = 0; String _status = 'empty';
void _addItem() { setState(() { _itemCount += 1; _status = 'has items'; }); }
@override Widget build(BuildContext context) => Text('$_itemCount $_status');}Branches and loops still count
Section titled “Branches and loops still count”Two calls in opposite if branches never both run, but they are still counted — the rule reads the whole method, not the paths through it. Hoist the shared setState around the branch:
// Reportedvoid _toggleBad(bool on) { if (on) { setState(() => _status = 'on'); } else { setState(() => _status = 'off'); }}
// Not reportedvoid _toggleGood(bool on) { setState(() => _status = on ? 'on' : 'off');}A custom state base class
Section titled “A custom state base class”Projects with a state abstraction that does not extend Flutter’s State can opt that base class in:
many_lints: rules: prefer_single_setstate: state_base_classes: [AppState]rules: prefer_single_setstate: state_base_classes: [AppState]Known limitations
Section titled “Known limitations”A closure or nested function stops the search. Calls inside one run later and belong to their own scope, so they are neither counted nor merged:
// Not reported: the second call is inside a callback.void _load() { setState(() => _status = 'loading'); _future.then((_) { setState(() => _status = 'done'); });}The same goes for two calls in different methods — each method is counted on its own.
In an async method, two calls around an await are still reported, even though they genuinely happen in different frames and merging them would change behaviour. Split the work into two methods, or suppress the diagnostic on that line.
Only classes reachable from State (or a class named in state_base_classes) are examined. A plain controller calling a method it happens to have named setState is left alone.
Turning this rule off
Section titled “Turning this rule off”This rule is in the opinionated preset, so it is on with
preset: opinionated, or by name:
rules: prefer_single_setstate: trueTo turn it off again:
rules: prefer_single_setstate: falseTo keep the rule on but skip certain paths, use per-rule exclude.
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
state_base_classes |
list of strings | [] |
Additional non-State base classes whose subclasses should be treated as state classes |
Related rules
Section titled “Related rules”avoid_accessing_other_classes_private_members— Make the underscore mean what everyone reads it as.avoid_commented_out_code— Detect and flag commented-out code.avoid_complex_conditions— Keep boolean conditions within an operand budget.avoid_deep_nesting— Keep control flow within a nesting budget.