State Management using Riverpod in Flutter
Riverpod is a next-generation reactive caching and data-binding framework designed as the spiritual successor to Provider. It offers compile-time safety, superior dependency injection, and exceptional testability while eliminating common state management pitfalls.
Comprehensive State Management with Riverpod in Flutter
Riverpod is a next-generation reactive caching and data-binding framework designed as the spiritual successor to Provider. It offers compile-time safety, superior dependency injection, and exceptional testability while eliminating common state management pitfalls.
Why Choose Riverpod?
Understanding the compelling advantages that make Riverpod an excellent choice for Flutter state management.
Key Advantages:
- Compile-time Safety: Catch errors during development rather than runtime
- Superior Dependency Injection: Flexible and powerful dependency management
- Enhanced Testability: Straightforward testing patterns with built-in support
- No BuildContext Dependency: Access providers without widget tree context
- Reactive Programming: Built-in support for streams and futures
- Provider Modifiers: Rich set of modifiers for advanced scenarios
- DevTools Integration: Excellent debugging and inspection capabilities
Installation and Setup
Add Riverpod to your Flutter project and configure the basic setup.
dependencies:
flutter_riverpod: ^<version_number>Core Provider Types and Use Cases
Riverpod offers various provider types tailored for different state management scenarios.
// Provider - For immutable values and simple dependencies
final helloWorldProvider = Provider<String>((ref) => 'Hello World');
// StateProvider - For simple mutable state
final counterProvider = StateProvider<int>((ref) => 0);
// StateNotifierProvider - For complex state management with business logic
final todosProvider = StateNotifierProvider<TodoNotifier, List<Todo>>((ref) => TodoNotifier());
// FutureProvider - For asynchronous operations and data fetching
final userProvider = FutureProvider<User>((ref) async {
return await fetchUser();
});
// StreamProvider - For real-time data streams
final messagesProvider = StreamProvider<List<Message>>((ref) {
return chatService.messagesStream();
});
// ChangeNotifierProvider - For legacy ChangeNotifier patterns
final themeProvider = ChangeNotifierProvider<ThemeNotifier>((ref) => ThemeNotifier());Application Setup and ProviderScope
Configure your Flutter application to use Riverpod with ProviderScope.
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
title: 'Riverpod Example',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const HomePage(),
debugShowCheckedModeBanner: false,
),
);
}
}
class HomePage extends ConsumerWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterProvider);
final helloMessage = ref.read(helloWorldProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Riverpod State Management'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
helloMessage,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 20),
Text(
'Counter Value: $counter',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 30),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).state--,
child: const Text('Decrement'),
),
const SizedBox(width: 20),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: const Text('Increment'),
),
],
),
],
),
),
);
}
}Advanced StateNotifier Implementation
Build complex state management with StateNotifier for real-world applications.
// Domain Model
class Todo {
final String id;
final String title;
final String description;
final bool completed;
final DateTime createdAt;
final DateTime? completedAt;
Todo({
required this.id,
required this.title,
this.description = '',
this.completed = false,
required this.createdAt,
this.completedAt,
});
Todo copyWith({
String? id,
String? title,
String? description,
bool? completed,
DateTime? createdAt,
DateTime? completedAt,
}) {
return Todo(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
completed: completed ?? this.completed,
createdAt: createdAt ?? this.createdAt,
completedAt: completedAt ?? this.completedAt,
);
}
}
// StateNotifier Implementation
class TodoNotifier extends StateNotifier<List<Todo>> {
TodoNotifier() : super([]);
void addTodo(String title, {String description = ''}) {
final newTodo = Todo(
id: DateTime.now().microsecondsSinceEpoch.toString(),
title: title,
description: description,
createdAt: DateTime.now(),
);
state = [...state, newTodo];
}
void toggleTodo(String id) {
state = state.map((todo) {
if (todo.id == id) {
return todo.copyWith(
completed: !todo.completed,
completedAt: !todo.completed ? DateTime.now() : null,
);
}
return todo;
}).toList();
}
void updateTodo(String id, String title, {String? description}) {
state = state.map((todo) {
if (todo.id == id) {
return todo.copyWith(
title: title,
description: description,
);
}
return todo;
}).toList();
}
void removeTodo(String id) {
state = state.where((todo) => todo.id != id).toList();
}
void clearCompleted() {
state = state.where((todo) => !todo.completed).toList();
}
void markAllAsCompleted() {
state = state.map((todo) => todo.copyWith(
completed: true,
completedAt: DateTime.now(),
)).toList();
}
void markAllAsIncomplete() {
state = state.map((todo) => todo.copyWith(
completed: false,
completedAt: null,
)).toList();
}
}
// Provider Definition
final todoProvider = StateNotifierProvider<TodoNotifier, List<Todo>>((ref) => TodoNotifier());Provider Composition and Derived State
Combine multiple providers to create derived state and complex business logic.
// Filter Types
enum TodoFilter { all, active, completed }
// Filter Provider
final todoFilterProvider = StateProvider<TodoFilter>((ref) => TodoFilter.all);
// Search Query Provider
final searchQueryProvider = StateProvider<String>((ref) => '');
// Derived Providers
final filteredTodosProvider = Provider<List<Todo>>((ref) {
final filter = ref.watch(todoFilterProvider);
final searchQuery = ref.watch(searchQueryProvider);
final todos = ref.watch(todoProvider);
var filteredTodos = todos;
// Apply filter
switch (filter) {
case TodoFilter.all:
break;
case TodoFilter.active:
filteredTodos = todos.where((todo) => !todo.completed).toList();
break;
case TodoFilter.completed:
filteredTodos = todos.where((todo) => todo.completed).toList();
break;
}
// Apply search
if (searchQuery.isNotEmpty) {
filteredTodos = filteredTodos.where((todo) {
return todo.title.toLowerCase().contains(searchQuery.toLowerCase()) ||
todo.description.toLowerCase().contains(searchQuery.toLowerCase());
}).toList();
}
return filteredTodos;
});
// Statistics Provider
final todoStatsProvider = Provider<TodoStats>((ref) {
final todos = ref.watch(todoProvider);
final total = todos.length;
final completed = todos.where((todo) => todo.completed).length;
final active = total - completed;
return TodoStats(
total: total,
completed: completed,
active: active,
completionPercentage: total > 0 ? (completed / total) * 100 : 0,
);
});
class TodoStats {
final int total;
final int completed;
final int active;
final double completionPercentage;
TodoStats({
required this.total,
required this.completed,
required this.active,
required this.completionPercentage,
});
}Family Modifier for Parameterized Providers
Use the family modifier to create providers that accept parameters.
// User Provider with Family
final userProvider = FutureProvider.family<User, String>((ref, userId) async {
// Cancel previous request if userId changes
ref.keepAlive();
final userRepository = ref.read(userRepositoryProvider);
return await userRepository.getUserById(userId);
});
// Product Provider with Multiple Parameters
final productProvider = FutureProvider.family<Product, ProductQuery>((ref, query) async {
final productRepository = ref.read(productRepositoryProvider);
return await productRepository.getProduct(
id: query.id,
includeReviews: query.includeReviews,
);
});
class ProductQuery {
final String id;
final bool includeReviews;
const ProductQuery({
required this.id,
this.includeReviews = false,
});
}
// Usage in Widgets
class UserProfile extends ConsumerWidget {
final String userId;
const UserProfile({super.key, required this.userId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider(userId));
return userAsync.when(
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
data: (user) {
return Column(
children: [
Text('Name: ${user.name}'),
Text('Email: ${user.email}'),
// ... other user details
],
);
},
);
}
}Provider Modifiers: AutoDispose and More
Utilize provider modifiers for advanced scenarios like automatic disposal and state persistence.
// AutoDispose - Automatically disposed when no longer used
final temporaryDataProvider = FutureProvider.autoDispose<String>((ref) async {
return await fetchTemporaryData();
});
// Keep Alive - Persist state even when not watched
final persistentConfigProvider = Provider<String>((ref) {
ref.keepAlive();
return loadPersistentConfiguration();
});
// StateProvider with AutoDispose
final sessionProvider = StateProvider.autoDispose<int>((ref) => 0);
// Combining Modifiers
final cachedUserProvider = FutureProvider.autoDispose.family<User, String>((ref, userId) async {
// This provider will be automatically disposed
// but can also accept parameters
final userService = ref.read(userServiceProvider);
return await userService.getUser(userId);
});
// State Notifier with AutoDispose
final temporaryTodoProvider = StateNotifierProvider.autoDispose<TodoNotifier, List<Todo>>((ref) => TodoNotifier());Testing Riverpod Providers
Comprehensive testing strategies for Riverpod providers and state management.
// Unit Tests for Providers
void main() {
test('counter provider increments correctly', () {
final container = ProviderContainer();
// Initial state
expect(container.read(counterProvider), 0);
// Increment
container.read(counterProvider.notifier).state++;
// Verify
expect(container.read(counterProvider), 1);
});
test('todo provider adds and removes todos', () {
final container = ProviderContainer();
final notifier = container.read(todoProvider.notifier);
// Initial state
expect(container.read(todoProvider), isEmpty);
// Add todo
notifier.addTodo('Test Todo');
expect(container.read(todoProvider), hasLength(1));
expect(container.read(todoProvider).first.title, 'Test Todo');
// Remove todo
final todoId = container.read(todoProvider).first.id;
notifier.removeTodo(todoId);
expect(container.read(todoProvider), isEmpty);
});
test('filtered todos provider works correctly', () {
final container = ProviderContainer();
final todoNotifier = container.read(todoProvider.notifier);
final filterNotifier = container.read(todoFilterProvider.notifier);
// Add todos
todoNotifier.addTodo('Active Todo');
todoNotifier.addTodo('Completed Todo');
// Mark one as completed
final completedTodoId = container.read(todoProvider).last.id;
todoNotifier.toggleTodo(completedTodoId);
// Test all filter
filterNotifier.state = TodoFilter.all;
expect(container.read(filteredTodosProvider), hasLength(2));
// Test active filter
filterNotifier.state = TodoFilter.active;
expect(container.read(filteredTodosProvider), hasLength(1));
expect(container.read(filteredTodosProvider).first.title, 'Active Todo');
// Test completed filter
filterNotifier.state = TodoFilter.completed;
expect(container.read(filteredTodosProvider), hasLength(1));
expect(container.read(filteredTodosProvider).first.title, 'Completed Todo');
});
}
// Widget Tests with Riverpod
void main() {
testWidgets('Counter increments when button is pressed', (tester) async {
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
home: HomePage(),
),
),
);
// Verify initial state
expect(find.text('Counter Value: 0'), findsOneWidget);
// Tap increment button
await tester.tap(find.text('Increment'));
await tester.pump();
// Verify updated state
expect(find.text('Counter Value: 1'), findsOneWidget);
});
}Best Practices and Performance Optimization
Follow these guidelines to ensure optimal performance and maintainable code.
Performance Optimization:
- Use *select* to watch only specific properties
- Implement proper provider scoping
- Utilize *autoDispose* for temporary state
- Avoid unnecessary provider rebuilds
- Use *family* for parameterized data
Code Organization:
- Group related providers in separate files
- Use consistent naming conventions
- Document provider dependencies
- Implement proper error handling
- Use enums for state management
State Management Patterns:
- Prefer StateNotifier for complex state
- Use derived state for computed values
- Implement proper loading and error states
- Handle edge cases and null states
- Use provider modifiers appropriately
Common Patterns and Real-World Examples
Explore practical implementation patterns for common application scenarios.
Authentication Flow:
- Use StateNotifier for auth state
- Implement token refresh logic
- Handle authentication errors gracefully
- Manage user session state
Data Fetching Patterns:
- Implement caching strategies
- Handle pagination with providers
- Manage loading and error states
- Implement pull-to-refresh functionality
Form Management:
- Use StateNotifier for form state
- Implement validation logic
- Handle form submission
- Manage form dirty state
Riverpod provides a robust, scalable, and developer-friendly solution for state management in Flutter applications. By following these patterns and best practices, you can build maintainable, testable, and performant applications that scale with your project's complexity while providing excellent developer experience.