Riverpod is a modern state management solution for Flutter. Learn providers, code generation, AsyncValue and advanced patterns for scalable applications.
Introduction to Flutter State Management¶
Riverpod is a modern state management solution for Flutter that addresses many limitations of Provider and InheritedWidget. It offers compile-time safety, better testability, and improved developer experience through code generation and strong typing.
In this article, we’ll explore key concepts, practical implementations, and best practices you need to know for effective use in production projects. Modern mobile development requires deep understanding of state management patterns, and Riverpod provides an excellent foundation.
Architecture and Key Concepts¶
The foundation of successful Flutter application with Riverpod is understanding its architecture and fundamental concepts. The system is designed with scalability, maintainability, and developer ergonomics in mind.
Key architectural layers include: - Presentation layer - Widgets and UI components - Business logic - Providers and state management - Data layer - Repositories and data sources - Infrastructure layer - Network, storage, and services
// Basic Riverpod provider example
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
void decrement() => state--;
}
// Usage in widget
class CounterWidget extends ConsumerWidget {
@override
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'),
),
],
);
}
}
Providers and Code Generation¶
Riverpod supports code generation which provides compile-time safety and reduces boilerplate. Use @riverpod annotation to generate providers automatically:
@riverpod
Future<List<User>> users(UsersRef ref) async {
final repository = ref.watch(userRepositoryProvider);
return repository.getUsers();
}
@riverpod
class UserNotifier extends _$UserNotifier {
@override
Future<User?> build(String userId) async {
final repository = ref.watch(userRepositoryProvider);
return repository.getUser(userId);
}
Future<void> updateUser(User user) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final repository = ref.watch(userRepositoryProvider);
return repository.updateUser(user);
});
}
}
AsyncValue and Error Handling¶
Riverpod provides AsyncValue for handling asynchronous operations with built-in loading, error, and data states:
class UserProfile extends ConsumerWidget {
final String userId;
const UserProfile({required this.userId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userNotifierProvider(userId));
return userAsync.when(
data: (user) => UserWidget(user: user),
loading: () => const CircularProgressIndicator(),
error: (error, stack) => ErrorWidget(error: error),
);
}
}
Testing with Riverpod¶
Riverpod makes testing easy with provider overrides:
void main() {
testWidgets('Counter increments', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
counterProvider.overrideWith(() => Counter()),
],
child: MyApp(),
),
);
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
expect(find.text('1'), findsOneWidget);
});
}
Best Practices¶
- Use code generation - Leverage
@riverpodfor type safety - Handle AsyncValue properly - Always handle loading and error states
- Keep providers focused - One provider per responsibility
- Use family for parametrized providers - Pass parameters to providers
- Test with overrides - Mock dependencies in tests
Riverpod provides a robust foundation for Flutter state management with excellent developer experience and production-ready patterns.