Flutter Lesson 41 of 83 4 min read
Riverpod in Flutter: Providers, Notifiers and Async State
Learn Riverpod for Flutter: ProviderScope, ref.watch and ref.read, Notifier, FutureProvider and AsyncValue, with complete examples.
On this page
Riverpod was written by the author of Provider to fix its weak points. The main differences:
- Providers are global declarations, not widgets in the tree. There is no “provider not found” error at run time.
- It does not depend on
BuildContext, so providers can be used outside widgets and combine easily. - Loading and error states for async data are built in.
- Mistakes are caught at compile time.
flutter pub add flutter_riverpod
Setup #
Wrap the app in a ProviderScope. It stores the state of every provider.
void main() {
runApp(const ProviderScope(child: MyApp()));
}
A counter #
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1. A notifier holds state and the methods that change it.
class CounterNotifier extends Notifier<int> {
@override
int build() => 0; // the initial state
void increment() => state++;
void reset() => state = 0;
}
// 2. A provider exposes it. Declared once, at the top level.
final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
void main() {
runApp(const ProviderScope(child: MaterialApp(home: CounterPage())));
}
// 3. A ConsumerWidget gets a `ref` for talking to providers.
class CounterPage extends ConsumerWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider); // rebuilds when it changes
return Scaffold(
appBar: AppBar(
title: const Text('Riverpod counter'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => ref.read(counterProvider.notifier).reset(),
),
],
),
body: Center(child: Text('$count', style: const TextStyle(fontSize: 48))),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: const Icon(Icons.add),
),
);
}
}
ref.watch, ref.read and ref.listen #
| Call | Does | Use in |
|---|---|---|
ref.watch(p) | Gets the value and rebuilds when it changes | build |
ref.read(p) | Gets the value once, no rebuild | Callbacks |
ref.read(p.notifier) | Gets the notifier, to call its methods | Callbacks |
ref.listen(p, (prev, next) {...}) | Runs code on change | build, for snackbars and navigation |
The same rule as Provider: watch in build, read in callbacks.
State must be replaced, not mutated #
Riverpod notices a change when state is assigned a new value. Changing a list in place does nothing.
class TodosNotifier extends Notifier<List<String>> {
@override
List<String> build() => [];
void add(String todo) => state = [...state, todo]; // new list
void remove(String todo) => state = state.where((t) => t != todo).toList();
// state.add(todo); // wrong: same list object, no rebuild
}
final todosProvider = NotifierProvider<TodosNotifier, List<String>>(TodosNotifier.new);
Kinds of provider #
| Provider | For |
|---|---|
Provider | A value that is computed or never changes: a service, or a value derived from other providers |
NotifierProvider | State with methods that change it |
FutureProvider | A value loaded asynchronously, once |
StreamProvider | Values arriving over time |
AsyncNotifierProvider | Async state with methods that change it |
Providers that depend on providers #
Inside a provider, ref.watch reads another provider. When that one changes, this one is recomputed automatically.
final filterProvider = NotifierProvider<FilterNotifier, String>(FilterNotifier.new);
class FilterNotifier extends Notifier<String> {
@override
String build() => '';
void set(String value) => state = value;
}
final filteredTodosProvider = Provider<List<String>>((ref) {
final todos = ref.watch(todosProvider);
final filter = ref.watch(filterProvider).toLowerCase();
return todos.where((t) => t.toLowerCase().contains(filter)).toList();
});
A widget watches filteredTodosProvider and never needs to know how it is produced.
Async data with FutureProvider #
final userProvider = FutureProvider<String>((ref) async {
await Future<void>.delayed(const Duration(seconds: 1)); // pretend network call
return 'Asha Rai';
});
class UserGreeting extends ConsumerWidget {
const UserGreeting({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userProvider); // an AsyncValue<String>
return user.when(
loading: () => const CircularProgressIndicator(),
error: (error, stackTrace) => Text('Something went wrong: $error'),
data: (name) => Text('Hello, $name'),
);
}
}
AsyncValue forces you to handle all three states: loading, error and data. Forget one and the code does not compile.
To reload: ref.invalidate(userProvider).
Parameters with family #
final productProvider = FutureProvider.family<Product, int>((ref, id) async {
return ref.watch(apiProvider).fetchProduct(id);
});
// In a widget
final product = ref.watch(productProvider(42));
Each id gets its own cached state.
Releasing state with autoDispose #
By default a provider’s state is kept for the life of the app. Add autoDispose to throw it away when nothing is listening, which suits per-screen data.
final searchProvider = FutureProvider.autoDispose.family<List<Product>, String>((ref, query) async {
return ref.watch(apiProvider).search(query);
});
Using ref in a StatefulWidget #
Use ConsumerStatefulWidget and ConsumerState. ref is then a property of the state.
class SearchPage extends ConsumerStatefulWidget {
const SearchPage({super.key});
@override
ConsumerState<SearchPage> createState() => _SearchPageState();
}
class _SearchPageState extends ConsumerState<SearchPage> {
@override
Widget build(BuildContext context) {
final results = ref.watch(filteredTodosProvider);
return ListView(children: [for (final r in results) Text(r)]);
}
}
To rebuild only part of a widget, wrap that part in Consumer(builder: (context, ref, child) => ...).
Code generation #
Riverpod can also generate providers from annotated functions and classes with the riverpod_generator package (@riverpod). It removes boilerplate and is recommended for larger projects. The concepts are identical, so learn the hand-written form first.
Try it yourself #
Build a to-do app with Riverpod: a NotifierProvider for the list, another for a “show completed” filter, and a derived Provider that combines them. Show the count of remaining tasks in the app bar.