keepAlive
This is Part 4 of Mastering Riverpod: Core Concepts. autoDispose is all-or-nothing: a provider either lives forever or dies the moment it's unwatched. But real caching is more nuanced — "auto-dispose this, but keep it alive once the fetch succeeds" or "keep it for 5 minutes, then let it go." That fine-grained control comes from ref.keepAlive(), and it turns autoDispose from a switch into a dial.
The problem: autoDispose is too aggressive sometimes
autoDispose (Part 3) is great for cleanup, but pure auto-dispose can over-fetch. Imagine a detail screen with an autoDispose provider: navigate away and back, and it refetches every time — even if nothing changed. You wanted cleanup for unused data, but caching for data you just loaded.
What you really want is conditional: "dispose if I never got the data, but if the fetch succeeded, hang onto it." ref.keepAlive() expresses exactly that.
ref.keepAlive() and the KeepAliveLink
Inside an autoDispose provider, calling ref.keepAlive() immediately stops the provider from being auto-disposed. It returns a KeepAliveLink — a handle you can later use to re-enable disposal:
final link = ref.keepAlive(); // stop auto-disposal now
// ...later...
link.close(); // re-enable auto-disposal
So keepAlive() is "pin this in memory," and link.close() is "unpin it (resume normal auto-dispose)." This pair is the building block for every caching policy below.
keepAlive()only makes sense inside anautoDisposeprovider — there's nothing to prevent on a keep-alive-by-default provider. (riverpod_linthas a rule about this.)
Pattern 1: keep alive only after a successful fetch
The most common use. Fetch the data; if it succeeds, keep it cached; if it fails, let it be disposed so the next visit retries cleanly:
final userProvider = FutureProvider.autoDispose<User>((ref) async {
final user = await ref.watch(userRepositoryProvider).fetchUser();
ref.keepAlive(); // reached only if the await succeeded → cache the result
return user;
});
If fetchUser() throws, the ref.keepAlive() line is never reached, so the failed provider stays auto-dispose and is torn down when unwatched — a fresh attempt next time. If it succeeds, the result is pinned and survives navigation. This "cache successes, discard failures" behavior is exactly what you want for detail screens, and it's just two lines.
Pattern 2: cache for a duration (time-based)
Sometimes you want data cached for a window — say 5 minutes — then refreshed. Combine keepAlive() (pin now), a Timer (unpin later), and ref.onDispose (cancel the timer if the provider is destroyed first). This is clean enough to package as a reusable extension on Ref:
import 'dart:async';
extension CacheForExtension on Ref {
/// Keep the provider alive for [duration], then re-enable auto-disposal.
void cacheFor(Duration duration) {
final link = keepAlive(); // pin
final timer = Timer(duration, link.close); // unpin after `duration`
onDispose(timer.cancel); // tidy the timer if disposed early
}
}
// Usage: cache this fetch for 5 minutes.
final dashboardProvider = FutureProvider.autoDispose<Dashboard>((ref) async {
ref.cacheFor(const Duration(minutes: 5));
return ref.watch(dashboardRepoProvider).fetch();
});
Now dashboardProvider is cached for 5 minutes (no refetch on revisit within the window), then auto-disposes so a later visit gets fresh data. That's a real-world caching policy in a handful of lines — the kind of thing that's painful to hand-roll without Riverpod.
keepAlive vs plain keep-alive (no autoDispose)
It's worth distinguishing two ways to "keep state":
| Approach | Behavior | Use when |
| --- | --- | --- |
| No autoDispose (default) | cached forever (until ProviderScope disposed) | genuinely app-wide state (auth, theme, DI services) |
| autoDispose + ref.keepAlive() | auto-dispose unless/until you decide to pin (and you can unpin) | conditional caching: keep-after-success, cache-for-duration, keep-while-condition |
The difference is control. Plain keep-alive is unconditional. autoDispose + keepAlive() lets you keep state based on runtime conditions (success, time, a flag) and release it later. For anything smarter than "always cache" or "never cache," reach for keepAlive().
A few real policies you can now express
With keepAlive() + link.close() + onDispose, you can build:
- Cache on success, refetch on failure (Pattern 1) — detail screens.
- Time-boxed cache (Pattern 2) — dashboards, feeds that should refresh periodically.
- Keep while logged in —
keepAlive()after fetch, thenref.listen(authProvider, ...)tolink.close()on logout. - Manual pin/unpin — store the link and close it from a method when the user clears a cache.
The pattern is always the same: keepAlive() to pin, hold the KeepAliveLink, link.close() to unpin when your condition says so.
Practice Challenges
Challenge 1 — Keep on success. Make an autoDispose FutureProvider<Profile> that caches only if the fetch succeeds.
Show solution
final profileProvider = FutureProvider.autoDispose<Profile>((ref) async {
final p = await ref.watch(profileRepo).fetch();
ref.keepAlive(); // only reached on success
return p;
});
On failure, keepAlive() isn't reached, so the failed state is disposed (retry next time).
Challenge 2 — What's a KeepAliveLink for? Explain link.close().
Show solution
ref.keepAlive() returns a KeepAliveLink that pins the provider in memory. Calling link.close() re-enables auto-disposal — "unpinning" it so it can be destroyed when unwatched again. It's how you make caching temporary/conditional.
Challenge 3 — Cache for 2 minutes. Use the cacheFor pattern on a feed provider.
Show solution
final feedProvider = FutureProvider.autoDispose((ref) async {
ref.cacheFor(const Duration(minutes: 2));
return ref.watch(feedRepo).fetch();
});
(With the cacheFor extension that pins, sets a Timer to link.close, and cancels it onDispose.)
Challenge 4 — Right tool. Auth state should live the whole session. Plain keep-alive or autoDispose + keepAlive?
Show solution
Plain keep-alive (no autoDispose) — it's unconditionally app-wide state for the whole session, so there's no condition to manage. keepAlive() is for conditional caching of otherwise-auto-dispose providers.
Challenge 5 — Why does keepAlive belong inside autoDispose? Explain.
Show solution
keepAlive() prevents auto-disposal — but a provider without autoDispose already never auto-disposes, so there's nothing to prevent. It only has meaning inside an autoDispose provider, where it selectively overrides the default tear-down (and riverpod_lint flags misuse).
Questions to test yourself
Q1 (basic). What does ref.keepAlive() do, and what does it return?
Show answer
Inside an autoDispose provider, it immediately prevents the provider from being auto-disposed, and returns a KeepAliveLink you can later use to re-enable disposal.
Q2 (basic). What does link.close() do?
Show answer
Re-enables automatic disposal — "unpins" the provider so it can be destroyed again when no longer watched. It reverses a keepAlive().
Q3 (intermediate). How do you cache only successful fetches?
Show answer
Put ref.keepAlive() after the await in an autoDispose provider. If the fetch throws, that line is never reached, so the failed state stays auto-dispose (refetches next time); on success it's pinned and cached.
Q4 (intermediate). Describe the cache-for-duration pattern.
Show answer
Call keepAlive() to pin the state, start a Timer(duration, link.close) to unpin after the window, and register ref.onDispose(timer.cancel) to clean up the timer if the provider is destroyed first. Packaged as a Ref.cacheFor(duration) extension, it caches a provider for a set time, then auto-disposes.
Q5 (intermediate). When is plain keep-alive (no autoDispose) the right choice instead of keepAlive()?
Show answer
When the state is unconditionally app-wide for the whole session (auth, theme, DI services) — there's no runtime condition to manage. autoDispose + keepAlive() is for conditional caching (keep-on-success, cache-for-duration, keep-while-condition).
Q6 (advanced). Why does keepAlive() only make sense inside an autoDispose provider?
Show answer
keepAlive() exists to override auto-disposal. A provider that isn't autoDispose is keep-alive by default — it never auto-disposes — so there's nothing for keepAlive() to prevent. Using it on a non-autoDispose provider is meaningless (and riverpod_lint warns), because the whole point is selectively keeping an otherwise-disposable provider alive.
Wrapping up
keepAlive turns disposal into a dial:
- Inside an
autoDisposeprovider,ref.keepAlive()pins the state and returns aKeepAliveLink;link.close()unpins it. - Keep-after-success: put
ref.keepAlive()after theawait— caches successes, discards failures. - Cache-for-duration:
keepAlive()+Timer(duration, link.close)+onDispose(timer.cancel)(thecacheForextension). - Use plain keep-alive for unconditional app-wide state; use
autoDispose+keepAlive()for conditional caching policies.
You've now got fine control over when providers live and die. The flip side is forcing a provider to recompute on demand — for a retry button or pull-to-refresh. Part 5 covers ref.invalidate vs ref.refresh — forcing provider recomputation.