In Riverpod 3, how would you cache an autoDispose product-detail provider for five minutes while never caching a failed request?
answer
- keepAlive returns a link
- call it after the await
- Timer closes the link
- links reset on recompute
- onCancel starts, onResume stops
basics
~20 sKeep the provider autoDispose, call ref.keepAlive() only after the fetch succeeds, and close the returned KeepAliveLink from a five-minute Timer cancelled in onDispose. A failed fetch throws before keepAlive, so its error is disposed normally.
solid answer
~40 sRiverpod has no built-in timed cache; you compose one. `ref.keepAlive()` returns a `KeepAliveLink`; while any link is open, an autoDispose provider is not disposed even with no listeners, and `link.close()` restores automatic disposal once all links are closed. Calling it after the `await` means a request that throws never reaches it, so failures are not cached. A `Timer(const Duration(minutes: 5), link.close)` expires the cache, and `ref.onDispose(timer.cancel)` cleans up if the provider rebuilds first - links are reset on recompute anyway. The docs wrap this as a `cacheFor` extension on `Ref`. If the five minutes should start when the last listener leaves rather than when the data arrived, start the timer in `ref.onCancel` and cancel it in `ref.onResume`.
code
dart · 19 linesimport 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
extension CacheForExtension on Ref {
void cacheFor(Duration duration) {
final link = keepAlive();
final timer = Timer(duration, link.close);
onDispose(timer.cancel);
}
}
final productDetailProvider =
FutureProvider.autoDispose.family<Product, String>((ref, productId) async {
final api = ref.watch(catalogApiProvider);
final product = await api.fetchProduct(productId);
if (ref.mounted) ref.cacheFor(const Duration(minutes: 5));
return product;
});go deeper
Know that ref.keepAlive stops an autoDispose provider from being freed and that the returned link can be closed later.
Explain placing keepAlive after the await to skip failures, the Timer plus onDispose pattern, and what onCancel and onResume signal.
Choose between fetch-time and last-listener expiry, handle the disposed-during-request case, and keep memory bounded for large families.
Standardise one cache helper for the codebase so expiry rules are consistent and reviewable instead of ad hoc timers in each provider.
## What keepAlive does In an **autoDispose** provider, `ref.keepAlive()` suspends automatic disposal. It returns a **`KeepAliveLink`**: - While at least one link is open, the provider is not disposed even if nothing listens to it. - `link.close()` removes that link. When the last open link closes and the provider has no listeners, disposal is scheduled as usual. - If `keepAlive` is called several times, **all** links must be closed. - When the provider **recomputes** - a watched dependency changes, or it is invalidated - its links are dropped and automatic disposal is re-enabled for the new state. On a provider without `autoDispose`, there is nothing to suspend. ## Not caching failures The docs' own example is exactly this requirement: keep successful responses, drop failed ones. ```dart final productDetailProvider = FutureProvider.autoDispose.family<Product, String>((ref, productId) async { final api = ref.watch(catalogApiProvider); final product = await api.fetchProduct(productId); ref.keepAlive(); return product; }); ``` If `fetchProduct` throws, execution never reaches `keepAlive()`. The provider holds an error, and as soon as the product screen is closed, that error state is disposed; the next visit retries from scratch. A successful response pins the state. ## Adding an expiry Pinned forever is rarely what you want for product data. The docs build a reusable extension: ```dart extension CacheForExtension on Ref { void cacheFor(Duration duration) { final link = keepAlive(); final timer = Timer(duration, link.close); onDispose(timer.cancel); } } ``` Used after the await as `ref.cacheFor(const Duration(minutes: 5))`. Three details matter: 1. `Timer` comes from `dart:async`. 2. `onDispose(timer.cancel)` stops a stale timer if the provider rebuilds or is disposed before it fires. 3. If the provider was disposed during the request - the user left and the disposal ran - the `Ref` is no longer mounted and `keepAlive` would throw, so guard with `if (ref.mounted)` before caching. ## Counting from the last listener instead `cacheFor` counts five minutes from when the data arrived. To count from when the screen was closed, use the listener life-cycles: | Callback | Fires when | |---|---| | `ref.onCancel` | the provider loses its last active listener | | `ref.onResume` | it gains an active listener again after `onCancel` | | `ref.onDispose` | the state is destroyed or about to be rebuilt | The pattern: open a link after success, start a timer that closes it in `onCancel`, cancel that timer in `onResume`, and cancel it in `onDispose`. A user bouncing between the list and the product within five minutes never triggers a refetch. One subtlety: if the last listener left during the request, `onCancel` has already fired before you registered it, so check `ref.isPaused` after the await and start the timer at once if it is true. ## Choosing a strategy - **No cache**: plain `autoDispose`. - **Cache successes until the session ends**: `keepAlive()` after the await. - **Cache for N minutes after fetching**: `cacheFor`. - **Cache for N minutes after the screen closes**: `onCancel`/`onResume` with a timer. - **App-wide, never disposed**: drop `autoDispose` entirely. ## Common mistakes - Calling `keepAlive()` at the top of the body, before the request, which pins error states as well as successes. - Forgetting `onDispose(timer.cancel)`, so a timer from a previous build fires later and closes a link that belongs to nobody. - Using `keepAlive` on a provider without `autoDispose` and expecting any effect. - Opening several links in different places and closing only one, then wondering why the product never leaves memory.
- What happens to open KeepAliveLinks when the provider recomputes because a watched dependency changed?They are discarded with the old state and automatic disposal is re-enabled. The new build decides again whether to call `keepAlive`. That is why `cacheFor` registers `onDispose(timer.cancel)`: the timer for the old link would otherwise fire later and close a link that no longer matters.
- Why is keepAlive preferable to simply removing autoDispose from a product-detail family?Without autoDispose, every product id ever opened stays in memory for the whole session, including error states. `keepAlive` keeps autoDispose as the default and pins only what you choose - successful responses - for only as long as you choose, so memory stays bounded.
saying these in an interview costs you the question
- Calling keepAlive before the await is fine for not caching errors
- One link.close() always re-enables disposal even if keepAlive was called twice
- A keepAlive link survives when the provider recomputes
- Riverpod ships a built-in cache duration parameter on autoDispose
- onResume fires every time the provider rebuilds