flutter
/

Flutter Riverpod – The Complete Guide to Reactive State Management

Last Sync: Today

On this page

15
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Flutter Riverpod – The Complete Guide to Reactive State Management

What is Riverpod?

Riverpod is a reactive state management library for Flutter that is built on top of the Provider package but offers several improvements. It is compile‑safe, scalable, and works seamlessly with Flutter’s reactive paradigm. Riverpod provides a way to define providers that encapsulate state and logic, and then allows widgets to read or listen to that state efficiently. It eliminates the need for BuildContext in many cases and prevents common pitfalls like accessing a provider before it exists.

Why Riverpod?

    • Compile‑safe: No runtime errors for missing providers.
    • Unified API: One consistent way to handle all types of state (simple values, futures, streams, mutable state).
    • Scoped providers: Providers can be overridden for specific parts of the widget tree.
    • Auto‑dispose: Providers can clean up resources automatically when no longer used.
    • Testable: Providers can be easily mocked and overridden in tests.

Adding Riverpod to Your Project

Add the riverpod package to your pubspec.yaml:

dependencies:
  flutter_riverpod: ^2.4.0
  riverpod: ^2.4.0

Then run flutter pub get.

For code generation, you may also need riverpod_annotation and build_runner, but we’ll start with the simple API.

Core Concepts

Riverpod revolves around providers. A provider is a declaration of a piece of state or a service. Providers are global, but they can be overridden locally. Widgets interact with providers using ref objects obtained from ConsumerWidget or Consumer widgets.

Creating a Simple Provider

The simplest provider is Provider, which just returns a value. Use it for services or immutable data.

DARTRead-only
1
final helloWorldProvider = Provider<String>((ref) => 'Hello, Riverpod!');

StateProvider for Mutable State

StateProvider holds a mutable piece of state that can be read and written.

DARTRead-only
1
final counterProvider = StateProvider<int>((ref) => 0);

// In a widget:
final count = ref.watch(counterProvider); // reads
ref.read(counterProvider.notifier).state++; // writes

FutureProvider for Asynchronous Data

FutureProvider handles asynchronous operations and manages loading, data, and error states.

DARTRead-only
1
final userProvider = FutureProvider<User>((ref) async {
  final response = await http.get(Uri.parse('https://api.example.com/user'));
  return User.fromJson(jsonDecode(response.body));
});

In your widget, you can watch the provider and react to its state:

DARTRead-only
1
final user = ref.watch(userProvider);
return user.when(
  data: (user) => Text(user.name),
  error: (error, stack) => Text('Error: $error'),
  loading: () => CircularProgressIndicator(),
);

StateNotifierProvider for Complex State

For more complex state that requires methods to modify it, use StateNotifierProvider with a StateNotifier subclass. This is similar to ChangeNotifier but with better integration with Riverpod.

DARTRead-only
1
class Todo {
  final String id;
  final String description;
  final bool completed;
  Todo({required this.id, required this.description, this.completed = false});
}

class TodoListNotifier extends StateNotifier<List<Todo>> {
  TodoListNotifier() : super([]);

  void addTodo(String description) {
    state = [...state, Todo(id: DateTime.now().toString(), description: description)];
  }

  void toggleTodo(String id) {
    state = state.map((todo) {
      if (todo.id == id) return Todo(id: todo.id, description: todo.description, completed: !todo.completed);
      return todo;
    }).toList();
  }
}

final todoListProvider = StateNotifierProvider<TodoListNotifier, List<Todo>>((ref) => TodoListNotifier());

Reading Providers in Widgets

To access providers, you need a ref object. This is available in:

  • ConsumerWidget (a StatelessWidget that has a ref parameter in its build method)
  • ConsumerStatefulWidget (StatefulWidget with ref in the State)
  • Consumer (a widget that exposes ref to its builder)
DARTRead-only
1
class CounterWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Text('$count');
  }
}

ref.watch vs ref.read vs ref.listen

    • ref.watch: Listens to a provider and rebuilds the widget when the provider’s value changes. Use it to display state.
    • ref.read: Reads the provider’s value once without listening. Use it for actions (like button taps).
    • ref.listen: Listens to changes without rebuilding the widget; useful for side effects like showing a snackbar.
DARTRead-only
1
// Inside a widget
final count = ref.watch(counterProvider); // rebuilds when counter changes

ElevatedButton(
  onPressed: () => ref.read(counterProvider.notifier).state++,
  child: Text('Increment'),
)

ref.listen<CounterState>(counterProvider, (previous, next) {
  if (next.value == 10) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Reached 10!')));
  }
});

Provider Modifiers

