| name | mobile-flutter-state-management |
|---|---|
| description | Choose and implement state management in a Flutter app. Covers Riverpod (recommended), Bloc, Provider, and setState. Patterns for async data, code generation, and testing. Use when the user needs to manage state beyond simple widget-local state. |
| standards-version | 1.7.0 |
Use this skill when the user:
- Asks which state management to use in Flutter
- Needs to share state across multiple widgets
- Wants to manage async data (API calls, streams)
- Mentions "Riverpod", "Bloc", "Provider", "setState", "state management", or "Cubit"
- Asks about code generation with
riverpod_generatororfreezed
- Complexity level: simple (local state), moderate (shared state), complex (event-driven architecture)
- Async data (optional): whether the app fetches data from APIs or databases
- Preferred library (optional): Riverpod, Bloc, or Provider (defaults to Riverpod recommendation)
-
Choose the right approach. Decision matrix:
Scenario Recommendation Why Counter, form inputs, toggle setStateNo library needed for widget-local state Shared state across screens Riverpod Simple API, compile-safe, testable Complex event-driven flows Bloc Explicit events/states, great for large teams Legacy codebase Provider Widely used, simpler than Bloc, but being superseded by Riverpod Server state / API caching Riverpod AsyncNotifierBuilt-in loading/error states -
Riverpod (recommended). Install:
flutter pub add flutter_riverpod riverpod_annotation flutter pub add --dev riverpod_generator build_runner
Wrap the app in
ProviderScope:void main() { runApp(const ProviderScope(child: App())); }
Simple state provider:
import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'counter_provider.g.dart'; @riverpod class Counter extends _$Counter { @override int build() => 0; void increment() => state++; void decrement() => state--; }
Run code generation:
dart run build_runner build --delete-conflicting-outputs
Use in a widget:
class CounterScreen extends ConsumerWidget { const CounterScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final count = ref.watch(counterProvider); return Scaffold( body: Center(child: Text('Count: $count')), floatingActionButton: FloatingActionButton( onPressed: () => ref.read(counterProvider.notifier).increment(), child: const Icon(Icons.add), ), ); } }
Async data (API calls):
@riverpod class TodoList extends _$TodoList { @override Future<List<Todo>> build() async { final response = await ref.read(apiClientProvider).getTodos(); return response; } Future<void> addTodo(String title) async { state = const AsyncLoading(); state = await AsyncValue.guard(() async { await ref.read(apiClientProvider).createTodo(title); return ref.read(apiClientProvider).getTodos(); }); } }
Consume async state:
@override Widget build(BuildContext context, WidgetRef ref) { final todosAsync = ref.watch(todoListProvider); return todosAsync.when( data: (todos) => ListView.builder( itemCount: todos.length, itemBuilder: (context, index) => TodoTile(todo: todos[index]), ), loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => Center(child: Text('Error: $error')), ); }
Key Riverpod concepts:
API Use for ref.watch(provider)Rebuild widget when state changes ref.read(provider)Read once without listening (use in callbacks) ref.listen(provider, callback)Side effects (show snackbar, navigate) ref.invalidate(provider)Force a provider to recompute provider.notifierAccess methods on a Notifier class -
Bloc pattern. Install:
flutter pub add flutter_bloc
Define events and states:
// Events sealed class AuthEvent {} class AuthLoginRequested extends AuthEvent { final String email; final String password; AuthLoginRequested({required this.email, required this.password}); } class AuthLogoutRequested extends AuthEvent {} // States sealed class AuthState {} class AuthInitial extends AuthState {} class AuthLoading extends AuthState {} class AuthAuthenticated extends AuthState { final User user; AuthAuthenticated(this.user); } class AuthError extends AuthState { final String message; AuthError(this.message); }
Implement the Bloc:
class AuthBloc extends Bloc<AuthEvent, AuthState> { final AuthRepository _authRepo; AuthBloc(this._authRepo) : super(AuthInitial()) { on<AuthLoginRequested>(_onLogin); on<AuthLogoutRequested>(_onLogout); } Future<void> _onLogin( AuthLoginRequested event, Emitter<AuthState> emit, ) async { emit(AuthLoading()); try { final user = await _authRepo.login(event.email, event.password); emit(AuthAuthenticated(user)); } catch (e) { emit(AuthError(e.toString())); } } Future<void> _onLogout( AuthLogoutRequested event, Emitter<AuthState> emit, ) async { await _authRepo.logout(); emit(AuthInitial()); } }
Use in widgets:
// Provide BlocProvider( create: (context) => AuthBloc(authRepository), child: const App(), ); // Consume BlocBuilder<AuthBloc, AuthState>( builder: (context, state) { return switch (state) { AuthInitial() => const LoginScreen(), AuthLoading() => const LoadingScreen(), AuthAuthenticated(:final user) => HomeScreen(user: user), AuthError(:final message) => ErrorScreen(message: message), }; }, ); // Dispatch events context.read<AuthBloc>().add( AuthLoginRequested(email: email, password: password), );
For simpler cases, use Cubit (Bloc without events):
class ThemeCubit extends Cubit<ThemeMode> { ThemeCubit() : super(ThemeMode.system); void setLight() => emit(ThemeMode.light); void setDark() => emit(ThemeMode.dark); }
-
Provider (legacy). If the codebase already uses Provider:
class CartModel extends ChangeNotifier { final List<Item> _items = []; List<Item> get items => List.unmodifiable(_items); int get totalItems => _items.length; void add(Item item) { _items.add(item); notifyListeners(); } void remove(Item item) { _items.remove(item); notifyListeners(); } } // Provide ChangeNotifierProvider( create: (context) => CartModel(), child: const App(), ); // Consume Consumer<CartModel>( builder: (context, cart, child) { return Text('${cart.totalItems} items'); }, );
Provider works but Riverpod is its spiritual successor with better compile-time safety and testability.
- Riverpod documentation
- riverpod_generator
- flutter_bloc documentation
- Provider documentation
- Flutter state management overview
User: "I need to manage a shopping cart that's shared across screens."
Agent:
- Recommends Riverpod for shared state
- Installs flutter_riverpod and riverpod_generator
- Creates
CartNotifierwith add, remove, clear methods - Wraps app in
ProviderScope - Shows
ref.watch(cartProvider)in the cart screen - Shows
ref.read(cartProvider.notifier).add(item)from a product screen - Adds a badge on the cart icon using
ref.watch(cartProvider).length
| Step | MCP Tool | Description |
|---|---|---|
| Install packages | mobile_installDependency |
Install Riverpod, Bloc, or Provider packages |
| Check build | mobile_checkBuildHealth |
Verify project builds after adding state management |
- Using
ref.readwhereref.watchis needed -ref.readdoes not rebuild the widget. Useref.watchinbuild()andref.readonly in callbacks (onPressed, onTap). - Forgetting
ProviderScope- Without wrapping the app inProviderScope, all Riverpod providers throw at runtime. - Not running
build_runner- After changing a@riverpodannotated class, you must re-rundart run build_runner build. The.g.dartfile must be regenerated. - Mutating state directly in Bloc - Never modify
statedirectly. Alwaysemit()a new state object. States should be immutable. - Overusing global state - Not everything needs to be in a provider. Form inputs, animation controllers, and ephemeral UI state belong in
setStateor local controllers. - Provider vs Riverpod confusion - They are different packages by the same author. Riverpod does NOT use
BuildContextfor dependency lookup, making it more testable. Do not mix them.
- Flutter Project Setup - installing Riverpod during project creation
- Flutter Navigation - auth state in route guards
- Mobile State Management (React Native) - equivalent patterns for Expo/RN