Choosing an Approach
The decision, made simple
Section titled “The decision, made simple”You now know the mechanisms. Choosing between them is mostly about scope and complexity, and it follows a short decision path:
flowchart TB
q1{"Does more than one widget
need this state?"} -->|no| ss["setState
(ephemeral / local)"]
q1 -->|yes| q2{"Complex flows,
large app, high testability?"}
q2 -->|no| prov["Provider / Riverpod
(shared app state)"]
q2 -->|yes| bloc["BLoC / Cubit
(unidirectional, testable)"] - One widget cares?
setState. Don’t add a package for a checkbox. - Several widgets share it? Provider or Riverpod. Riverpod for new/larger apps.
- Complex, async-heavy, needs to be testable in isolation? BLoC/Cubit.
There’s no prize for using the heaviest tool. A real app happily mixes all three: setState for local UI bits, a couple of Riverpod providers for shared state, and BLoC only where the complexity truly warrants it.
Lifting state up
Section titled “Lifting state up”When two sibling widgets need the same state, move it to their nearest common ancestor and pass it down — lifting state up. This is the first thing to try before reaching for any shared-state package.
| Situation | Where the state should live |
|---|---|
| One widget uses it | In that widget (setState) |
| Two siblings share it | Lifted to the common parent |
| Many widgets across the tree | A shared store (Provider/Riverpod) |
| App-wide, complex, async | A BLoC/Cubit layer |
Ephemeral vs app state, and layering
Section titled “Ephemeral vs app state, and layering”The single most important habit is classifying state correctly:
- Ephemeral state — a
PageViewindex, a form field, an animation. Lives in aState. Losing it on rebuild is fine. - App state — auth, cart, settings, cached server data. Shared and often persisted. Belongs in a store outside any one widget.
For anything beyond a toy app, layer these responsibilities so they don’t tangle:
- UI layer — widgets. Renders state, sends inputs. No business logic.
- State/logic layer — providers, BLoCs, notifiers. Holds and transitions state.
- Data layer — repositories and services. Talks to APIs, databases, device.
Keeping business logic and data access out of widgets is what makes an app testable and survivable as it grows — regardless of which state package you picked.