Combining requests
Up until now, we've only seen cases where requests are independent of each other. But a common use-case is to have to trigger a request based on the result of another request.
We could be using the Passing arguments to your requests mechanism to do that, by passing the result of a provider as a parameter to another provider.
But this approach has a few downsides:
- This leaks implementation details. Now, your UI needs to know about all the providers that are used by your other provider.
- Whenever the parameter changes, a brand new state will be created. By passing parameters, there is no way to keep the previous state when the parameter changes.
- It makes combining requests harder.
- This makes tooling less useful. A devtool wouldn't know about the relationship between providers.
To improve this, Riverpod offers a different approach to combining requests.
The basics: Obtaining a "ref"
All possible ways of combining requests have one thing in common:
They are all based on the Ref
object.
The Ref
object is an object to which all providers have access.
It grants them access to various life-cycle listeners, but also
various methods to combine providers.
The place where Ref
can be obtained depends on the type of provider.
In functional providers, the Ref
is passed as a parameter to the
provider's function:
int example(Ref ref) {
// "Ref" can be used here to read other providers
final otherValue = ref.watch(otherProvider);
return 0;
}
In class variants, the Ref
is a property of the Notifier class:
class Example extends _$Example {
int build() {
// "Ref" can be used here to read other providers
final otherValue = ref.watch(otherProvider);
return 0;
}
}
Using ref to read a provider.
The ref.watch
method.
Now that we've obtained a Ref
, we can use it to combine requests.
The main way to do so is by using ref.watch
.
It is generally recommended to architecture your code such that you
can use ref.watch
over other options, as it is generally easier to maintain.
The ref.watch
method takes a provider, and returns its current state.
Then, whenever the listened provider changes, our provider will be
invalidated and rebuilt next frame or on next read.
By using ref.watch
, your logic becomes both "reactive" and "declarative".
Meaning that your logic will automatically recompute when needed.
And that the update mechanism doesn't rely on side-effects, such as an "on change".
This is similar to how StatelessWidgets behave.
As an example, we could define a provider that listens to the user's location. Then, we could use this location to fetch the list of restaurants near the user.
Stream<({double longitude, double latitude})> location(Ref ref) {
// TO-DO: Return a stream which obtains the current location
return someStream;
}
Future<List<String>> restaurantsNearMe(Ref ref) async {
// We use "ref.watch" to obtain the latest location.
// By specifying that ".future" after the provider, our code will wait
// for at least one location to be available.
final location = await ref.watch(locationProvider.future);
// We can now make a network request based on that location.
// For example, we could use the Google Map API:
// https://developers.google.com/maps/documentation/places/web-service/search-nearby
final response = await http.get(
Uri.https('maps.googleapis.com', 'maps/api/place/nearbysearch/json', {
'location': '${location.latitude},${location.longitude}',
'radius': '1500',
'type': 'restaurant',
'key': '<your api key>',
}),
);
// Obtain the restaurant names from the JSON
final json = jsonDecode(response.body) as Map;
final results = (json['results'] as List).cast<Map<Object?, Object?>>();
return results.map((e) => e['name']! as String).toList();
}
When the listened to provider changes and our request recomputes, the previous
state is kept until the new request is completed.
At the same time, while the request is pending, the "isLoading" and "isReloading"
flags will be set.
This enables UI to either show the previous state or a loading indicator, or even both.
Notice how we used ref.watch(locationProvider.future)
instead of ref.watch(locationProvider)
.
That is because our locationProvider
is asynchronous. As such, we want to
await for an initial value to be available.
If we omit that .future
, we would receive an AsyncValue
, which is a snapshot
of the current state of the locationProvider
. But if no location is available yet,
we won't be able to do anything.
It is considered bad practice to call ref.watch
inside code that is executed
"imperatively". Meaning any code that is possibly not executed during the build
phase of the provider. This includes "listener" callbacks or methods on Notifiers:
int example(Ref ref) {
ref.watch(otherProvider); // Good!
ref.onDispose(() => ref.watch(otherProvider)); // Bad!
final someListenable = ValueNotifier(0);
someListenable.addListener(() {
ref.watch(otherProvider); // Bad!
});
return 0;
}
class MyNotifier extends _$MyNotifier {
int build() {
ref.watch(otherProvider); // Good!
ref.onDispose(() => ref.watch(otherProvider)); // Bad!
return 0;
}
void increment() {
ref.watch(otherProvider); // Bad!
}
}
The ref.listen
methods.
The ref.listen
method is an alternative to ref.watch
.
It is similar to your traditional "listen"/"addListener" method. It takes a provider
and a callback, and will invoke said callback whenever the content of the provider
changes.
Refactoring your code such that you can use ref.watch
instead of ref.listen
is generally recommended, as the latter is more error-prone due to its imperative nature.
But ref.listen
can be helpful to add some quick logic without having to do
significant refactor.
We could rewrite the ref.watch
example to use ref.listen
instead
int example(Ref ref) {
ref.listen(otherProvider, (previous, next) {
print('Changed from: $previous, next: $next');
});
return 0;
}
It is entirely safe to use ref.listen
during the build phase of a provider.
If the provider somehow is recomputed, previous listeners will be removed.
Alternatively, you can use the return value of ref.listen
to remove the listener
manually when you wish.
The ref.read
method.
The last option available is ref.read
.
It is similar to ref.watch
in that it returns the current state of a provider.
But unlike ref.watch
, it doesn't listen to the provider.
As such, ref.read
should only be used in places where you can't use
ref.watch
, such as inside methods of Notifiers.
class MyNotifier extends _$MyNotifier {
int build() {
// Bad! Do not use "read" here as it is not reactive
ref.read(otherProvider);
return 0;
}
void increment() {
ref.read(otherProvider); // Using "read" here is fine
}
}
Be careful when using ref.read
on a provider as, since it doesn't listen to the
provider, said provider may decide to destroy its state if it isn't listened.