Salta al contenuto principale

Reading a Provider

caution

The content of this page may be outdated.
It will be updated in the future, but for now you may want to refer to the content in the top of the sidebar instead (introduction/essentials/case-studies/...)

Before reading this guide, make sure to Providers first.

In this guide, we will see how to consume a provider.

Obtaining a "ref" object

First and foremost, before reading a provider, we need to obtain a "ref" object.

This object is what allows us to interact with providers, be it from a widget or another provider.

Obtaining a "ref" from a provider

All providers receive a "ref" as a parameter:



String value(Ref ref) {
// use ref to obtain other providers
final repository = ref.watch(repositoryProvider);
return repository.get();
}

This parameter is safe to pass to the value exposed by the provider.



class Counter extends _$Counter {

int build() => 0;

void increment() {
// Counter can use the "ref" to read other providers
final repository = ref.read(repositoryProvider);
repository.post('...');
}
}

Doing so allows our Counter class to read providers.

Obtaining a "ref" from a widget

Widgets naturally do not have a ref parameter. But Riverpod offers multiple solutions to obtain one from widgets.

Extending ConsumerWidget instead of StatelessWidget

The most common way to obtain a ref in the widget tree is to replace StatelessWidget with ConsumerWidget.

ConsumerWidget is identical in use to StatelessWidget, with the only difference being that it has an extra parameter on its build method: the "ref" object.

A typical ConsumerWidget looks like:


class HomeView extends ConsumerWidget {
const HomeView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
// use ref to listen to a provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Extending ConsumerStatefulWidget+ConsumerState instead of StatefulWidget+State

Similar to ConsumerWidget, ConsumerStatefulWidget and ConsumerState are the equivalent of a StatefulWidget with its State, with the difference that the state has a "ref" object.

This time, the "ref" isn't passed as parameter of the build method, but is a property of the ConsumerState object:


class HomeView extends ConsumerStatefulWidget {
const HomeView({super.key});


HomeViewState createState() => HomeViewState();
}

class HomeViewState extends ConsumerState<HomeView> {

void initState() {
super.initState();
// "ref" can be used in all life-cycles of a StatefulWidget.
ref.read(counterProvider);
}


Widget build(BuildContext context) {
// We can also use "ref" to listen to a provider inside the build method
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Using ref to interact with providers

Now that we have a "ref", we can start using it.

There are three primary usages for "ref":

  • obtaining the value of a provider and listening to changes, such that when this value changes, this will rebuild the widget or provider that subscribed to the value. This is done using ref.watch
  • adding a listener on a provider, to execute an action such as navigating to a new page or showing a modal whenever that provider changes.
    This is done using ref.listen.
  • obtaining the value of a provider while ignoring changes. This is useful when we need the value of a provider in an event such as "on click". This is done using ref.read.
note

Whenever possible, prefer using ref.watch over ref.read or ref.listen to implement a feature.
By relying on ref.watch, your application becomes both reactive and declarative, which makes it more maintainable.

Using ref.watch to observe a provider

ref.watch is used inside the build method of a widget or inside the body of a provider to have the widget/provider listen to a provider:

For example, a provider could use ref.watch to combine multiple providers into a new value.

An example would be filtering a todo-list. We could have two providers:

  • filterTypeProvider, a provider that exposes the current type of filter (none, show only completed tasks, ...)
  • todosProvider, a provider that exposes the entire list of tasks

And by using ref.watch, we could make a third provider that combines both providers to create a filtered list of tasks:



FilterType filterType(Ref ref) {
return FilterType.none;
}


class Todos extends _$Todos {

List<Todo> build() {
return [];
}
}


List<Todo> filteredTodoList(Ref ref) {
// obtains both the filter and the list of todos
final FilterType filter = ref.watch(filterTypeProvider);
final List<Todo> todos = ref.watch(todosProvider);

switch (filter) {
case FilterType.completed:
// return the completed list of todos
return todos.where((todo) => todo.isCompleted).toList();
case FilterType.none:
// returns the unfiltered list of todos
return todos;
}
}

With this code, filteredTodoListProvider now exposes the filtered list of tasks.

The filtered list will also automatically update if either the filter or the list of tasks changed. At the same time, the filtered list will not be recomputed if neither the filter nor the list of tasks changed.

Similarly, a widget can use ref.watch to show the content from a provider and update the user interface whenever that content changes:



int counter(Ref ref) => 0;

class HomeView extends ConsumerWidget {
const HomeView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
// use ref to listen to a provider
final counter = ref.watch(counterProvider);

return Text('$counter');
}
}

This snippet shows a widget that listens to a provider which stores a count. And if that count changes, the widget will rebuild and the UI will update to show the new value.

caution

The watch method should not be called asynchronously, like inside an onPressed of an ElevatedButton. Nor should it be used inside initState and other State life-cycles.

In those cases, consider using ref.read instead.

Using ref.listen to react to a provider change

Similarly to ref.watch, it is possible to use ref.listen to observe a provider.

The main difference between them is that, rather than rebuilding the widget/provider if the listened to provider changes, using ref.listen will instead call a custom function.

That can be useful for performing actions when a certain change happens, such as showing a snackbar when an error happens.

The ref.listen method needs 2 positional arguments, the first one is the Provider and the second one is the callback function that we want to execute when the state changes. The callback function when called will be passed 2 values, the value of the previous State and the value of the new State.

The ref.listen method can be used inside the body of a provider:



void another(Ref ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
// ...
}

or inside the build method of a widget:



class Counter extends _$Counter {

int build() => 0;
}

class HomeView extends ConsumerWidget {
const HomeView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});

return Container();
}
}
caution

