Riverpod — a compiled state and dependency manager for Flutter, created by Remi Rousselet in 2021 as a successor to Provider. Riverpod solves fundamental Provider problems: lack of compile-time checking, dependency on BuildContext, and complexity with ProviderNotFoundException. According to pub.dev, the package has over 5 thousand likes and is actively replacing Provider in new projects.
Key Takeaways
Riverpod is a state management and dependency injection library for Flutter that compiles provider descriptions into safe Dart code. Unlike Provider, Riverpod providers are not tied to BuildContext: they are created globally or in ProviderScope and are accessible from anywhere. The compiler checks types, dependencies and the integrity of the provider graph at build time, eliminating runtime errors like ProviderNotFoundException.
Riverpod uses the override model for testing: each provider can be overridden via ProviderScope.overrideWith without creating subclasses or mocking interfaces. This makes testing isolated: each test gets its own copy of the dependency graph that is fully controlled.
According to the Flutter Community Survey 2025, Riverpod ranks third in popularity after Provider and BLoC. However, Riverpod is the fastest-growing package: +120% installs in 2024. The main reasons: compile-time safety, no ProviderNotFoundException, built-in async support via AsyncValue.
Riverpod provides 8 types of providers, each for a specific scenario: Provider (constant/service), StateProvider (primitive state), StateNotifierProvider (complex logic with StateNotifier), ChangeNotifierProvider (for migration from Provider), FutureProvider (async data, one-time), StreamProvider (reactive stream), NotifierProvider (new API, Flutter 3.10+) and AsyncNotifierProvider (async Notifier).
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
void decrement() => state--;
}
class CounterScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}ProviderRef — an object passed to each provider for accessing other providers. ref.watch — subscription to changes, ref.read — one-time read, ref.invalidate — cache reset. ProviderRef replaces BuildContext from Provider: any provider can read other providers without access to the widget tree. This allows building a dependency graph outside the UI layer.
ProviderScope — the root widget, mandatory for Riverpod to work. ProviderScope stores all providers, manages their lifecycle and caches values. Without ProviderScope the app will crash with ProviderNotFoundException. ProviderScope can be nested — a nested scope overrides parent providers, which is used for testing and feature isolation.
AsyncValue — a Riverpod sealed class for representing asynchronous state. AsyncValue has three variants: AsyncData (successful data), AsyncError (error), AsyncLoading (loading). Instead of manually switching between loading/error/data, each FutureProvider or StreamProvider automatically returns AsyncValue, and the widget handles all three states via ref.watch.
final userProvider = FutureProvider((ref) async {
final api = ref.watch(apiProvider);
return await api.fetchUser();
});
class UserScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider);
return userAsync.when(
data: (user) => UserWidget(user),
error: (e, _) => ErrorWidget(e.toString()),
loading: () => CircularProgressIndicator(),
);
}
}AsyncValue.when — a method for pattern-matching all three states. The compiler checks that all three cases are handled — if you forget loading or error, the code won't compile. AsyncValue.whenData — only for data (if loading/error are not needed). AsyncValue.guard — a wrapper over try-catch for converting exceptions to AsyncError. keepAlive — a flag that prevents the provider cache from being destroyed when it goes out of scope.
Code generation — a key feature of Riverpod 2.0+. The @riverpod annotation on a function automatically generates a provider with the correct type, refactoring support and autocomplete. Code generation uses riverpod_generator and build_runner. The developer writes a pure function, and everything else — types, classes, factory constructors — is generated automatically.
@riverpod
String helloWorld(HelloWorldRef ref) {
return 'Hello World';
}
// Generated: final helloWorldProvider = Provider((ref) => 'Hello World');
@riverpod
class Counter extends _$Counter {
int build() => 0;
void increment() => state++;
}Notifier — the new API for mutable state with code generation. Notifier is a class with a build() method and state mutation methods. Unlike StateNotifier, Notifier does not require a separate state class and provides direct access to state via getter/setter. Riverpod automatically generates a NotifierProvider for each Notifier class annotated with @riverpod.
build_runner: code generation is launched with the command dart run build_runner build. Generated files have the .g.dart suffix and are imported into the source code. When annotations or provider types change, code generation needs to be re-run. Riverpod 2.x recommends code generation for all new projects — manual provider creation is becoming deprecated.
Key differences between Riverpod and Provider: independence from BuildContext, compile-time safety, built-in async support, auto-caching, and testing via override. Provider requires BuildContext to access state (context.watch, context.read), Riverpod uses WidgetRef and globally declared providers.
| Feature | Provider | Riverpod |
|---|---|---|
| BuildContext dependency | Yes | No |
| Compile-time check | No | Yes (via @riverpod) |
| ProviderNotFoundException | Runtime | Impossible |
| Asynchrony | Manual | AsyncValue (built-in) |
| Testing | Wrapper in Provider | ProviderScope.overrideWith |
| Caching | No | Automatic + keepAlive |
Migration from Provider: Riverpod supports ChangeNotifierProvider.adaptive for using existing ChangeNotifier without rewriting. Gradual migration: first new features are written with Riverpod, then old Provider instances are replaced with Riverpod providers via an adapter. Both packages can coexist in one project, allowing migration without freezing development.
Testing Riverpod is built on ProviderScope.overrideWith. Each provider is overridden inside a test ProviderScope without mocks or DI containers. ProviderContainer — an isolated environment for tests without Flutter (pure Dart), allowing providers to be tested without widget rendering.
import 'package:flutter_test/flutter_test.dart';
import 'package:riverpod/riverpod.dart';
void main() {
test('Counter increments correctly', () {
final container = ProviderContainer();
container.read(counterProvider.notifier).increment();
expect(container.read(counterProvider), 1);
});
testWidgets('UI updates on increment', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [counterProvider.overrideWithValue(5)],
child: CounterScreen(),
),
);
expect(find.text('5'), findsOneWidget);
});
}ProviderContainer — without Flutter. Use ProviderContainer for unit testing providers without widgets. overrideWithValue — replacing a provider with a specific value. overrideWith — replacing with a provider factory (for mocking services). autodispose — in tests check that the provider is destroyed when it goes out of scope using container.dispose().
Frequently Asked Questions
Riverpod is a state management library with global providers, AsyncValue and code generation. BLoC is an architectural pattern with Event → Stream → State. Riverpod is easier to learn and has better DX through @riverpod annotations. BLoC provides strict business logic isolation and Event tracing via BlocObserver. The choice depends on the project paradigm: Riverpod is closer to Provider, BLoC — to reactive streams.
Autodispose is a mechanism for automatically destroying a provider when no one is subscribed to it. By default, all Riverpod providers autodispose: when a widget exits the tree, the provider is removed from memory. keepAlive — a flag that disables autodispose for providers that should always live (API clients, repositories, settings). This prevents memory leaks — unused providers are automatically destroyed.
ref.invalidate — a method that forcibly resets the provider cache. After invalidate, the provider is recreated on the next read: FutureProvider re-executes the async function, StreamProvider re-subscribes to the stream. Use invalidate to force data refresh (pull-to-refresh, user switch). ref.refresh — a combination of invalidate + read: resets and immediately reads the new value in one operation.
Yes. Riverpod 1.x works only without code generation — providers are created manually using Provider(), StateNotifierProvider(), FutureProvider(), etc. Riverpod 2.x supports both approaches. Without code generation there is more boilerplate but no dependency on build_runner and dart run build_runner build. For small projects (up to 30 providers), manual creation is justified; for large ones, code generation is mandatory.
Family — a provider modifier that accepts an external parameter. For example, userProvider(123) — a provider that loads a user with ID 123. Family providers cache the result for each unique parameter separately. Use Family for lists of items where each item is loaded by ID. The Family modifier is available for all provider types: Provider.family, FutureProvider.family, StreamProvider.family.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also