Flutter State Management
Choose, implement, and maintain state management in Flutter apps. Covers the
full decision framework: picking the right approach, setting it up correctly,
and avoiding common anti-patterns.
First question always: "Is this state local to one widget, or shared across
the app?" That single question eliminates most wrong choices.
Decision Framework
| Scenario |
Recommendation |
Reason |
| Async data scoped to one screen |
FutureBuilder or local AsyncNotifier |
No global state needed |
| Shared state across screens, reactive |
Riverpod (AsyncNotifierProvider, NotifierProvider) |
Code gen, DI, testable, compile-safe |
| Complex event-driven flows with audit trails |
Bloc or Cubit |
Explicit transitions, traceable |
| Simple shared state, small app |
ChangeNotifier + ListenableBuilder |
Zero deps, minimal ceremony |
| Server-driven real-time data |
StreamProvider (Riverpod) or StreamBuilder |
Reactive by nature |
| Avoid |
GetX, Redux, MobX |
High ceremony, implicit magic, poor testability |
Riverpod vs Bloc:
- Riverpod → code generation, compile-time safety, built-in DI
- Bloc → explicit event log, strong team expertise required
Riverpod Setup
# pubspec.yaml
dependencies:
flutter_riverpod: ^2.5.0
riverpod_annotation: ^2.3.0
dev_dependencies:
riverpod_generator: ^2.4.0
build_runner: ^2.4.0
Always use @riverpod annotation with riverpod_generator. Hand-writing providers loses compile-time safety.
Wrap the app root with ProviderScope:
void main() => runApp(const ProviderScope(child: MyApp()));
Run code generation:
dart run build_runner watch --delete-conflicting-outputs
Riverpod Provider Types
| Provider |
Use for |
AsyncNotifierProvider |
API calls, anything async that can fail or be pending |
NotifierProvider |
Synchronous mutable state (tab selection, filters, toggles) |
Provider |
Derived/computed values with no mutation |
StreamProvider |
Firestore listeners, WebSocket feeds, Stream<T> |
| Family modifier |
Parameterized providers (e.g., productProvider('id-123')) |
See @references/state-management-snippets.md for full Riverpod code examples (AsyncNotifier, NotifierProvider, StreamProvider, family, testing).
ref.watch vs ref.read
|
ref.watch |
ref.read |
| Where |
Inside build() or provider build() |
Inside callbacks, event handlers, notifier methods |
| Behavior |
Subscribes; rebuilds when value changes |
One-time read; no subscription |
| Wrong usage |
Inside callbacks (creates leak) |
Inside build() (misses updates) |
Lifecycle
autoDispose (default with code gen) — provider destroyed when no listeners remain. Use for screen-scoped data.
keepAlive — opt-in to prevent disposal. Use for app-wide singletons.
- Force refresh:
ref.invalidate(myProvider)
- Use
ref.select((state) => state.field) to rebuild only when a specific field changes (avoids broad rebuilds).
Bloc / Cubit
|
Cubit |
Bloc |
| API |
Method calls emit states |
Explicit Event types map to state transitions |
| Use when |
Forms, toggles, simple screens |
Complex flows needing an audit trail |
| Boilerplate |
Low |
Higher |
# pubspec.yaml
dependencies:
flutter_bloc: ^8.1.0
freezed_annotation: ^2.4.0
dev_dependencies:
bloc_test: ^9.1.0
freezed: ^2.4.0
| Widget |
Use for |
BlocBuilder |
Rebuild subtree when state changes |
BlocListener |
Side effects only (navigation, dialogs, snackbars) |
BlocConsumer |
Both rebuild and side effects |
See @references/state-management-snippets.md for Cubit, Bloc, sealed state (freezed), BlocConsumer, and bloc_test examples.
Anti-Patterns
setState for business logic — use it only for widget-local UI state (animation phase, focus). Never for data from a repository.
- Mixing two state managers in one feature — pick one per feature; mixing creates unpredictable rebuild chains.
- Not handling loading and error states — every async provider must handle all three states: loading, error, data.
ref.read inside build — read does not subscribe; the widget will not rebuild. Use ref.watch in build.
- Holding
BuildContext in providers — providers outlive widgets; capturing context causes memory leaks.
- Global state for widget-local concerns — promote state to the minimal scope that all consumers share.
- Watching too broadly —
ref.watch(provider) on a large object rebuilds on any field change. Use select.
Output Artifacts
No dedicated docs artifact — state management decisions are recorded in:
docs/architecture/architecture_decisions.md (ADR for chosen approach)
docs/architecture/technical_plan.md (tech stack section)
Cross-references
- Decision context:
flutter-architecture skill
- Code examples:
@references/state-management-snippets.md
- Agent:
state-management-specialist
- Error mapping from providers:
@references/dart-error-mapping.md
1---2name: flutter-state-management3description: Use this skill when the user wants to choose, design, or implement state management in a Flutter application. Trigger phrases: "state management Flutter", "Riverpod", "Bloc Flutter", "Cubit", "manage state", "provider vs bloc", "choose state management", "how to handle state in Flutter", "AsyncNotifier", "NotifierProvider", "BlocBuilder", "state architecture".4---56# Flutter State Management78Choose, implement, and maintain state management in Flutter apps. Covers the9full decision framework: picking the right approach, setting it up correctly,10and avoiding common anti-patterns.1112**First question always:** "Is this state local to one widget, or shared across13the app?" That single question eliminates most wrong choices.1415---1617## Decision Framework1819| Scenario | Recommendation | Reason |20|---|---|---|21| Async data scoped to one screen | `FutureBuilder` or local `AsyncNotifier` | No global state needed |22| Shared state across screens, reactive | Riverpod (`AsyncNotifierProvider`, `NotifierProvider`) | Code gen, DI, testable, compile-safe |23| Complex event-driven flows with audit trails | Bloc or Cubit | Explicit transitions, traceable |24| Simple shared state, small app | `ChangeNotifier` + `ListenableBuilder` | Zero deps, minimal ceremony |25| Server-driven real-time data | `StreamProvider` (Riverpod) or `StreamBuilder` | Reactive by nature |26| Avoid | GetX, Redux, MobX | High ceremony, implicit magic, poor testability |2728**Riverpod vs Bloc:**29- Riverpod → code generation, compile-time safety, built-in DI30- Bloc → explicit event log, strong team expertise required3132---3334## Riverpod Setup3536```yaml37# pubspec.yaml38dependencies:39 flutter_riverpod: ^2.5.040 riverpod_annotation: ^2.3.041dev_dependencies:42 riverpod_generator: ^2.4.043 build_runner: ^2.4.044```4546Always use `@riverpod` annotation with `riverpod_generator`. Hand-writing providers loses compile-time safety.4748Wrap the app root with `ProviderScope`:49```dart50void main() => runApp(const ProviderScope(child: MyApp()));51```5253Run code generation:54```bash55dart run build_runner watch --delete-conflicting-outputs56```5758---5960## Riverpod Provider Types6162| Provider | Use for |63|---|---|64| `AsyncNotifierProvider` | API calls, anything async that can fail or be pending |65| `NotifierProvider` | Synchronous mutable state (tab selection, filters, toggles) |66| `Provider` | Derived/computed values with no mutation |67| `StreamProvider` | Firestore listeners, WebSocket feeds, `Stream<T>` |68| Family modifier | Parameterized providers (e.g., `productProvider('id-123')`) |6970See `@references/state-management-snippets.md` for full Riverpod code examples (AsyncNotifier, NotifierProvider, StreamProvider, family, testing).7172---7374## `ref.watch` vs `ref.read`7576| | `ref.watch` | `ref.read` |77|---|---|---|78| Where | Inside `build()` or provider `build()` | Inside callbacks, event handlers, notifier methods |79| Behavior | Subscribes; rebuilds when value changes | One-time read; no subscription |80| Wrong usage | Inside callbacks (creates leak) | Inside `build()` (misses updates) |8182---8384## Lifecycle8586- `autoDispose` (default with code gen) — provider destroyed when no listeners remain. Use for screen-scoped data.87- `keepAlive` — opt-in to prevent disposal. Use for app-wide singletons.88- Force refresh: `ref.invalidate(myProvider)`89- Use `ref.select((state) => state.field)` to rebuild only when a specific field changes (avoids broad rebuilds).9091---9293## Bloc / Cubit9495| | Cubit | Bloc |96|---|---|---|97| API | Method calls emit states | Explicit Event types map to state transitions |98| Use when | Forms, toggles, simple screens | Complex flows needing an audit trail |99| Boilerplate | Low | Higher |100101```yaml102# pubspec.yaml103dependencies:104 flutter_bloc: ^8.1.0105 freezed_annotation: ^2.4.0106dev_dependencies:107 bloc_test: ^9.1.0108 freezed: ^2.4.0109```110111| Widget | Use for |112|---|---|113| `BlocBuilder` | Rebuild subtree when state changes |114| `BlocListener` | Side effects only (navigation, dialogs, snackbars) |115| `BlocConsumer` | Both rebuild and side effects |116117See `@references/state-management-snippets.md` for Cubit, Bloc, sealed state (`freezed`), BlocConsumer, and `bloc_test` examples.118119---120121## Anti-Patterns122123- **`setState` for business logic** — use it only for widget-local UI state (animation phase, focus). Never for data from a repository.124- **Mixing two state managers in one feature** — pick one per feature; mixing creates unpredictable rebuild chains.125- **Not handling loading and error states** — every async provider must handle all three states: loading, error, data.126- **`ref.read` inside `build`** — `read` does not subscribe; the widget will not rebuild. Use `ref.watch` in `build`.127- **Holding `BuildContext` in providers** — providers outlive widgets; capturing context causes memory leaks.128- **Global state for widget-local concerns** — promote state to the minimal scope that all consumers share.129- **Watching too broadly** — `ref.watch(provider)` on a large object rebuilds on any field change. Use `select`.130131---132133## Output Artifacts134135No dedicated docs artifact — state management decisions are recorded in:136- `docs/architecture/architecture_decisions.md` (ADR for chosen approach)137- `docs/architecture/technical_plan.md` (tech stack section)138139---140141## Cross-references142143- Decision context: `flutter-architecture` skill144- Code examples: `@references/state-management-snippets.md`145- Agent: `state-management-specialist`146- Error mapping from providers: `@references/dart-error-mapping.md`