Reading a Provider
本页内容可能已经过时。
今后会进行更新,但目前您可能需要参考侧边栏顶部的内容(介绍/要点/应用案例/......)。
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 usingref.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
.
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.
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();
}
}
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();
},
),
);
}
}
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.
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.
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}'));