Zum Hauptinhalt springen

Make your first provider/network request

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.

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 and running the code-generator), check out 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 Performing 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. 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 or json_serializable to handle JSON decoding. But of course, it's also possible to do it by hand.

Anyway, here's our model:

activity.dart

import 'package:freezed_annotation/freezed_annotation.dart';

part 'activity.freezed.dart';
part 'activity.g.dart';

/// The response of the `GET /api/activity` endpoint.
///
/// It is defined using `freezed` and `json_serializable`.

class Activity with _$Activity {
factory Activity({
required String key,
required String activity,
required String type,
required int participants,
required double price,
}) = _Activity;

/// Convert a JSON object into an [Activity] instance.
/// This enables type-safe reading of the API response.
factory Activity.fromJson(Map<String, dynamic> json) => _$ActivityFromJson(json);
}

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:

@riverpod
Result myFunction(MyFunctionRef ref) {
  <your logic here>
}
The annotation

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).

The annotated function

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.

Ref

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.

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:

provider.dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'activity.dart';

// Necessary for code-generation to work
part 'provider.g.dart';

/// This will create a provider named `activityProvider`
/// which will cache the result of this function.

Future<Activity> activity(ActivityRef ref) async {
// Using package:http, we fetch a random activity from the Bored API.
final response = await http.get(Uri.https('boredapi.com', '/api/activity'));
// Using dart:convert, we then decode the JSON payload into a Map data structure.
final json = jsonDecode(response.body) as Map<String, dynamic>;
// Finally, we convert the Map into an Activity instance.
return Activity.fromJson(json);
}

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:

consumer.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'activity.dart';
import 'provider.dart';

/// The homepage of our application
class Home extends StatelessWidget {
const Home({super.key});


Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, child) {
// Read the activityProvider. This will start the network request
// if it wasn't already started.
// By using ref.watch, this widget will rebuild whenever the
// the activityProvider updates. This can happen when:
// - The response goes from "loading" to "data/error"
// - The request was refreshed
// - The result was modified locally (such as when performing side-effects)
// ...
final AsyncValue<Activity> activity = ref.watch(activityProvider);

return Center(
/// Since network-requests are asynchronous and can fail, we need to
/// handle both error and loading states. We can use pattern matching for this.
/// We could alternatively use `if (activity.isLoading) { ... } else if (...)`
child: switch (activity) {
AsyncData(:final value) => Text('Activity: ${value.activity}'),
AsyncError() => const Text('Oops, something unexpected happened'),
_ => const CircularProgressIndicator(),
},
);
},
);
}
}

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 Getting started 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 couterpart, but with the added benefit of offering a "ref".

We can rewrite the previous examples to use ConsumerWidget as followed:


/// We subclassed "ConsumerWidget" instead of "StatelessWidget".
/// This is equivalent to making a "StatelessWidget" and retuning "Consumer".
class Home extends ConsumerWidget {
const Home({super.key});


// Notice how "build" now receives an extra parameter: "ref"
Widget build(BuildContext context, WidgetRef ref) {
// We can use "ref.watch" inside our widget like we did using "Consumer"
final AsyncValue<Activity> activity = ref.watch(activityProvider);

// The rendering logic stays the same
return Center(/* ... */);
}
}

As for ConsumerStatefulWidget, we would instead write:


// We extend ConsumerStatefulWidget.
// This is the equivalent of "Consumer" + "StatefulWidget".
class Home extends ConsumerStatefulWidget {
const Home({super.key});


ConsumerState<ConsumerStatefulWidget> createState() => _HomeState();
}

// Notice how instead of "State", we are extending "ConsumerState".
// This uses the same principle as "ConsumerWidget" vs "StatelessWidget".
class _HomeState extends ConsumerState<Home> {

void initState() {
super.initState();

// State life-cycles have access to "ref" too.
// This enables things such as adding a listener on a specific provider
// to show dialogs/snackbars.
ref.listenManual(activityProvider, (previous, next) {
// TODO show a snackbar/dialog
});
}


Widget build(BuildContext context) {
// "ref" is not passed as parameter anymore, but is instead a property of "ConsumerState".
// We can therefore keep using "ref.watch" inside "build".
final AsyncValue<Activity> activity = ref.watch(activityProvider);

return Center(/* ... */);
}
}

Flutter_hooks considerations: Combining HookWidget and ConsumerWidget

caution

If you have never heard about "hooks" before, feel free to skip this section.
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 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:


/// We subclassed "HookConsumerWidget".
/// This combines "StatelessWidget" + "Consumer" + "HookWidget" together.
class Home extends HookConsumerWidget {
const Home({super.key});


// Notice how "build" now receives an extra parameter: "ref"
Widget build(BuildContext context, WidgetRef ref) {
// It is possible to use hooks such as "useState" inside our widget
final counter = useState(0);

// We can also use read providers
final AsyncValue<Activity> activity = ref.watch(activityProvider);

return Center(/* ... */);
}
}