Flutter Riverpod
Modern, compile-safe state management
๐ What is Riverpod?
Riverpod is a complete rewrite of Provider that fixes its limitations. It offers compile-time safety, no BuildContext dependency, better testability, and powerful features like auto-dispose and family modifiers for modern Flutter development.
// Simple provider declaration
final counterProvider = StateProvider((ref) => 0);
Key Concepts
Compile Safe
Catch errors at compile time
// Type-safe access
ref.watch(counterProvider)
No Context
Access providers anywhere
// No BuildContext needed
ref.read(provider)
Testable
Easy to mock and test
ProviderContainer(
overrides: [...])
Auto-dispose
Automatic memory management
final provider =
StateProvider.autoDispose
๐น Installation
Add Riverpod to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.4.0
Then run:
flutter pub get
๐น Basic Counter Example
Create a simple counter with Riverpod:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1. Create a provider (outside any class)
final counterProvider = StateProvider<int>((ref) => 0);
// 2. Wrap app with ProviderScope
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CounterPage(),
);
}
}
// 3. Use ConsumerWidget to access providers
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch the provider
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(title: Text('Riverpod Counter')),
body: Center(
child: Text(
'$count',
style: TextStyle(fontSize: 48),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Update the provider
ref.read(counterProvider.notifier).state++;
},
child: Icon(Icons.add),
),
);
}
}
Output:
A counter app that increments when the floating button is pressed.
๐น StateNotifierProvider Example
For more complex state logic:
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1. Create a StateNotifier
class Counter extends StateNotifier<int> {
Counter() : super(0);
void increment() => state++;
void decrement() => state--;
void reset() => state = 0;
}
// 2. Create a provider
final counterProvider = StateNotifierProvider<Counter, int>((ref) {
return Counter();
});
// 3. Use in widget
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
final counter = ref.read(counterProvider.notifier);
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$count', style: TextStyle(fontSize: 48)),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: counter.decrement,
child: Text('-'),
),
SizedBox(width: 20),
ElevatedButton(
onPressed: counter.increment,
child: Text('+'),
),
SizedBox(width: 20),
ElevatedButton(
onPressed: counter.reset,
child: Text('Reset'),
),
],
),
],
),
),
);
}
}
Output:
A counter with increment, decrement, and reset buttons.
๐น FutureProvider Example
Handle async data with FutureProvider:
// Simulate API call
Future<String> fetchUserName() async {
await Future.delayed(Duration(seconds: 2));
return 'John Doe';
}
// Create FutureProvider
final userProvider = FutureProvider<String>((ref) async {
return await fetchUserName();
});
// Use in widget
class UserPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider);
return Scaffold(
appBar: AppBar(title: Text('User Profile')),
body: Center(
child: userAsync.when(
data: (name) => Text('Hello, $name!',
style: TextStyle(fontSize: 24)),
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
),
),
);
}
}
Output:
Shows loading spinner, then displays the user name, or shows error if failed.
๐น Family Modifier
Create providers with parameters:
// Provider that takes a parameter
final todoProvider = Provider.family<String, int>((ref, id) {
return 'Todo #$id';
});
class TodoList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ListView.builder(
itemCount: 5,
itemBuilder: (context, index) {
// Access provider with parameter
final todo = ref.watch(todoProvider(index));
return ListTile(
title: Text(todo),
);
},
);
}
}
Output:
A list displaying "Todo #0", "Todo #1", etc.
๐น Consumer vs ConsumerWidget
Two ways to consume providers:
// Option 1: ConsumerWidget (entire widget rebuilds)
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}
// Option 2: Consumer (partial rebuild)
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Static text'), // Won't rebuild
Consumer(
builder: (context, ref, child) {
final count = ref.watch(counterProvider);
return Text('$count'); // Only this rebuilds
},
),
],
);
}
}
Output:
Both approaches display the counter, but Consumer offers more granular control.
๐น When to Use Riverpod
โ Good For:
- New Flutter projects
- Apps requiring compile-time safety
- Complex state management needs
- Apps with lots of async operations
- When testability is important
โ Consider Alternatives For:
- Very simple apps (use setState)
- Existing Provider apps (migration effort)
- Teams unfamiliar with reactive programming
- When you need Redux DevTools
๐น Riverpod vs Provider
Riverpod Advantages:
- No BuildContext: Access providers anywhere
- Compile-safe: Errors caught at compile time
- Better testing: Easy to override providers
- Auto-dispose: Automatic cleanup
- Family modifier: Parameterized providers