Back to Riverpod

Implementing pull-to-refresh

website/docs/how_to/pull_to_refresh.mdx

2.0.0-dev.94.7 KB
Original Source

import { Link } from "/src/components/Link"; import { AutoSnippet, When } from "/src/components/CodeSnippet"; import activity from "./pull_to_refresh/activity"; import fetchActivity from "./pull_to_refresh/fetch_activity"; import displayActivity from "!!raw-loader!./pull_to_refresh/display_activity.dart"; import displayActivity2 from "!!raw-loader!./pull_to_refresh/display_activity2.dart"; import displayActivity3 from "!!raw-loader!./pull_to_refresh/display_activity3.dart"; import displayActivity4 from "!!raw-loader!./pull_to_refresh/display_activity4.dart"; import fullApp from "./pull_to_refresh/full_app";

Riverpod natively supports pull-to-refresh thanks to its declarative nature.

In general, pull-to-refreshes can be complex due as there are multiple problems to solve:

  • Upon first entering a page, we want to show a spinner. But during refresh, we want to show the refresh indicator instead. We shouldn't show both the refresh indicator and spinner.
  • While a refresh is pending, we want to show the previous data/error.
  • We need to show the refresh indicator for as long as the refresh is happening.

Let's see how to solve this using Riverpod.
For this, we will make a simple example which recommends a random activity to users.
And doing a pull-to-refresh will trigger a new suggestion:

Making a bare-bones application.

Before implement a pull-to-refresh, we first need something to refresh.
We can make a simple application which uses Bored API to suggests a random activity to users.

First, let's define an Activity class:

<AutoSnippet {...activity} />

That class will be responsible for representing a suggested activity in a type-safe manner, and handle JSON encoding/decoding.
Using Freezed/json_serializable is not required, but it is recommended.

Now, we'll want to define a provider making a HTTP GET request to fetch a single activity:

<AutoSnippet {...fetchActivity} />

We can now use this provider to display a random activity.
For now, we will not handle the loading/error state, and simply display the activity when available:

<AutoSnippet raw={displayActivity} />

Adding RefreshIndicator

Now that we have a simple application, we can add a RefreshIndicator to it.
That widget is an official Material widget responsible for displaying a refresh indicator when the user pulls down the screen.

Using RefreshIndicator requires a scrollable surface. But so far, we don't have any. We can fix that by using a ListView/GridView/SingleChildScrollView/etc:

<AutoSnippet raw={displayActivity2} />

Users can now pull down the screen. But our data isn't refreshed yet.

Adding the refresh logic

When users pull down the screen, RefreshIndicator will invoke the onRefresh callback. We can use that callback to refresh our data. In there, we can use ref.refresh to refresh the provider of our choice.

Note: onRefresh is expected to return a Future. And it is important for that future to complete when the refresh is done.

To obtain such a future, we can read our provider's .future property. This will return a future which completes when our provider has resolved.

We can therefore update our RefreshIndicator to look like this:

<AutoSnippet raw={displayActivity3} />

Showing a spinner only during initial load and handling errors.

At the moment, our UI does not handle the error/loading states.
Instead the data magically pops up when the loading/refresh is done.

Let's change this by gracefully handling those states. There are two cases:

  • During the initial load, we want to show a full-screen spinner.
  • During a refresh, we want to show the refresh indicator and the previous data/error.

Fortunately, when listening to an asynchronous provider in Riverpod, Riverpod gives us an AsyncValue, which offers everything we need.

That AsyncValue can then be combined with Dart 3.0's pattern matching as follows:

<AutoSnippet raw={displayActivity4} />

:::caution We use valueOrNull here, as currently, using value throws if in error/loading state.

Riverpod 3.0 will change this to have value behave like valueOrNull. But for now, let's stick to valueOrNull. :::

:::tip Notice the usage of the :final valueOrNull? syntax in our pattern matching. This syntax can be used only because activityProvider returns a non-nullable Activity.

If your data can be null, you can instead use AsyncValue(hasData: true, :final valueOrNull). This will correctly handle cases where the data is null, at the cost of a few extra characters. :::

Wrapping up: full application

Here is the combined source of everything we've covered so far:

<AutoSnippet {...fullApp} />