In Riverpod state management, which provider should I use and when? What are Provider, FutureProvider, NotifierProvider, AsyncNotifierProvider, and StreamProvider? When should I use Riverpod annotations (@riverpod)? What are Mutations in Riverpod 3.0?
Answer
Overview
Riverpod is Flutter's most powerful state management and dependency injection framework. Modern Riverpod (v3.0+) solves two critical problems in Flutter development:
text1. Dependency Injection (DI) 2. State Management
Riverpod 3.0 introduces major improvements:
- Mutations for side effects (POST, PUT, DELETE)
- Unified provider APIs
- Better async lifecycle handling
- Improved code generation
- Retry mechanisms
This guide covers production-level architecture with real-world examples.
Quick Decision Guide
textNeed dependency injection? → Use Provider with @riverpod Need read-only async data (GET)? → Use FutureProvider or AsyncNotifierProvider Need mutable state (local)? → Use NotifierProvider Need async mutable state (API)? → Use AsyncNotifierProvider Need side effects (POST/PUT/DELETE)? → Use Mutation (Riverpod 3.0+) Need realtime streams? → Use StreamProvider or StreamNotifierProvider
Core Concept: Providers
Riverpod revolves around Providers.
A provider is a function or class that:
- Exposes data/state
- Caches values automatically
- Rebuilds UI when state changes
- Enables dependency injection
Providers can represent:
textServices (ApiClient, AuthService) Repositories (UserRepository) Configurations (AppConfig) Async data (User profile, Posts) Business logic (LoginNotifier)
1. Dependency Injection in Riverpod
Key Concept
Providers are Riverpod's Dependency Injection system.
Instead of manually creating instances:
dart// ❌ Without DI - Manual instantiation class UsersService { final repo = UserRepository(); }
With Riverpod:
dart// ✅ With Riverpod DI - Injected UserRepository userRepository(UserRepositoryRef ref) { return UserRepository(); } // Usage final repo = ref.read(userRepositoryProvider);
Benefits:
- Centralized dependency management
- Easy testing (swap implementations)
- Automatic lifecycle management
- Dependencies injected automatically
2. Types of Providers - Complete Guide
Provider Comparison Table
| Provider Type | Purpose | Mutable | Async | Use Case |
|---|---|---|---|---|
| Provider | Dependency Injection | ❌ No | ⚠️ Can be | Inject services, configs |
| FutureProvider | Async read-only data | ❌ No | ✅ Yes | API GET, database queries |
| StreamProvider | Realtime streams | ❌ No | ✅ Yes | Firebase, WebSocket |
| NotifierProvider | Mutable sync state | ✅ Yes | ❌ No | Counter, local UI state |
| AsyncNotifierProvider | Mutable async state | ✅ Yes | ✅ Yes | User profile, posts list |
| StreamNotifierProvider | Mutable stream state | ✅ Yes | ✅ Yes | Chat messages |
| StateProvider | Simple mutable state | ✅ Yes | ❌ No | Theme, filters (deprecated) |
3. Provider - Dependency Injection
Purpose
Inject dependencies across the app.
When to Use
text✅ API clients ✅ Repositories ✅ Services ✅ Configurations ✅ Utility classes
Example
dartimport 'package:riverpod_annotation/riverpod_annotation.dart'; part 'providers.g.dart'; // Inject API client Dio dio(DioRef ref) { return Dio(BaseOptions( baseUrl: 'https://jsonplaceholder.typicode.com', )); } // Inject repository (depends on Dio) UserRepository userRepository(UserRepositoryRef ref) { final dio = ref.watch(dioProvider); return UserRepository(dio); }
Key Point: Provider is for dependency injection, not state management.
4. FutureProvider - Async Read-Only Data
Purpose
Fetch data once (GET requests).
When to Use
text✅ Fetch user profile (one-time) ✅ Load config from API ✅ Database queries ❌ Refreshable lists (use AsyncNotifier instead)
Example
dartFuture<User> userProfile(UserProfileRef ref, int userId) async { final repo = ref.watch(userRepositoryProvider); return repo.getUserById(userId); } // UI Usage class UserProfileScreen extends ConsumerWidget { final int userId; Widget build(BuildContext context, WidgetRef ref) { final userAsync = ref.watch(userProfileProvider(userId)); return userAsync.when( data: (user) => Text('Hello, ${user.name}'), loading: () => CircularProgressIndicator(), error: (err, stack) => Text('Error: $err'), ); } }
Limitation: Cannot refresh/reload manually. For that, use AsyncNotifier.
5. StreamProvider - Realtime Streams
Purpose
Listen to continuous data streams.
When to Use
text✅ Firebase Firestore streams ✅ WebSocket connections ✅ Real-time notifications ✅ Live location updates
Example
dartStream<List<Message>> messagesStream(MessagesStreamRef ref, String chatId) { final firestore = ref.watch(firestoreProvider); return firestore .collection('chats') .doc(chatId) .collection('messages') .orderBy('timestamp') .snapshots() .map((snapshot) => snapshot.docs.map((doc) => Message.fromDoc(doc)).toList()); } // UI Usage class ChatScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final messagesAsync = ref.watch(messagesStreamProvider('chat123')); return messagesAsync.when( data: (messages) => ListView.builder( itemCount: messages.length, itemBuilder: (context, index) => MessageTile(messages[index]), ), loading: () => CircularProgressIndicator(), error: (err, stack) => Text('Error: $err'), ); } }
6. NotifierProvider - Mutable Synchronous State
Purpose
Manage mutable state without async operations.
When to Use
text✅ Counter ✅ Form state (before submission) ✅ UI toggles (theme, dark mode) ✅ Filters, sorting ❌ API calls (use AsyncNotifier)
Example
dartclass Counter extends _$Counter { int build() { return 0; // Initial state } void increment() { state++; // Mutable } void decrement() { state--; } void reset() { state = 0; } } // UI Usage class CounterScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final count = ref.watch(counterProvider); return Column( children: [ Text('Count: $count'), ElevatedButton( onPressed: () => ref.read(counterProvider.notifier).increment(), child: Text('Increment'), ), ], ); } }
7. AsyncNotifierProvider - Most Common in Production
Purpose
Manage mutable async state (API calls, caching).
When to Use
text✅ Fetch + cache user list ✅ Pagination ✅ Refresh/reload data ✅ CRUD operations (via mutations) ✅ Network state management
Example
dartclass Users extends _$Users { Future<List<User>> build() async { // Initial fetch final repo = ref.read(userRepositoryProvider); return repo.fetchUsers(); } Future<void> refresh() async { state = const AsyncLoading(); final repo = ref.read(userRepositoryProvider); state = await AsyncValue.guard(() => repo.fetchUsers()); } Future<void> addUser(User user) async { // Optimistic update final currentUsers = state.value ?? []; state = AsyncData([...currentUsers, user]); try { await ref.read(userRepositoryProvider).createUser(user); } catch (e) { // Rollback on error state = AsyncData(currentUsers); rethrow; } } } // UI Usage class UsersScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final usersAsync = ref.watch(usersProvider); return RefreshIndicator( onRefresh: () => ref.read(usersProvider.notifier).refresh(), child: usersAsync.when( data: (users) => ListView.builder( itemCount: users.length, itemBuilder: (context, index) => UserTile(users[index]), ), loading: () => Center(child: CircularProgressIndicator()), error: (err, stack) => Center(child: Text('Error: $err')), ), ); } }
This is the most common pattern in production apps.
8. Riverpod Mutations (Riverpod 3.0+)
What Are Mutations?
Mutations handle side effects like:
textPOST requests PUT updates DELETE operations Form submissions Login/logout File uploads
Why Mutations?
Problem without Mutations:
dart// ❌ Pollutes state with UI concerns class Users extends _$Users { bool isCreating = false; // UI state mixed with data String? createError; Future<void> createUser(User user) async { isCreating = true; createError = null; // ... logic } }
With Mutations:
dart// ✅ Clean separation final createUserMutation = Mutation<User>(); // State stays clean (just data)
Mutation States
| State | Meaning |
|---|---|
text | No operation running |
text | Loading (request in progress) |
text | Request succeeded |
text | Request failed |
Production Mutation Example
dartimport 'package:riverpod_mutation/riverpod_mutation.dart'; // Define mutation final createUserMutation = Mutation<User>(); // Use in UI class CreateUserButton extends ConsumerWidget { final String name; Widget build(BuildContext context, WidgetRef ref) { final mutation = ref.watch(createUserMutation); return ElevatedButton( onPressed: mutation is MutationPending ? null : () async { createUserMutation.run(ref, (tsx) async { // Get repository final repo = tsx.get(userRepositoryProvider); // Execute mutation final user = await repo.createUser(name); // Invalidate users list to refresh ref.invalidate(usersProvider); return user; }); }, child: mutation is MutationPending ? CircularProgressIndicator() : Text('Create User'), ); } }
Mutations Are Optional
You can still use AsyncNotifier for write operations:
dartclass Users extends _$Users { Future<List<User>> build() async => fetchUsers(); // Write operation without mutation Future<void> createUser(String name) async { final repo = ref.read(userRepositoryProvider); await repo.createUser(name); ref.invalidate(usersProvider); // Refresh } }
When to use Mutations:
- ✅ Need explicit loading/error states in UI
- ✅ Complex form submissions
- ✅ Retry logic needed
When to skip Mutations:
- ✅ Simple CRUD with AsyncNotifier is enough
- ✅ Don't need separate mutation state
9. When to Use Riverpod Annotations (@riverpod)
Always Use @riverpod for New Code
Modern Riverpod uses code generation with
@riverpodSetup:
yaml# pubspec.yaml dependencies: flutter_riverpod: ^2.5.0 riverpod_annotation: ^2.3.0 dev_dependencies: build_runner: ^2.4.0 riverpod_generator: ^2.3.0
Run generator:
bashflutter pub run build_runner watch
Benefits:
- Less boilerplate
- Type-safe
- Auto-generated providers
- Better refactoring support
Without @riverpod (manual):
dartfinal userRepositoryProvider = Provider<UserRepository>((ref) { return UserRepository(); });
With @riverpod (generated):
dartUserRepository userRepository(UserRepositoryRef ref) { return UserRepository(); } // Provider auto-generated!
10. Production-Level Architecture
Recommended Stack
textRiverpod (annotation + code generation) + MVVM Architecture + Feature-First Folder Structure + Repository Pattern + Dio for networking
Project Structure
textlib/ ├── core/ │ ├── network/ │ │ └── dio_provider.dart │ └── router/ │ └── app_router.dart ├── features/ │ ├── users/ │ │ ├── data/ │ │ │ ├── models/ │ │ │ │ └── user_model.dart │ │ │ ├── repositories/ │ │ │ │ └── user_repository.dart │ │ │ └── data_sources/ │ │ │ └── user_remote_data_source.dart │ │ ├── domain/ │ │ │ └── entities/ │ │ │ └── user.dart │ │ ├── presentation/ │ │ │ ├── providers/ │ │ │ │ └── users_provider.dart │ │ │ ├── screens/ │ │ │ │ └── users_screen.dart │ │ │ └── widgets/ │ │ │ └── user_tile.dart │ │ └── users_feature.dart │ └── posts/ │ └── ... (same structure) └── main.dart
11. Complete Production Example
API: JSONPlaceholder (https://jsonplaceholder.typicode.com)
We'll build a Users feature with:
- Fetch users (GET)
- Create user (POST with Mutation)
- MVVM + Feature-first structure
- Riverpod annotation
Step 1: Core - Dio Provider
dart// core/network/dio_provider.dart import 'package:dio/dio.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'dio_provider.g.dart'; Dio dio(DioRef ref) { return Dio( BaseOptions( baseUrl: 'https://jsonplaceholder.typicode.com', connectTimeout: Duration(seconds: 5), receiveTimeout: Duration(seconds: 3), headers: { 'Content-Type': 'application/json', }, ), )..interceptors.add(LogInterceptor( requestBody: true, responseBody: true, )); }
Step 2: Domain - Entity
dart// features/users/domain/entities/user.dart class User { final int id; final String name; final String email; final String phone; User({ required this.id, required this.name, required this.email, required this.phone, }); }
Step 3: Data - Model
dart// features/users/data/models/user_model.dart import '../../domain/entities/user.dart'; class UserModel extends User { UserModel({ required super.id, required super.name, required super.email, required super.phone, }); factory UserModel.fromJson(Map<String, dynamic> json) { return UserModel( id: json['id'] ?? 0, name: json['name'] ?? '', email: json['email'] ?? '', phone: json['phone'] ?? '', ); } Map<String, dynamic> toJson() { return { 'id': id, 'name': name, 'email': email, 'phone': phone, }; } }
Step 4: Data - Remote Data Source
dart// features/users/data/data_sources/user_remote_data_source.dart import 'package:dio/dio.dart'; import '../models/user_model.dart'; class UserRemoteDataSource { final Dio dio; UserRemoteDataSource(this.dio); Future<List<UserModel>> fetchUsers() async { final response = await dio.get('/users'); final List<dynamic> data = response.data; return data.map((json) => UserModel.fromJson(json)).toList(); } Future<UserModel> getUserById(int id) async { final response = await dio.get('/users/$id'); return UserModel.fromJson(response.data); } Future<UserModel> createUser(UserModel user) async { final response = await dio.post('/users', data: user.toJson()); return UserModel.fromJson(response.data); } Future<UserModel> updateUser(int id, UserModel user) async { final response = await dio.put('/users/$id', data: user.toJson()); return UserModel.fromJson(response.data); } Future<void> deleteUser(int id) async { await dio.delete('/users/$id'); } }
Step 5: Data - Repository
dart// features/users/data/repositories/user_repository.dart import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../../../core/network/dio_provider.dart'; import '../../domain/entities/user.dart'; import '../data_sources/user_remote_data_source.dart'; import '../models/user_model.dart'; part 'user_repository.g.dart'; UserRepository userRepository(UserRepositoryRef ref) { final dio = ref.watch(dioProvider); final dataSource = UserRemoteDataSource(dio); return UserRepository(dataSource); } class UserRepository { final UserRemoteDataSource _dataSource; UserRepository(this._dataSource); Future<List<User>> fetchUsers() async { return await _dataSource.fetchUsers(); } Future<User> getUserById(int id) async { return await _dataSource.getUserById(id); } Future<User> createUser({ required String name, required String email, required String phone, }) async { final userModel = UserModel( id: 0, // Server will assign name: name, email: email, phone: phone, ); return await _dataSource.createUser(userModel); } Future<User> updateUser(int id, User user) async { final userModel = UserModel( id: user.id, name: user.name, email: user.email, phone: user.phone, ); return await _dataSource.updateUser(id, userModel); } Future<void> deleteUser(int id) async { await _dataSource.deleteUser(id); } }
Step 6: Presentation - Providers (ViewModel)
dart// features/users/presentation/providers/users_provider.dart import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../data/repositories/user_repository.dart'; import '../../domain/entities/user.dart'; part 'users_provider.g.dart'; // Main users list provider (ViewModel) class Users extends _$Users { Future<List<User>> build() async { // Initial fetch return _fetchUsers(); } Future<List<User>> _fetchUsers() async { final repo = ref.read(userRepositoryProvider); return await repo.fetchUsers(); } // Refresh users Future<void> refresh() async { state = const AsyncLoading(); state = await AsyncValue.guard(() => _fetchUsers()); } // Delete user Future<void> deleteUser(int id) async { // Optimistic delete final currentUsers = state.value ?? []; final updatedUsers = currentUsers.where((u) => u.id != id).toList(); state = AsyncData(updatedUsers); try { await ref.read(userRepositoryProvider).deleteUser(id); } catch (e) { // Rollback on error state = AsyncData(currentUsers); rethrow; } } } // Single user provider Future<User> userDetail(UserDetailRef ref, int userId) async { final repo = ref.read(userRepositoryProvider); return await repo.getUserById(userId); }
Step 7: Presentation - Mutation (Optional)
dart// features/users/presentation/providers/create_user_mutation.dart import 'package:riverpod_mutation/riverpod_mutation.dart'; import '../../domain/entities/user.dart'; // Create user mutation final createUserMutation = Mutation<User>(); // Update user mutation final updateUserMutation = Mutation<User>();
Step 8: Presentation - Screen
dart// features/users/presentation/screens/users_screen.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/users_provider.dart'; import '../widgets/user_tile.dart'; import 'create_user_screen.dart'; class UsersScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final usersAsync = ref.watch(usersProvider); return Scaffold( appBar: AppBar( title: Text('Users'), actions: [ IconButton( icon: Icon(Icons.refresh), onPressed: () => ref.read(usersProvider.notifier).refresh(), ), ], ), body: usersAsync.when( data: (users) { if (users.isEmpty) { return Center(child: Text('No users found')); } return RefreshIndicator( onRefresh: () => ref.read(usersProvider.notifier).refresh(), child: ListView.builder( itemCount: users.length, itemBuilder: (context, index) { final user = users[index]; return UserTile( user: user, onDelete: () async { final confirmed = await showDialog<bool>( context: context, builder: (context) => AlertDialog( title: Text('Delete User'), content: Text('Delete ${user.name}?'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: Text('Cancel'), ), TextButton( onPressed: () => Navigator.pop(context, true), child: Text('Delete'), ), ], ), ); if (confirmed == true) { await ref.read(usersProvider.notifier).deleteUser(user.id); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('${user.name} deleted')), ); } }, ); }, ), ); }, loading: () => Center(child: CircularProgressIndicator()), error: (error, stack) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Error: $error'), SizedBox(height: 16), ElevatedButton( onPressed: () => ref.read(usersProvider.notifier).refresh(), child: Text('Retry'), ), ], ), ), ), floatingActionButton: FloatingActionButton( onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (_) => CreateUserScreen()), ); }, child: Icon(Icons.add), ), ); } }
Step 9: Presentation - Widget
dart// features/users/presentation/widgets/user_tile.dart import 'package:flutter/material.dart'; import '../../domain/entities/user.dart'; class UserTile extends StatelessWidget { final User user; final VoidCallback? onDelete; const UserTile({ required this.user, this.onDelete, }); Widget build(BuildContext context) { return ListTile( leading: CircleAvatar( child: Text(user.name[0].toUpperCase()), ), title: Text(user.name), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(user.email), Text(user.phone, style: TextStyle(fontSize: 12)), ], ), trailing: IconButton( icon: Icon(Icons.delete, color: Colors.red), onPressed: onDelete, ), ); } }
Step 10: Create User Screen (with Mutation)
dart// features/users/presentation/screens/create_user_screen.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/create_user_mutation.dart'; import '../providers/users_provider.dart'; import '../../data/repositories/user_repository.dart'; class CreateUserScreen extends ConsumerStatefulWidget { _CreateUserScreenState createState() => _CreateUserScreenState(); } class _CreateUserScreenState extends ConsumerState<CreateUserScreen> { final _nameController = TextEditingController(); final _emailController = TextEditingController(); final _phoneController = TextEditingController(); void dispose() { _nameController.dispose(); _emailController.dispose(); _phoneController.dispose(); super.dispose(); } Widget build(BuildContext context) { final mutation = ref.watch(createUserMutation); return Scaffold( appBar: AppBar(title: Text('Create User')), body: Padding( padding: EdgeInsets.all(16), child: Column( children: [ TextField( controller: _nameController, decoration: InputDecoration(labelText: 'Name'), ), SizedBox(height: 16), TextField( controller: _emailController, decoration: InputDecoration(labelText: 'Email'), keyboardType: TextInputType.emailAddress, ), SizedBox(height: 16), TextField( controller: _phoneController, decoration: InputDecoration(labelText: 'Phone'), keyboardType: TextInputType.phone, ), SizedBox(height: 32), ElevatedButton( onPressed: mutation is MutationPending ? null : () async { final name = _nameController.text.trim(); final email = _emailController.text.trim(); final phone = _phoneController.text.trim(); if (name.isEmpty || email.isEmpty || phone.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('All fields required')), ); return; } createUserMutation.run(ref, (tsx) async { final repo = tsx.get(userRepositoryProvider); final user = await repo.createUser( name: name, email: email, phone: phone, ); // Refresh users list ref.invalidate(usersProvider); return user; }); // Navigate back on success if (mutation is MutationSuccess) { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('User created successfully')), ); } }, child: mutation is MutationPending ? SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : Text('Create User'), ), if (mutation is MutationError) Padding( padding: EdgeInsets.only(top: 16), child: Text( 'Error: ${mutation.error}', style: TextStyle(color: Colors.red), ), ), ], ), ), ); } }
Step 11: Main App
dart// main.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'features/users/presentation/screens/users_screen.dart'; void main() { runApp( ProviderScope( child: MyApp(), ), ); } class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Riverpod Production Example', theme: ThemeData( primarySwatch: Colors.blue, useMaterial3: true, ), home: UsersScreen(), ); } }
12. Complete Feature Structure
textlib/ ├── core/ │ └── network/ │ ├── dio_provider.dart │ └── dio_provider.g.dart # Generated ├── features/ │ └── users/ │ ├── data/ │ │ ├── data_sources/ │ │ │ └── user_remote_data_source.dart │ │ ├── models/ │ │ │ └── user_model.dart │ │ └── repositories/ │ │ ├── user_repository.dart │ │ └── user_repository.g.dart # Generated │ ├── domain/ │ │ └── entities/ │ │ └── user.dart │ └── presentation/ │ ├── providers/ │ │ ├── users_provider.dart │ │ ├── users_provider.g.dart # Generated │ │ └── create_user_mutation.dart │ ├── screens/ │ │ ├── users_screen.dart │ │ └── create_user_screen.dart │ └── widgets/ │ └── user_tile.dart └── main.dart
13. Best Practices Summary
1. Always Use Code Generation
dart✅ Use ❌ Avoid manual provider declaration
2. Dependency Injection
dart✅ Inject via Provider ❌ Don't create instances manually
3. State Management
dart✅ AsyncNotifier for mutable async state ✅ FutureProvider for read-only async ✅ NotifierProvider for sync mutable state ❌ Don't use StateProvider (deprecated)
4. Mutations
dart✅ Use for explicit side effects (POST/PUT/DELETE) ✅ Clean separation of concerns ⚠️ Optional - AsyncNotifier can handle writes too
5. Architecture
dart✅ Feature-first structure ✅ MVVM (ViewModel = Provider) ✅ Repository pattern ✅ Dependency injection
14. Migration from Old Riverpod
ChangeNotifier → AsyncNotifier
dart// ❌ Old - ChangeNotifier class UserNotifier extends ChangeNotifier { User? _user; bool _isLoading = false; Future<void> fetchUser() async { _isLoading = true; notifyListeners(); _user = await api.getUser(); _isLoading = false; notifyListeners(); } } // ✅ New - AsyncNotifier class UserNotifier extends _$UserNotifier { Future<User?> build() async { return await ref.read(apiProvider).getUser(); } Future<void> refresh() async { state = const AsyncLoading(); state = await AsyncValue.guard(() => ref.read(apiProvider).getUser()); } }
StateNotifier → NotifierProvider
dart// ❌ Old - StateNotifier class CounterNotifier extends StateNotifier<int> { CounterNotifier() : super(0); void increment() => state++; } final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) { return CounterNotifier(); }); // ✅ New - NotifierProvider class Counter extends _$Counter { int build() => 0; void increment() => state++; }
15. Summary Table
| Feature | Provider | FutureProvider | NotifierProvider | AsyncNotifierProvider | Mutation |
|---|---|---|---|---|---|
| Purpose | DI | Async read | Sync state | Async state | Side effects |
| Mutable | ❌ | ❌ | ✅ | ✅ | ✅ |
| Async | ⚠️ | ✅ | ❌ | ✅ | ✅ |
| Refresh | ❌ | ❌ | ✅ | ✅ | ✅ |
| Use @riverpod | ✅ | ✅ | ✅ | ✅ | ⚠️ |
| Production Use | ✅ Always | ⚠️ Limited | ✅ Often | ✅ Most Common | ✅ Optional |
16. Key Takeaways
Provider is for Dependency Injection (inject services, repos)
AsyncNotifier is the most common provider in production (mutable async state)
FutureProvider is for read-only async data (cannot refresh)
Mutations are optional but useful for explicit side effects
Always use @riverpod annotation for code generation
Feature-first + MVVM + Repository is the recommended architecture