The listen method should not be called asynchronously, like inside an onPressed of an ElevatedButton. Nor should it be used inside initState and other State life-cycles.

Using ref.read to obtain the state of a provider

The ref.read method is a way to obtain the state of a provider without listening to it.

It is commonly used inside functions triggered by user interactions. For example, we can use ref.read to increment a counter when a user clicks a button:



class Counter extends _$Counter {

int build() => 0;
void increment() => state = state + 1;
}

class HomeView extends ConsumerWidget {
const HomeView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// Call `increment()` on the `Counter` class
ref.read(counterProvider.notifier).increment();
},
),
);
}
}
note

Using ref.read should be avoided as much as possible because it is not reactive.

It exists for cases where using watch or listen would cause issues. If you can, it is almost always better to use watch/listen, especially watch.

DON'T use ref.read inside the build method

You might be tempted to use ref.read to optimize the performance of a widget by doing:



class Counter extends _$Counter {

int build() => 0;
void increment() => state = state + 1;
}

Widget build(BuildContext context, WidgetRef ref) {
// use "read" to ignore updates on a provider
final counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: counter.increment,
child: const Text('button'),
);
}

But this is a very bad practice and can cause bugs that are difficult to track.

Using ref.read this way is commonly associated with the thought "The value exposed by a provider never changes so using 'ref.read' is safe". The problem with this assumption is that, while today that provider may indeed never update its value, there is no guarantee that tomorrow will be the same.

Software tends to change a lot, and it is likely that in the future, a value that previously never changed will need to change.
If you use ref.read, when that value needs to change, you have to go through your entire codebase to change ref.read into ref.watch – which is error prone and you are likely to forget some cases.

If you use ref.watch to begin with, you will have fewer problems when refactoring.

But I wanted to use ref.read to reduce the number of times my widget rebuilds

While the goal is commendable, it is important to note that you can achieve the exact same effect (reducing the number of builds) using ref.watch instead.

Providers offer various ways to obtain a value while reducing the number of rebuilds, which you could use instead.

For example instead of



class Counter extends _$Counter {

int build() => 0;
void increment() => state = state + 1;
}

Widget build(BuildContext context, WidgetRef ref) {
Counter counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.increment(),
child: const Text('button'),
);
}

we could do:



class Counter extends _$Counter {

int build() => 0;
void increment() => state = state + 1;
}

Widget build(BuildContext context, WidgetRef ref) {
Counter counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.increment(),
child: const Text('button'),
);
}

Both snippets achieve the same effect: our button will not rebuild when the counter increments.

On the other hand, the second approach supports cases where the counter is reset. For example, another part of the application could call:

ref.refresh(counterProvider);

which would recreate the Counter object.

If we used ref.read here, our button would still use the previous Counter instance, which was disposed and should no-longer be used. Whereas using ref.watch correctly rebuilds the button to use the new Counter.

Deciding what to read

Depending on the provider you want to listen to, you may have multiple possible values that you can listen to.

As an example, consider the following StreamProvider:

final userProvider = StreamProvider<User>(...);

When reading this userProvider, you can:

  • synchronously read the current state by listening to userProvider itself:

    Widget build(BuildContext context, WidgetRef ref) {
    AsyncValue<User> user = ref.watch(userProvider);

    return switch (user) {
    AsyncData(:final value) => Text(value.name),
    AsyncError(:final error) => const Text('Oops $error'),
    _ => const CircularProgressIndicator(),
    };
    }
  • obtain the associated Stream, by listening to userProvider.stream:

    Widget build(BuildContext context, WidgetRef ref) {
    Stream<User> user = ref.watch(userProvider.stream);
    }
  • obtain a Future that resolves with the latest value emitted, by listening to userProvider.future:

    Widget build(BuildContext context, WidgetRef ref) {
    Future<User> user = ref.watch(userProvider.future);
    }

Other providers may offer different alternative values.
For more information, refer to the documentation of each provider by consulting the API reference.

Using "select" to filter rebuilds

One final feature to mention related to reading providers is the ability to reduce the number of times a widget/provider rebuilds from ref.watch, or how often ref.listen executes a function.

This is important to keep in mind as, by default, listening to a provider listens to the entire object state. But sometimes, a widget/provider may only care about changes to some properties instead of the whole object.

For example, a provider may expose a User:

abstract class User {
String get name;
int get age;
}

But a widget may only use the user name:

Widget build(BuildContext context, WidgetRef ref) {
User user = ref.watch(userProvider);
return Text(user.name);
}

If we naively used ref.watch, this would rebuild the widget when the user's age changes.

The solution is to use select to explicitly tell Riverpod that we only want to listen to the name property of the User.

The updated code would be:

Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name));
return Text(name);
}

By using select, we are able to specify a function that returns the property that we care about.

Whenever the User changes, Riverpod will call this function and compare the previous and new result. If they are different (such as when the name changed), Riverpod will rebuild the widget.
However, if they are equal (such as when the age changed), Riverpod will not rebuild the widget.

info

It is also possible to use select with ref.listen:

ref.listen<String>(
userProvider.select((user) => user.name),
(String? previousName, String newName) {
print('The user name changed $newName');
}
);

Doing so will call the listener only when the name changes.

tip

You don't have to return a property of the object. Any value that overrides == will work. For example you could do:

final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));