Introduction to Riverpod: The Future of Flutter State Management
Introduction to Riverpod: The Future of Flutter State Management
Riverpod is a reactive state management library for Flutter and Dart, created by Remi Rousseau (the same developer who built Provider). It addresses several long-standing pain points in Provider while adding powerful new capabilities: compile-time safety, testability without BuildContext, and a code generation API that eliminates boilerplate. This introduction gets you productive with Riverpod's modern annotation-based API.
Why Riverpod Over Provider?
Provider is excellent but has known limitations:
- Providers must be in the widget tree, making them awkward to access outside widgets
- Reading a provider requires
BuildContext, complicating tests and non-widget code - Accidentally watching a provider in the wrong scope causes hard-to-debug issues
- No native support for provider families (parameterized providers)
Riverpod solves all of these. Providers live outside the widget tree in a ProviderScope, can be read anywhere, are fully testable, and support families natively.
Installation
dependencies:
flutter_riverpod: ^2.5.1
riverpod_annotation: ^2.3.5
dev_dependencies:
riverpod_generator: ^2.4.0
build_runner: ^2.4.9
Wrap your app in a ProviderScope:
void main() {
runApp(const ProviderScope(child: MyApp()));
}
The @riverpod Annotation
With code generation, you annotate functions and classes and let the generator create the provider:
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 generator:
flutter pub run build_runner watch --delete-conflicting-outputs
The generator creates counterProvider automatically.
Reading Providers in Widgets
Extend ConsumerWidget (or use ConsumerStatefulWidget) to get a WidgetRef:
class CounterDisplay extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('Count: $count');
}
}
class IncrementButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: const Text('Increment'),
);
}
}
ref.watch rebuilds the widget when the value changes. ref.read accesses the provider once without subscribing — use it in callbacks.
Async Providers
Async data fetching is a first-class citizen in Riverpod:
@riverpod
Future<List<Post>> posts(PostsRef ref) async {
final response = await http.get(Uri.parse('https://api.example.com/posts'));
return (jsonDecode(response.body) as List)
.map((e) => Post.fromJson(e))
.toList();
}
In the widget, handle loading/error/data states with when:
final postsAsync = ref.watch(postsProvider);
return postsAsync.when(
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
data: (posts) => PostList(posts: posts),
);
Provider Families
Families let you parameterize a provider — a different instance per parameter value:
@riverpod
Future<User> user(UserRef ref, String userId) async {
return api.getUser(userId);
}
Usage:
final user = ref.watch(userProvider('uid_123'));
Each unique userId gets its own provider instance, automatically cached and disposed when no longer watched.
Dependency Between Providers
Providers can depend on other providers using ref.watch inside the provider body:
@riverpod
Future<List<Post>> userPosts(UserPostsRef ref) async {
final userId = ref.watch(currentUserIdProvider);
return api.getPostsByUser(userId);
}
When currentUserIdProvider changes (user logs in/out), userPostsProvider automatically re-fetches. This reactive dependency graph is one of Riverpod's most powerful features.
Testing
Testing Riverpod providers is straightforward because they live outside the widget tree:
test('counter increments', () {
final container = ProviderContainer();
addTearDown(container.dispose);
expect(container.read(counterProvider), 0);
container.read(counterProvider.notifier).increment();
expect(container.read(counterProvider), 1);
});
Override providers for testing:
final container = ProviderContainer(
overrides: [
postsProvider.overrideWith(() => FakePostsNotifier()),
],
);
Conclusion
Riverpod's annotation-based API reduces boilerplate significantly compared to manual provider definitions, and the compile-time safety catches mistakes that Provider catches only at runtime. The reactive dependency graph, built-in async support, and easy testability make it the strongest Flutter state management solution available in 2026. If you are starting a new Flutter project or ready to graduate from Provider, Riverpod is the natural next step.
Sign in to like, dislike, or report.