Dart Tutorial

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 #

CallDoesUse in
ref.watch(p)Gets the value and rebuilds when it changesbuild
ref.read(p)Gets the value once, no rebuildCallbacks
ref.read(p.notifier)Gets the notifier, to call its methodsCallbacks
ref.listen(p, (prev, next) {...})Runs code on changebuild, 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 #

ProviderFor
ProviderA value that is computed or never changes: a service, or a value derived from other providers
NotifierProviderState with methods that change it
FutureProviderA value loaded asynchronously, once
StreamProviderValues arriving over time
AsyncNotifierProviderAsync 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.

Practise in the playground Updated by Santosh Adhikari