website/docs/how_to/cancel.mdx
import { Link } from "/src/components/Link"; import { AutoSnippet, When } from "/src/components/CodeSnippet"; import homeScreen from "!raw-loader!./cancel/home_screen.dart"; import extension from "!raw-loader!./cancel/extension.dart"; import detailScreen from "./cancel/detail_screen"; import detailScreenCancel from "./cancel/detail_screen_cancel"; import detailScreenDebounce from "./cancel/detail_screen_debounce"; import providerWithExtension from "./cancel/provider_with_extension";
As applications grow in complexity, it's common to have multiple network requests in flight at the same time. For example, a user might be typing in a search box and triggering a new request for each keystroke. If the user types quickly, the application might have many requests in flight at the same time.
Alternatively, a user might trigger a request, then navigate to a different page before the request completes. In this case, the application might have a request in flight that is no longer needed.
To optimize performance in those situations, there are a few techniques you can use:
In Riverpod, both of these techniques can be implemented in a similar way.
The key is to use ref.onDispose combined with "automatic disposal" or ref.watch
to achieve the desired behavior.
To showcase this, we will make a simple application with two pages:
We will then implement the following behaviors:
First, let's create the application, without any debouncing or cancelling.
We won't use anything fancy here, and stick to a plain FloatingActionButton with
a Navigator.push to open the detail page.
First, let's start with defining our home screen. As usual,
let's not forget to specify a ProviderScope at the root of our application.
Then, let's define our detail page. To fetch the activity and implement pull-to-refresh, refer to the <Link documentID="how_to/pull_to_refresh" /> case study.
<AutoSnippet title="lib/src/detail_screen.dart" {...detailScreen} />
Now that we have a working application, let's implement the cancellation logic.
To do so, we will use ref.onDispose to cancel the request when the user
navigates away from the page. For this to work, it is important that the
automatic disposal of providers is enabled.
The exact code needed to cancel the request will depend on the HTTP client.
In this example, we will use package:http, but the same principle applies
to other clients.
The key here is that ref.onDispose will be called when the user navigates away.
That is because our provider is no-longer used, and therefore disposed
thanks to automatic disposal.
We can therefore use this callback to cancel the request. When using package:http,
this can be done by closing our HTTP client.
<AutoSnippet {...detailScreenCancel} />
Now that we have implemented cancellation, let's implement debouncing.
At the moment, if the user refreshes the activity multiple times in a row,
we will send a request for each refresh.
Technically speaking, now that we have implemented cancellation, this is not a problem. If the user refreshes the activity multiple times in a row, the previous request will be cancelled, when a new request is made.
However, this is not ideal. We are still sending multiple requests, and
wasting bandwidth and server resources.
What we could instead do is delay our requests until the user stops refreshing
the activity for a fixed amount of time.
The logic here is very similar to the cancellation logic. We will again
use ref.onDispose. However, the idea here is that instead of
closing an HTTP client, we will rely on onDispose to abort the request
before it starts.
We will then arbitrarily wait for 500ms before sending the request.
Then, if the user refreshes the activity again before the 500ms have elapsed,
onDispose will be invoked, aborting the request.
:::info
To abort requests, a common practice is to voluntarily throw.
It is safe to throw inside providers after the provider has been disposed.
The exception will naturally be caught by Riverpod and be ignored.
:::
<AutoSnippet {...detailScreenDebounce} />
We now know how to debounce and cancel requests.
But currently, if we want to do another request, we need to copy-paste
the same logic in multiple places. This is not ideal.
However, we can go further and implement a reusable utility to do both at once.
The idea here is to implement an extension method on Ref that will
handle both cancellation and debouncing in a single method.
We can then use this extension method in our providers as followed:
<AutoSnippet {...providerWithExtension} />