Skip to main content

Reading a provider

Before reading this guide, make sure to read about 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 parameter:

final provider = Provider((ref) {
// use ref to obtain other providers
final repository = ref.watch(repositoryProvider);

return SomeValue(repository);
})

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

For example, a common use-case is to pass the provider's "ref" to a StateNotifier:

final counter = StateNotifierProvider<Counter, int>((ref) {
return Counter(ref);
});

class Counter extends StateNotifier<int> {
Counter(this.ref): super(0);

final Ref ref;

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

Doing so would allow 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.

warning

When a widget obtains a "ref", this "ref" should not be passed around. It should be used only by the widget that created this object.

Extending ConsumerWidget instead of StatelessWidget

The most common solution will be to replace StatelessWidget by ConsumerWidget when creating a widget.

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

A typical ConsumerWidget would look like:

class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);

@override
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 as a "ref" object.

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

class HomeView extends ConsumerStatefulWidget {
const HomeView({Key? key}): super(key: key);

@override
HomeViewState createState() => HomeViewState();
}

class HomeViewState extends ConsumerState<HomeView> {
@override
void initState() {
super.initState();
// "ref" can be used in all life-cycles of a StatefulWidget.
ref.read(counterProvider);
}

@override
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');
}
}

Extending HookConsumerWidget instead of HookWidget

This solution is specific to flutter_hooks users. Since flutter_hooks requires extending HookWidget to work, widgets that use hooks are unable to extend ConsumerWidget.

One solution is, through the package hooks_riverpod, replace HookWidget by HookConsumerWidget.
HookConsumerWidget acts as both a ConsumerWidget and a HookWidget. This allows a widget to both listen to providers and use hooks.

An example would be:

class HomeView extends HookConsumerWidget {
const HomeView({Key? key}): super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
// HookConsumerWidget allows using hooks inside the build method
final state = useState(0);

// We can also use the ref parameter to listen to providers.
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Consumer and HookConsumer widgets

A final solution for obtaining a "ref" inside widgets is to rely on Consumer/ HookConsumer.

These classes are widgets that can be used to obtain a "ref", with the same properties as ConsumerWidget/HookConsumerWidget.

These widgets can be a way to obtain a "ref" without having to define a class. An example would be:

Scaffold(
body: HookConsumer(
builder: (context, ref, child) {
// Like HookConsumerWidget, we can use hooks inside the builder
final state = useState(0);

// We can also use the ref parameter to listen to providers.
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. That can be done using ref.watch
  • adding a listener on a provider, to execute an action whenever that provider changes.
    That can be 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". That can be done using ref.read.
note

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

Using ref.watch to observe a provider

It is possible to use ref.watch inside the build method of a widget or inside the body of a provider to have the widget/provider listen to 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

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

final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none);
final todosProvider = StateNotifierProvider<TodoList, List<Todo>>((ref) => TodoList());

final filteredTodoListProvider = Provider((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 the filter or the list of tasks changed. Yet at the same time, the filtered list will not be recomputed if neither the filter nor the list of tasks changed.

Similarly, a widget could use ref.watch to show a user interface that depends on a provider:

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

class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);

@override
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 widgets that listens to a provider which stores a counter. And if that counter 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 onPressed or a 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 provider changes, using ref.listen will instead call a custom function.

That can be useful to perform actions when a certain change happens, such that to show a snackbar when an error happens.

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

final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter());

final anotherProvider = Provider((ref) {
ref.listen<int>(counterProvider, (int count) {
print('The counter changed ${count}');
});
...
});

or inside the build method of a widget:

final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter());

class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (int count) {
print('The counter changed ${count}');
});
...
}
}
caution

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

Using ref.read to obtain the state of a provider once

The ref.read method is a way to obtain the state of a provider, without any extra effect.

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

final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter());

class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);

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

It exists as a work-around for cases where using watch or listen would be otherwise too inconvenient to use.
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:

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

Widget build(BuildContext context, WidgetRef ref) {
// use "read" to ignore updates on a provider
final counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
)
}

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.

Softwares tend to change a lot, and it is likely that in the future, a value that previously never changed will need to change.
But if you used ref.read, when that value starts to change, you would have to go through your entire codebase to change ref.read into ref.watch – which is error prone and chances are you'll forget some cases.

Whereas if you used ref.watch to begin with, you wouldn't have any problem

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

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 offers various way to obtain a value while reducing the number of rebuilds, which you could use instead.

For example instead of

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

Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
)
}

we could do:

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

Widget build(BuildContext context, WidgetRef ref) {
// ".notifier" subscribes to the StateController instance but does
// not rebuild the widget if its state changes
StateController<int> counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
)
}

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 StateController object.

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

Deciding what to read

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

As 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 user.when(
    loading: () => const CircularProgressIndicator(),
    error: (error, stack) => const Text('Oops'),
    data: (user) => Text(user.name),
    );
    }
  • 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, 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. But in some cases, a widget/provider may only care about 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 some properties 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);
}

The way this works is, by using select, we are specifying a function that returns the property that we care about.

Then, 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.
But if they are equal (such as when the age changed), then Riverpod will not rebuild the widget.

info

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

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

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}'));