← Back to blog
Mastering Riverpod: Core Concepts · Part 3 of 8
August 18, 20267 min read

The autoDispose Modifier — Memory Management in Riverpod

RiverpodFlutterDart

The autoDispose Modifier

This is Part 3 of Mastering Riverpod: Core Concepts. By default, a provider's state lives forever — once created, it stays cached in the ProviderContainer (Foundation Part 3) for the app's lifetime. That's great for caching, but a problem for state tied to a specific screen or argument. The autoDispose modifier flips this: it destroys a provider's state when nothing is watching it anymore. Understanding it is the key to memory management in Riverpod.


The default: providers live forever

A normal provider is created lazily on first watch and then never disposed (until the whole ProviderScope goes away):

final userProvider = FutureProvider<User>((ref) => fetchUser());

Open a screen that watches userProvider, navigate away, come back — the cached User is still there (great, no refetch). But this also means: if you have a per-screen provider, or a family with many arguments, those instances pile up in memory even after their screens are gone. That's a leak.


autoDispose: destroy when unwatched

Marking a provider autoDispose tells Riverpod: "when the last listener stops watching this, destroy its state."

// 3.0 manual syntax — the isAutoDispose named parameter:
final userProvider = FutureProvider<User>(
  isAutoDispose: true,
  (ref) => fetchUser(),
);

// Equivalent modifier form (also used with .family):
final userProvider = FutureProvider.autoDispose<User>((ref) => fetchUser());

Now when the screen watching userProvider is disposed and no one else watches it, Riverpod tears down the state. Re-open the screen later and it's recreated fresh (a new fetch). The trade-off in one line:

autoDispose trades caching for cleanup. Keep-forever (default) = cached, fast re-entry, more memory. autoDispose = fresh each time, less memory, possible refetch. Choose per provider based on what the data needs.

Code-gen note: with @riverpod code generation (a later series), providers are autoDispose by default — you opt out with @Riverpod(keepAlive: true). Manual providers are keep-alive by default and you opt in. Don't let that asymmetry surprise you.


The disposal lifecycle

autoDispose doesn't fire the instant the last widget unmounts — it's careful to avoid disposing during a quick rebuild. The precise sequence (per the docs):

last listener removed
   → ref.onCancel fires
   → Riverpod waits one frame
   → if STILL unwatched → ref.onDispose fires → state destroyed
   (if a new listener appears within that frame → ref.onResume, state kept)

That one-frame grace period is important: when you navigate between screens (or a widget rebuilds), the provider may briefly have zero listeners and then a new one — Riverpod won't dispose in that gap. So autoDispose is safe for normal navigation; it only tears down when the provider is genuinely unused. We cover onCancel/onResume/onDispose fully in Part 7.


Why families need autoDispose

This is the most important use case. Recall from Part 2 that a family caches one instance per argument. Without autoDispose, every argument you ever pass leaves a cached instance behind forever:

// ❌ Each distinct id leaks a cached User provider forever.
final userProvider = FutureProvider.family<User, String>((ref, id) => fetchUser(id));

// ✅ Instances are destroyed when their screen stops watching them.
final userProvider =
    FutureProvider.autoDispose.family<User, String>((ref, id) => fetchUser(id));

Scroll through 500 user profiles with the first version and you cache 500 User providers. With autoDispose, each is cleaned up when you scroll past it. Families should almost always be autoDispose — it's the single most common place this modifier matters.


Typical candidates (and non-candidates)

| Good fit for autoDispose | Better as keep-alive (default) | | --- | --- | | per-screen detail data (user by id, product by id) | app-wide singletons (auth state, theme, config) | | search results / query-parameterized state | reference data fetched once and reused everywhere | | any family with many arguments | expensive data you want cached across navigation | | state that should reset when you leave a screen | DI services/repositories (Provider for a Dio) |

A useful instinct: "Is this data tied to a screen/argument that comes and goes? → autoDispose. Is it app-wide and worth caching? → keep-alive."


autoDispose is the foundation for cleanup and caching control

Two important features build on top of autoDispose:

  • ref.onDispose (Part 7) — run cleanup (cancel timers, close controllers) when the state is destroyed. Especially relevant for autoDispose providers, which actually get destroyed during normal use.
  • ref.keepAlive (Part 4) — selectively keep an autoDispose provider alive (e.g. only after a successful fetch, or for a cache duration). This gives you fine-grained "auto-dispose unless…" control.

