Flutter State Management Skill
Purpose
Select, implement, and optimize Flutter state management with the right tool for the context, enforcing minimal rebuilds, testability, and clean separation between UI and business logic.
Scope and authority
This skill is the lead authority for:
- state management solution selection (Riverpod / Bloc / Signals / Hooks)
- provider/notifier architecture and scoping
- rebuild optimization and granular subscription design
- form state management (Formz)
- async state patterns (AsyncNotifier, StreamNotifier)
- migration from Provider / GetX / setState to modern solutions
This skill defers to:
flutter-architecturefor layer boundaries and dependency directionflutter-performancefor measured frame-time regressions caused by rebuildsflutter-securityfor state containing sensitive data
When to use
- Selecting state management solution for new project or feature
- Diagnosing rebuild storms, jank caused by over-broad subscriptions
- Designing provider/notifier tree for complex features
- Migrating from legacy state solutions (Provider, GetX, setState)
- Implementing form state with validation
- Designing async state patterns with loading/error/data
Required inputs
- team familiarity with state solutions
- app complexity (CRUD vs event-heavy vs realtime)
- performance requirements (rebuild sensitivity)
- current state solution (if migrating)
- offline/optimistic update requirements
Decision engine workflow
- Classify state type (ephemeral / shared / server / form).
- Classify app complexity and team context.
- Apply selection matrix (see below).
- Design provider/notifier tree with correct scoping.
- Define rebuild optimization strategy.
- Validate against architecture constraints.
- Return implementation plan with concrete code patterns.
State classification
| Type | Definition | Solution |
|---|---|---|
| Ephemeral | Single widget, not shared | StatefulWidget + setState or flutter_hooks |
| Shared UI | Across widget subtree | Riverpod Notifier or flutter_hooks |
| App state | Persisted across features | Riverpod Notifier + optional persistence |
| Server state | Remote data + cache + sync | Riverpod AsyncNotifier + repository |
| Form state | Multi-field validation | Formz + Riverpod |
| Stream state | Continuous backend events | Riverpod StreamNotifier |
| Event-driven | Complex state machines | flutter_bloc with sealed events |
Selection branching tree
Branch A: default choice
For all new Flutter projects (2026):
Riverpod 3 with Notifier / AsyncNotifier
- Type-safe providers
- Compile-time dependency graph
- No BuildContext dependency
- First-class async support
- Excellent testability (ProviderContainer in tests)
Branch B: when to deviate from Riverpod
flutter_bloc: when the team needs explicit event modeling, complex state machines, audit trail of all state transitions, or existing enterprise BLoC codebase.
Signals (signals_flutter): when UI performance requires zero-rebuild granularity for high-frequency updates (e.g., real-time price tickers, animations, maps).
flutter_hooks: for ephemeral UI-local state (animation controllers, focus nodes, text controllers) — combine with Riverpod for app state.
Formz: for typed multi-field form validation with explicit field states.
Branch C: what to NEVER use
❌ GetX — globally prohibited
❌ Provider (only as migration step toward Riverpod)
❌ Global mutable singletons
❌ setState() for shared app state
❌ InheritedWidget manual implementation (use Riverpod)
Riverpod 3 patterns (required standards)
Provider scoping rules
// ✅ Feature-scoped providers — defined in feature provider files
@riverpod
class AuthNotifier extends _$AuthNotifier {
@override
AuthState build() => const AuthState.initial();
}
// ✅ Repository providers — singleton per app lifecycle
@riverpod
AuthRepository authRepository(Ref ref) {
return AuthRepositoryImpl(
remoteDataSource: ref.watch(authRemoteDataSourceProvider),
localDataSource: ref.watch(authLocalDataSourceProvider),
);
}
// ❌ Never define providers inside widget build() methods
// ❌ Never use ProviderScope.override without tests
Granular subscription (mandatory for performance)
// ✅ Use select() to subscribe to specific fields only
final userName = ref.watch(userProvider.select((user) => user.name));
// ❌ Never watch entire provider when only one field needed
final user = ref.watch(userProvider); // rebuilds on ALL user changes
AsyncNotifier pattern (server state)
@riverpod
class ProductList extends _$ProductList {
@override
Future<List<Product>> build() async {
return ref.watch(productRepositoryProvider).getProducts();
}
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() => ref.read(productRepositoryProvider).getProducts(),
);
}
}
Rebuild optimization rules
- Use
select()for any watch that needs only part of a larger state object. - Use
Consumerwidgets to isolate rebuild scope inside larger widget trees. - Use
constconstructors everywhere — const widgets never rebuild unnecessarily. - Avoid watching in parent widgets when only child widgets need the state.
- Use
ref.listen()for side effects (navigation, dialogs) — notref.watch(). - Use
ref.read()inside callbacks (onPressed) — neverref.watch().
Migration patterns
Provider → Riverpod migration
- Add
riverpod+flutter_riverpodto pubspec.yaml alongsideprovider. - Wrap
MaterialAppwithProviderScope(replacesMultiProvider). - Migrate one feature at a time: create Riverpod provider, remove Provider equivalent.
- Replace
Consumer/context.watch()withConsumerWidget/ref.watch(). - Migrate repositories and services to Riverpod providers last.
- Remove
providerpackage when all migrations complete.
GetX → Riverpod migration
- Identify all
GetxControllerclasses → migrate toNotifier/AsyncNotifier. - Replace
Get.find<>()→ Riverpodref.read(provider). - Replace
Obx(()=>)→ConsumerWidget+ref.watch(). - Replace
Get.to()/Get.toNamed()→go_routerroutes. - Remove
GetMaterialApp→ useMaterialApp.router. - Remove
GetXpackage entirely.
Anti-pattern detection
ref.watch()inside callbacks or event handlers (useref.read())- Providers defined inside widget
build()methods - Watching entire state objects when only one field is needed
setState()for state that crosses widget boundaries- Using
BuildContextinside Notifiers - Async operations without
mountedcheck (in widget callbacks) - Multiple providers for the same domain with no clear boundary
- GetX controllers used anywhere
Uncertainty protocol
High(≥ 0.80): requirements clear, solution deterministicMedium(0.60–0.79): team familiarity or migration complexity unclearLow(< 0.60): conflicting requirements or unknown existing codebase size
Cross-skill handoff payload
Use the standard payload from ../../AGENTS.md.
Set requesting_skill to flutter-state-management.
Output contract
Follow global section order from ../../AGENTS.md. Also include:
State classificationRecommended solution with justificationProvider/notifier tree designRebuild optimization strategyMigration plan (if applicable)Test strategy for state
Related resources
references/riverpod-patterns.mdreferences/bloc-patterns.mdreferences/state-selection-guide.mdtemplates/notifier.darttemplates/async-notifier.darttemplates/bloc-feature.dart