Passing arguments to your requests
In a previous article, we saw how we could define a "provider" to make
a simple GET HTTP request.
But often, HTTP requests depend on external parameters.
For example, previously we used the Bored API
to suggest a random activity to users.
But maybe users would want to filter the type of activity they want to do,
or have price requirements, etc...
These parameters are not known in advance. So we need a way to pass
these parameters from our UI to our providers.
Updating our providers to accept arguments
As a reminder, previously we defined our provider like this:
// A "functional" provider
Future<Activity> activity(Ref ref) async {
// TODO: perform a network request to fetch an activity
return fetchActivity();
}
// Or alternatively, a "notifier"
class ActivityNotifier2 extends _$ActivityNotifier2 {
Future<Activity> build() async {
// TODO: perform a network request to fetch an activity
return fetchActivity();
}
}
Future<Activity> activity(
Ref ref,
// We can add arguments to the provider.
// The type of the parameter can be whatever you wish.
String activityType,
) async {
// We can use the "activityType" argument to build the URL.
// This will point to "https://boredapi.com/api/activity?type=<activityType>"
final response = await http.get(
Uri(
scheme: 'https',
host: 'boredapi.com',
path: '/api/activity',
// No need to manually encode the query parameters, the "Uri" class does it for us.
queryParameters: {'type': activityType},
),
);
final json = jsonDecode(response.body) as Map<String, dynamic>;
return Activity.fromJson(json);
}
class ActivityNotifier2 extends _$ActivityNotifier2 {
/// Notifier arguments are specified on the build method.
/// There can be as many as you want, have any name, and even be optional/named.
Future<Activity> build(String activityType) async {
// Arguments are also available with "this.<argumentName>"
print(this.activityType);
// TODO: perform a network request to fetch an activity
return fetchActivity();
}
}
When passing arguments to providers, it is highly encouraged to
enable "autoDispose" on the provider.
Failing to do so may result in memory leaks.
See Clearing cache and reacting to state disposal for more details.
Updating our UI to pass arguments
Previously, widgets consumed our provider like this:
AsyncValue<Activity> activity = ref.watch(activityProvider);
But now that our provider receives arguments, the syntax to consume it is slightly
different. The provider is now a function, which needs to be invoked with the parameters
requested.
We could update our UI to pass a hard-coded type of activity like this:
AsyncValue<Activity> activity = ref.watch(
// The provider is now a function expecting the activity type.
// Let's pass a constant string for now, for the sake of simplicity.
activityProvider('recreational'),
);
The parameters passed to the provider corresponds to the parameters of the annotated function, minus the "ref" parameter.
It is entirely possible to listen to the same provider with different arguments
simultaneously.
For example, our UI could render both "recreational" and "cooking" activities:
return Consumer(
builder: (context, ref, child) {
final recreational = ref.watch(activityProvider('recreational'));
final cooking = ref.watch(activityProvider('cooking'));
// We can then render both activities.
// Both requests will happen in parallel and correctly be cached.
return Column(
children: [
Text(recreational.valueOrNull?.activity ?? ''),
Text(cooking.valueOrNull?.activity ?? ''),
],
);
},
);
Caching considerations and parameter restrictions
When passing parameters to providers, the computation is still cached. The difference is that the computation is now cached per-argument.
This means that if two widgets consumes the same provider with the same
parameters, only a single network request will be made.
But if two widgets consumes the same provider with different parameters,
two network requests will be made.
For this to work, Riverpod relies on the ==
operator of the parameters.
As such, it is important that the parameters passed to the provider
have consistent equality.
A common mistake is to directly instantiate a new object as the parameter
of a provider, when that object does not override ==
.
For example, you may be tempted to pass a List
like so:
// We could update activityProvider to accept a list of strings instead.
// Then be tempted to create that list directly in the watch call.
ref.watch(activityProvider(['recreational', 'cooking']));
The problem with this code is that ['recreational', 'cooking'] == ['recreational', 'cooking']
is false
.
As such, Riverpod will consider that the two parameters are different,
and attempt to make a new network request.
This would result in an infinite loop of network requests, permanently
showing a progress indicator to the user.
To fix this, you could either use a const
list (const ['recreational', 'cooking']
)
or use a custom list implementation that overrides ==
.
To help spot this mistake, it is recommended to use the riverpod_lint and enable the provider_parameters lint rule. Then, the previous snippet would show a warning. See Getting started for installation steps.