So autoDispose isn't just on/off — it's the base that lets you express nuanced caching policies. We'll build those next.


Practice Challenges

Challenge 1 — Make one autoDispose. Convert final p = FutureProvider<User>((ref) => fetch()); to autoDispose (modifier form).

Show solution
final p = FutureProvider.autoDispose<User>((ref) => fetch());
// or: FutureProvider<User>(isAutoDispose: true, (ref) => fetch());

Challenge 2 — Family memory. Why does a non-autoDispose family<User, String> leak, and what fixes it?

Show solution

Each distinct id caches a separate User provider that persists forever, so many ids accumulate unbounded instances. Add autoDispose so each instance is destroyed when no longer watched: FutureProvider.autoDispose.family<User, String>(...).

Challenge 3 — Choose the policy. autoDispose or keep-alive for: (a) auth state, (b) a product-detail-by-id provider, (c) app theme, (d) search results?

Show solution

(a) keep-alive (app-wide). (b) autoDispose (per-screen/argument). (c) keep-alive (app-wide). (d) autoDispose (transient, per-query).

Challenge 4 — Disposal timing. Does an autoDispose provider get destroyed the instant its widget unmounts? Explain.

Show solution

No — Riverpod fires onCancel when the last listener leaves, waits one frame, and only disposes if still unwatched (else onResume keeps it). This grace period prevents disposal during quick rebuilds/navigation; it tears down only when genuinely unused.

Challenge 5 — Code-gen default. With @riverpod code generation, is a provider autoDispose or keep-alive by default, and how do you change it?

Show solution

autoDispose by default. Opt out with @Riverpod(keepAlive: true). (Manual providers are the opposite: keep-alive by default, opt in with autoDispose/isAutoDispose: true.)


Questions to test yourself

Q1 (basic). What does autoDispose do?

Show answer

It destroys a provider's state when the last listener stops watching it (no more ref.watch/ref.listen), instead of caching it for the app's lifetime. Re-watching later recreates it fresh.

Q2 (basic). What's the default disposal behavior of a manual provider (no modifier)?

Show answer

Keep-alive — created lazily on first watch and never disposed until the whole ProviderScope is gone. The state is cached for the app's lifetime.

Q3 (intermediate). What's the trade-off autoDispose makes?

Show answer

It trades caching for cleanup: keep-alive caches the result (fast re-entry, more memory), while autoDispose frees memory when unwatched (less memory, but recreates/refetches on next use). Choose per provider by whether the data is worth caching.

Q4 (intermediate). Walk through the autoDispose lifecycle when the last listener leaves.

Show answer

ref.onCancel fires; Riverpod waits one frame; if the provider is still unwatched, ref.onDispose fires and the state is destroyed. If a new listener appears within that frame, ref.onResume fires and the state is kept — so quick rebuilds/navigation don't cause disposal.

Q5 (intermediate). Why do family providers almost always need autoDispose?

Show answer

A family caches one instance per argument; without autoDispose, every argument ever passed leaves a cached instance forever — unbounded memory growth over many arguments (e.g. many ids). autoDispose destroys each instance when no longer watched, bounding memory.

Q6 (advanced). How does autoDispose enable nuanced caching policies rather than just on/off?

Show answer

It's the base layer that ref.keepAlive (Part 4) and ref.onDispose (Part 7) build on. Within an autoDispose provider you can call keepAlive() to selectively prevent disposal (e.g. only after a successful fetch, or for a timed cache window) and onDispose to clean up resources when it is destroyed — letting you express "auto-dispose unless X" policies instead of a binary choice.


Wrapping up

autoDispose is Riverpod's memory-management lever:

  • By default providers are keep-alive (cached forever); autoDispose destroys state when no listeners remain.
  • The lifecycle: last listener → onCancel → wait one frame → onDispose (destroy) — with a grace period so navigation/rebuilds are safe.
  • It trades caching for cleanup — pick per provider (per-screen/argument → autoDispose; app-wide → keep-alive).
  • Families almost always need it to avoid accumulating per-argument instances.
  • Code-gen providers are autoDispose by default (opt out with keepAlive: true); manual ones are the reverse.

autoDispose is all-or-nothing per provider — but sometimes you want "auto-dispose, but keep this one alive once I've fetched it." That selective control is keepAlive. Part 4 covers keepAlive — when you don't want your provider to die.