Skip to main content

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();
}
}
To pass parameters to our providers, we can simply add our parameters on the annotated function itself. For example, we could update our provider to accept a `String` argument corresponding to the type of activity desired:

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();
}
}
caution

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.

info

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.

caution

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.