Files
riverpod/website/docs/essentials/first_request.mdx
Remi Rousselet a360fca98d Another
2025-05-06 14:54:07 +02:00

347 lines
12 KiB
Plaintext

---
title: Make your first provider/network request
pagination_prev: introduction/getting_started
version: 3
---
import { Link } from "/src/components/Link";
import { AutoSnippet } from "/src/components/CodeSnippet";
import activity from "./first_request/activity";
import provider from "./first_request/provider";
import consumer from "./first_request/consumer";
import consumerWidget from "./first_request/consumer_widget";
import consumerStatefulWidget from "./first_request/consumer_stateful_widget";
import hookConsumerWidget from "./first_request/hook_consumer_widget";
import Legend from "./first_request/legend/legend";
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
Network requests are the core of any application. But there are a lot of things to consider when
making a network request:
- The UI should render a loading state while the request is being made
- Errors should be gracefully handled
- The request should be cached if possible
In this section, we will see how Riverpod can help us deal with all of this naturally.
## Setting up `ProviderScope`
Before we start making network requests, make sure that `ProviderScope` is added at the
root of the application.
```dart
void main() {
runApp(
// To install Riverpod, we need to add this widget above everything else.
// This should not be inside "MyApp" but as direct parameter to "runApp".
ProviderScope(
child: MyApp(),
),
);
}
```
Doing so will enable Riverpod for the entire application.
:::note
For complete installation steps (such as installing [riverpod_lint](https://pub.dev/packages/riverpod_lint)
and running the code-generator), check out <Link documentID="introduction/getting_started" />.
:::
## Performing your network request in a "provider"
Performing a network request is usually what we call "business logic".
In Riverpod, business logic is placed inside "providers".
A provider is a super-powered function.
They behave like normal functions, with the added benefits of:
- Being cached
- Offering default error/loading handling
- Being listenable
- Automatically re-executing when some data changes
This make providers a perfect fit for _GET_ network requests (as for _POST/etc_ requests, see <Link documentID="essentials/side_effects" />).
As an example, let's make a simple application which suggests a random activity to do when bored.
To do so, we will use the [Bored API](https://boredapi.com/). In particular,
we will perform a _GET_ request on the `/api/activity` endpoint. This returns a JSON object,
which we will parse into a Dart class instance.
The next step would then be to display this activity in the UI. We would also make sure
to render a loading state while the request is being made, and to gracefully handle errors.
Sounds great? Let's do it!
### Defining the model
Before we start, we need to define the model of the data we will receive from the API.
This model will also need a way to parse the JSON object into a Dart class instance.
Generally, it is recommended to use a code-generator such as [Freezed](https://pub.dev/packages/freezed)
or [json_serializable](https://pub.dev/packages/json_serializable) to handle
JSON decoding. But of course, it's also possible to do it by hand.
Anyway, here's our model:
<AutoSnippet title="activity.dart" {...activity} />
### Creating the provider
Now that we have our model, we can start querying the API.
To do so, we will need to create our first provider.
The syntax for defining a provider is as followed:
<Tabs>
<TabItem value="riverpod" label="riverpod">
<Legend
code={`
final name = SomeProvider.someModifier<Result>((ref) {
<your logic here>
});
`}
annotations={[
{
offset: 6,
length: 4,
label: "The provider variable",
description: <>
This variable is what will be used to interact with our provider.
The variable must be final and "top-level" (global).
</>
},
{
offset: 13,
length: 12,
label: "The provider type",
description: <>
Generally either `Provider`, `FutureProvider` or `StreamProvider`.
The type of provider used depends on the return value of your function.
For example, to create a `Future<Activity>`, you'll want a `FutureProvider<Activity>`.
`FutureProvider` is the one you'll want to use the most.
:::tip
Don't think in terms of "Which provider should I pick".
Instead, think in terms of "What do I want to return". The provider type
will follow naturally.
:::
</>
},
{
offset: 25,
length: 13,
label: "Modifiers (optional)",
description: <>
Often, after the type of the provider you may see a "modifier".
Modifiers are optional, and are used to tweak the behavior of the provider
in a way that is type-safe.
There are currently two modifiers available:
- `autoDispose`, which will automatically clear the cache when the provider
stops being used.
See also <Link documentID="essentials/auto_dispose" />
- `family`, which enables passing arguments to your provider.
See also <Link documentID="essentials/passing_args" />.
</>
},
{
offset: 48,
length: 3,
label: "Ref",
description: <>
An object used to interact with other providers.
All providers have one; either as parameter of the provider function,
or as a property of a Notifier.
</>
},
{
offset: 57,
length: 17,
label: "The provider function",
description: <>
This is where we place the logic of our providers.
This function will be called when the provider is first read.
Subsequent reads will not call the function again, but instead return the cached value.
</>
},
]}
/>
</TabItem>
<TabItem value="riverpod_generator" label="riverpod_generator">
<Legend
code={`
@riverpod
Result myFunction(Ref ref) {
<your logic here>
}
`}
annotations={[
{
offset: 0,
length: 9,
label: "The annotation",
description: <>
All providers must be annotated with `@riverpod` or `@Riverpod()`.
This annotation can be placed on global functions or classes.
Through this annotation, it is possible to configure the provider.
For example, we can disable "auto-dispose" (which we will see later) by writing `@Riverpod(keepAlive: true)`.
</>
},
{
offset: 17,
length: 10,
label: "The annotated function",
description: <>
The name of the annotated function determines how the provider
will be interacted with.
For a given function `myFunction`, a generated `myFunctionProvider` variable will be generated.
Annotated functions **must** specify a "ref" as first parameter.
Besides that, the function can have any number of parameters, including generics.
The function is also free to return a `Future`/`Stream` if it wishes to.
This function will be called when the provider is first read.
Subsequent reads will not call the function again, but instead return the cached value.
</>
},
{
offset: 28,
length: 7,
label: "Ref",
description: <>
An object used to interact with other providers.
All providers have one; either as parameter of the provider function,
or as a property of a Notifier.
The type of this object is determined by the name of the function/class.
</>
},
]}
/>
</TabItem>
</Tabs>
In our case, we want to _GET_ an activity from the API.
Since a _GET_ is an asynchronous operation, that means we will want
to create a `Future<Activity>`.
Using the syntax defined previously, we can therefore define our provider as followed:
<AutoSnippet title="provider.dart" {...provider} />
In this snippet, we've defined a provider named `activityProvider` which
our UI will be able to use to obtain a random activity. It is worth noting
that:
- The network request will not be executed until the UI reads the provider
at least once.
- Subsequent reads will not re-execute the network request,
but instead return the previously fetched activity.
- If the UI stops using this provider, the cache will be destroyed.
Then, if the UI ever uses the provider again, that a new network request will be made.
- We did not catch errors. This is voluntary, as providers
natively handle errors.
If the network request or if the JSON parsing throws, the error
will be caught by Riverpod. Then, the UI will automatically have the necessary
information to render an error page.
:::info
Providers are "lazy". Defining a provider will not execute the network request.
Instead, the network request will be executed when the provider is first read.
:::
### Rendering the network request's response in the UI
Now that we have defined a provider, we can start using it inside our UI
to display the activity.
To interact with a provider, we need an object called "ref". You may have seen
it previously in the provider definition, as providers naturally have access
to a "ref" object.
But in our case, we aren't in a provider, but a widget. So how do we get a "ref"?
The solution is to use a custom widget called `Consumer`. A `Consumer` is a widget
similar to `Builder`, but with the added benefit of offering us a "ref". This enables our UI to read providers.
The following example showcases how to use a `Consumer`:
<AutoSnippet title="consumer.dart" {...consumer} />
In that snippet, we've used a `Consumer` to read our `activityProvider` and display the activity.
We also gracefully handled the loading/error states.
Notice how the UI was able to handle loading/error states without having to do anything special
in the provider.
At the same time, if the widget were to rebuild, the network request would correctly
not be re-executed. Other widgets could also access the same provider without
re-executing the network request.
:::info
Widgets can listen to as many providers as they want. To do so, simply add more `ref.watch` calls.
:::
:::tip
Make sure to install the linter. That will enable your IDE to offer refactoring
options to automatically add a `Consumer` or convert a `StatelessWidget` into a `ConsumerWidget`.
See <Link documentID="introduction/getting_started" hash="enabling-riverpod_lintcustom_lint" /> for installation steps.
:::
## Going further: Removing code indentation by using `ConsumerWidget` instead of `Consumer`.
In the previous example, we used a `Consumer` to read our provider.
Although there is nothing wrong with this approach, the added indentation
can make the code harder to read.
Riverpod offers an alternative way of achieving the same result:
Instead of writing a `StatelessWidget`/`StatefulWidget` returns a `Consumer`, we can
define a `ConsumerWidget`/`ConsumerStatefulWidget`.
`ConsumerWidget` and `ConsumerStatefulWidget` are effectively the fusion
of a `StatelessWidget`/`StatefulWidget` and a `Consumer`. They behave the same
as their original counterpart, but with the added benefit of offering a "ref".
We can rewrite the previous examples to use `ConsumerWidget` as followed:
<AutoSnippet {...consumerWidget} />
As for `ConsumerStatefulWidget`, we would instead write:
<AutoSnippet {...consumerStatefulWidget} />
### Flutter_hooks considerations: Combining `HookWidget` and `ConsumerWidget`
:::caution
If you have never heard about "hooks" before, feel free to skip this section.
[Flutter_hooks](https://pub.dev/packages/flutter_hooks) is a package
independent from Riverpod but often used alongside it. If you are new to Riverpod,
using "hooks" is discouraged. See more in <Link documentID="concepts/about_hooks"/>.
:::
If you are using `flutter_hooks`, you may be wondering how to combine `HookWidget`
and `ConsumerWidget`. After all, both involve changing the extended widget class.
Riverpod offers a solution to this problem: `HookConsumerWidget` and `StatefulHookConsumerWidget`.
Similarly to how `ConsumerWidget` and `ConsumerStatefulWidget` are the fusion of `Consumer` and `StatelessWidget`/`StatefulWidget`,
`HookConsumerWidget` and `StatefulHookConsumerWidget` are the fusion of `Consumer` and `HookWidget`/`HookStatefulWidget`.
As such, they enable using both hooks and providers in the same widget.
To showcase this, we could one more time rewrite the previous example:
<AutoSnippet {...hookConsumerWidget} />