Salta al contenuto principale

Combining requests

Up till now, we've only seens cases were requests are independent from 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 parameter to another a 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 your other provider.
  • Whenever the parameter change, a brand new state will be made. By passing parameters, there are 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 combine 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 which all providers have access to. 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 parameter to the provider's function:


int example(ExampleRef 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(LocationRef ref) {
// TO-DO: Return a stream which obtains the current location
return someStream;
}


Future<List<String>> restaurantsNearMe(RestaurantsNearMeRef 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();
}
info

When the listened provider changes and our request recomputes, the previous state is kept until the new request completes.
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.

info

Notice how we used ref.watch(locationProvider.future) instead of ref.watch(locationProvider). That if 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 wouldn't be able to do anything.

caution

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(ExampleRef 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/listenSelf 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 thar 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(ExampleRef ref) {
ref.listen(otherProvider, (previous, next) {
print('Changed from: $previous, next: $next');
});

return 0;
}
info

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 be only be used in placed where you can't use ref.watch, such as inside methods of Notifiers.


class MyNotifier extends _$MyNotifier {

int build() {
// Bad! Do not use "ref" here as it is not reactive
ref.read(otherProvider);

return 0;
}

void increment() {
ref.read(otherProvider); // Using "read" here is fine
}
}
caution

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.