skip to content

In Riverpod 3, how would you cache an autoDispose product-detail provider for five minutes while never caching a failed request?

level: middleimportance: should knowfreq 36%

answer

  1. keepAlive returns a link
  2. call it after the await
  3. Timer closes the link
  4. links reset on recompute
  5. onCancel starts, onResume stops

basics

~20 s

Keep 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 s

Riverpod 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 lines
dart
import '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

for a junior

Know that ref.keepAlive stops an autoDispose provider from being freed and that the returned link can be closed later.

for a middle

Explain placing keepAlive after the await to skip failures, the Timer plus onDispose pattern, and what onCancel and onResume signal.

for a senior

Choose between fetch-time and last-listener expiry, handle the disposed-during-request case, and keep memory bounded for large families.

for a principal

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