State Management in Flutter with Riverpod 2.0
Exploring Riverpod's powerful state management patterns for building scalable Flutter applications.
State Management in Flutter with Riverpod 2.0
After trying various state management solutions in Flutter, Riverpod has become my go-to choice. Here’s why and how to use it effectively.
Why Riverpod?
Riverpod solves many pain points I had with other solutions:
- Compile-time safety: Catch errors before runtime
- No context required: Access providers from anywhere
- Testability: Easy to mock and test
- DevTools integration: Great debugging experience
Setting Up Riverpod
Add the dependencies:
dependencies:
flutter_riverpod: ^2.4.0
riverpod_annotation: ^2.3.0
dev_dependencies:
riverpod_generator: ^2.3.0
build_runner: ^2.4.0 Wrap your app with ProviderScope:
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
} Basic Providers
The simplest provider holds a value:
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'counter.g.dart';
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
void decrement() => state--;
} Run the code generator:
dart run build_runner build Using Providers in Widgets
Access providers with ref:
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: const Text('Increment'),
),
],
);
}
} Async Providers
Handle async data elegantly:
@riverpod
Future<List<User>> users(UsersRef ref) async {
final response = await http.get(Uri.parse('/api/users'));
final data = jsonDecode(response.body) as List;
return data.map((json) => User.fromJson(json)).toList();
} Use in widgets with AsyncValue:
class UserList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(usersProvider);
return usersAsync.when(
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
data: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) => UserTile(user: users[index]),
),
);
}
} Provider Dependencies
Providers can depend on other providers:
@riverpod
Future<UserProfile> userProfile(UserProfileRef ref) async {
// Watch the auth state
final user = ref.watch(authProvider);
if (user == null) throw Exception('Not authenticated');
return ref.watch(apiProvider).getProfile(user.id);
} Tips for Success
- Keep providers small: One responsibility per provider
- Use code generation: Less boilerplate, fewer errors
- Leverage autodispose: Clean up when providers aren’t used
- Family for parameters: When you need dynamic providers
@riverpod
Future<Post> post(PostRef ref, int id) async {
return ref.watch(apiProvider).getPost(id);
}
// Usage
ref.watch(postProvider(42)); Conclusion
Riverpod brings a level of safety and ergonomics that makes Flutter development much more enjoyable. The learning curve is worth it for the reliability and maintainability you gain.
Next up: Building a complete app with Riverpod and GoRouter!