Riverpod provides modifiers to adjust provider behavior:

  • .family: Allows you to pass parameters to a provider.
  • .autoDispose: Automatically disposes the provider when it is no longer used (e.g., when the widget that watches it is removed).
DARTRead-only
1
// Family provider with autoDispose
final userProvider = FutureProvider.autoDispose.family<User, String>((ref, userId) async {
  return fetchUser(userId);
});

// Usage
final user = ref.watch(userProvider('123'));

Scoping Providers

You can override a provider for a part of the widget tree using ProviderScope. This is useful for testing or providing different values in different parts of the app.

DARTRead-only
1
ProviderScope(
  overrides: [
    counterProvider.overrideWithValue(42),
  ],
  child: MyApp(),
)

Best Practices

    • Use ref.watch only where needed – avoid watching providers high in the tree to prevent large rebuilds.
    • Prefer ConsumerWidget over Consumer for simpler code.
    • Use .autoDispose for providers that are not used throughout the app’s lifetime.
    • Name providers clearly (e.g., counterProvider, todoListNotifier).
    • Separate providers into logical files to keep your codebase organized.

Common Mistakes

    • Calling ref.watch inside a callback – This can cause the widget to rebuild at unexpected times. Use ref.read instead.
    • Not disposing resources – Use autoDispose or override dispose in your notifier to clean up streams, controllers, etc.
    • Using ref.read to watch a provider – ref.read does not rebuild the widget; use ref.watch for listening.
    • Passing providers as parameters – Instead, use ref.watch inside the widget to get the value.

Key Takeaways

    • Riverpod is a modern, compile‑safe state management solution.
    • Providers are the building blocks; they can be simple values, futures, streams, or mutable state.
    • Use ConsumerWidget to get a ref in your widgets.
    • ref.watch rebuilds the widget; ref.read gets a value without listening; ref.listen reacts to changes.
    • Use .family to pass parameters, and .autoDispose to clean up resources.
    • Scope providers with ProviderScope to override values in subtrees.

Try it yourself

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final counterProvider = StateProvider<int>((ref) => 0);

void main() {
  runApp(ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Riverpod Demo')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              CounterDisplay(),
              SizedBox(height: 20),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  CounterButton(delta: -1, text: '-'),
                  SizedBox(width: 20),
                  CounterButton(delta: 1, text: '+'),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class CounterDisplay extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Text('Count: $count', style: TextStyle(fontSize: 24));
  }
}

class CounterButton extends ConsumerWidget {
  final int delta;
  final String text;
  CounterButton({required this.delta, required this.text});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ElevatedButton(
      onPressed: () => ref.read(counterProvider.notifier).state += delta,
      child: Text(text),
    );
  }
}

Test Your Knowledge

Q1
of 4

Which widget should you use to get a `ref` object in a stateless widget?

A
StatelessWidget
B
ConsumerWidget
C
StatefulWidget
D
ProviderWidget
Q2
of 4

What method is used to listen to a provider and rebuild when it changes?

A
ref.read
B
ref.watch
C
ref.listen
D
ref.observe
Q3
of 4

How do you make a provider accept parameters?

A
.family
B
.param
C
.args
D
.withParam
Q4
of 4

What does `autoDispose` do?

A
Automatically destroys the provider when the app closes
B
Automatically disposes the provider when it is no longer used
C
Prevents the provider from being garbage collected
D
Keeps the provider alive forever

Frequently Asked Questions

What is the difference between Riverpod and Provider?

Provider is an older library that relies on BuildContext and can cause runtime errors if a provider is missing. Riverpod is a rewrite that is compile‑safe, does not require BuildContext, supports scoping, and has a unified API for different types of state. Riverpod also provides better testability and auto‑dispose functionality.

How do I combine multiple providers?

You can watch one provider inside another using ref.watch. For example, a provider that depends on another provider: final userGreetingProvider = Provider<String>((ref) => 'Hello, ${ref.watch(userProvider).name}');

How do I handle loading and error states with Riverpod?

For asynchronous providers like FutureProvider and StreamProvider, you can use the .when method on the returned AsyncValue. This gives you a clean way to handle loading, data, and error states.

Can I use Riverpod with existing ChangeNotifier code?

Yes, you can wrap a ChangeNotifier with ChangeNotifierProvider (from flutter_riverpod) or use StateNotifierProvider for a more Riverpod‑native approach. However, Riverpod’s own StateNotifier is more efficient.

How do I test a widget that uses Riverpod?

Wrap your widget in a ProviderScope with overrides for the providers you want to mock. Then you can use ref.read or ref.watch inside your tests to verify state.

Previous

flutter provider

Next

flutter getx

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.