Riverpod — Compiled Dependency Management for Flutter

Author: IT Sectr Published: 2026-02-19 Reading time: 7 min

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

  • ProviderRef — an object for accessing other providers inside a provider
  • AsyncValue — a wrapper for asynchronous data with loading/error/data states
  • Notifier — a class for mutable state with mutation methods
  • ProviderScope — the root widget that manages all providers
  • Code Generation — @riverpod annotations for automatic provider generation

What is Riverpod?

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.

Provider Types

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).

Dart
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 and Working with Asynchrony

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.

Dart
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 and @riverpod

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.

Dart
@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.

Riverpod vs Provider

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.

FeatureProviderRiverpod
BuildContext dependencyYesNo
Compile-time checkNoYes (via @riverpod)
ProviderNotFoundExceptionRuntimeImpossible
AsynchronyManualAsyncValue (built-in)
TestingWrapper in ProviderProviderScope.overrideWith
CachingNoAutomatic + 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

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.

Dart
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

How is Riverpod different from BLoC?

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.

What is autodispose in Riverpod?

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.

How does ref.invalidate work?

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.

Can Riverpod be used without code generation?

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.

What are Family providers?

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

  • Riverpod — a compiled state manager, successor to Provider without ProviderNotFoundException
  • ProviderRef — a replacement for BuildContext for accessing providers inside other providers
  • AsyncValue — a sealed class with loading/error/data states for asynchronous data
  • @riverpod code generation — automatic type inference and provider factories
  • ProviderScope.overrideWith — isolated testing without mocks and DI containers
  • Family — parameterized providers with individual caching
  • autodispose and keepAlive — automatic provider lifecycle management

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.

Discuss the project

Read also