State Management Decisions
Choose state management by state scope, not by package popularity.
First Split
| State type | Examples | Start with |
|---|---|---|
| Ephemeral widget state | selected tab, text field focus, expanded panel | StatefulWidget plus setState |
| Screen UI state | loading/error/data for one screen | View model, ValueNotifier, or package-backed notifier |
| App state | login session, theme, cart, synced data, unread counts | App-level state mechanism and repository backing |
| Durable data | local database rows, preferences, cached server data | Repository plus storage package |
Decision Ladder
- Use
setStatefor local visual state. - Use
ValueNotifierorChangeNotifierfor small shared state. - Add Provider when inherited access and dependency wiring become annoying.
- Add Riverpod when you want explicit providers, testable dependency overrides, and less
BuildContextcoupling. - Add Bloc/Cubit when event/state discipline, logs, and predictable flows matter more than boilerplate cost.
Default For This Lab
Do not set a universal package default. For a small solo-dev Flutter starter, start with local state plus simple view models. Add Riverpod only when dependency overrides, async state, or cross-feature shared state become real. Use Bloc only when the app benefits from explicit event/state machinery.
Migration Rule
State can move outward:
setState -> view model -> repository-backed app state -> sync-aware state
If state never crosses a widget boundary, do not promote it.
Gotcha: Global state is easy to add and hard to reason about. Persisted state is even harder because migrations, restore, and privacy enter the design.