skip to content

Auto-Dispose & Families

Riverpod decides how long provider state lives: autoDispose frees it when unlistened, keepAlive pins it, and a family keys one state per argument. Interviewers probe leaks and cache timing.

part ofFlutter Riverpodoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Riverpod 3, what does autoDispose do, and why does a product-detail screen refetch after you leave and reopen it?

level: juniorimportance: must knowfreq 55%

answer

  1. state tied to having listeners
  2. last listener leaves, onCancel
  3. disposal scheduled, not instant
  4. default lifetime is the container
  5. isAutoDispose: true or .autoDispose

basics

~20 s

autoDispose destroys a provider's state once nothing listens to it any more. Leaving the product screen removes its last listener, so the state and fetched product are dropped; reopening reads the provider again, which rebuilds it and refetches.

solid answer

~40 s

By default a Riverpod provider keeps its state for as long as its `ProviderContainer` - in Flutter, the `ProviderScope` - lives. `autoDispose`, enabled with `FutureProvider.autoDispose(...)` or `isAutoDispose: true`, ties the state to having listeners instead. When the last `ref.watch` or `ref.listen` goes away, `onCancel` runs and a disposal is scheduled; if no listener returns before it runs, the state is destroyed and `onDispose` runs. The next read builds it from scratch. So a product-detail screen that watches `productDetailProvider('p-42')` refetches when reopened: popping the route removed the only listener. That is usually the point - freeing memory and cancelling work - and if you want a short cache, `ref.keepAlive` adjusts it. Families should almost always be autoDispose, or every argument ever used stays in memory.

code

dart · 26 lines
dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final productDetailProvider =
    FutureProvider.autoDispose.family<Product, String>((ref, productId) async {
  final api = ref.watch(catalogApiProvider);
  ref.onCancel(() => debugPrint('no listeners for $productId'));
  ref.onDispose(() => debugPrint('disposed $productId'));
  return api.fetchProduct(productId);
});

class ProductDetailPage extends ConsumerWidget {
  const ProductDetailPage({super.key, required this.productId});

  final String productId;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final product = ref.watch(productDetailProvider(productId));
    return switch (product) {
      AsyncData(:final value) => Text(value.name),
      AsyncError() => const Text('Could not load product'),
      _ => const CircularProgressIndicator(),
    };
  }
}

go deeper

for a junior

Remember the rule: default providers live as long as ProviderScope, autoDispose ones only while something listens. Know how to add .autoDispose.

for a middle

Walk through onCancel, the scheduled disposal and onDispose, and explain why a quick hand-off between two listeners does not lose the state.

for a senior

Decide per provider whether it should be app-wide or screen-scoped, and catch families without autoDispose as memory leaks in review.

for a principal

Set a default policy - autoDispose for screen data and families, explicit exceptions for app-wide services - so cache lifetimes are a deliberate design choice.

## Default lifetime versus autoDispose A Riverpod provider's **state** lives in a `ProviderContainer`. In a Flutter app that container belongs to the `ProviderScope` at the root, so without any modifier a provider, once read, keeps its state for the life of the app. It is reset only if it is invalidated or if a watched dependency changes. **`autoDispose`** changes that rule: the state lives only while something is listening to it. | | Default provider | autoDispose provider | |---|---|---| | State created | on first read | on first read | | State kept while unlistened | yes, until container disposal | no | | Rebuilt when a watched dependency changes | yes | yes | | Typical use | app-wide settings, clients, session | screen data, search results, anything keyed by an argument | The docs stress one point: enabling or disabling automatic disposal has no effect on recomputation. A recomputed provider always destroys its previous state. ## How to opt in Without code generation: - the modifier: `FutureProvider.autoDispose<Product>(...)`, chainable with a family as `FutureProvider.autoDispose.family<Product, String>(...)`; - or the constructor flag: `Provider<String>(isAutoDispose: true, (ref) => ...)`. With `riverpod_generator`, generated providers are auto-dispose by default and the annotation opts out; that is the codegen topic. ## When exactly state is destroyed Riverpod tracks listeners created by `ref.watch` and `ref.listen`: 1. The last listener is removed - the product screen is popped and its `ConsumerWidget` unmounts. 2. The provider becomes inactive and its **`ref.onCancel`** callbacks run. 3. A disposal is **scheduled**, not performed on the spot. The docs describe it as waiting one frame. 4. When the scheduled task runs, Riverpod checks again. If a listener came back, or a `ref.keepAlive` link is open, nothing happens. 5. Otherwise the state is destroyed and **`ref.onDispose`** callbacks run - cancel the HTTP request, close the stream controller. Step 4 is why navigating from one screen to another that watches the same provider does not cause a refetch: the new listener arrives before the disposal runs. ## Why the product screen refetches ```dart final productDetailProvider = FutureProvider.autoDispose.family<Product, String>((ref, productId) async { final api = ref.watch(catalogApiProvider); return api.fetchProduct(productId); }); ``` The detail screen is the only listener of `productDetailProvider('p-42')`. Popping it removes that listener, the state is disposed, and reopening the screen creates a fresh state, which runs the fetch again. Without `autoDispose`, the product would stay cached for the rest of the session - and so would every other product the user ever opened. ## Choosing - **Use autoDispose** for screen-scoped data, anything parameterised by a family argument, and providers holding resources such as stream subscriptions or timers. - **Skip it** for app-wide objects - an API client, the current user - that should survive navigation. - **Want a short cache in between?** Keep `autoDispose` and open a `ref.keepAlive()` link, closing it after a delay. ## Riverpod 3 cleanup In 2.x every class had an auto-dispose twin (`AutoDisposeNotifier`, `AutoDisposeRef`, ...). Riverpod 3 removed those interfaces: an auto-dispose notifier extends plain `Notifier`, and the modifier lives only on the provider.

  • If screen A pushes screen B and both watch the same autoDispose provider, is the state disposed during the transition?
    No. The state is disposed only when it has no listeners at all. While both routes are mounted, both widgets listen. Even if A were removed before B mounted, disposal is scheduled rather than immediate, and Riverpod checks again before disposing, so a listener that arrives in time keeps the state.
  • Why do the Riverpod docs recommend autoDispose for every family?
    A family keeps one state per argument. Without autoDispose, every product id, search query or page number ever requested stays in memory until the `ProviderScope` is disposed - effectively a leak that grows with use. With autoDispose, each argument's state disappears once no widget or provider listens to it.

saying these in an interview costs you the question

  • autoDispose providers are rebuilt every time the widget rebuilds
  • State is destroyed the instant the last listener is removed
  • Without autoDispose the state is freed when the screen is popped
  • autoDispose also prevents the provider from recomputing when dependencies change
  • In Riverpod 3 an auto-dispose notifier must extend AutoDisposeNotifier
open as a page

In Riverpod 3, how does .family give each product id its own state, and why must the argument have a consistent == and hashCode?

level: middleimportance: should knowfreq 50%

basics

~20 s

A family turns one provider definition into a map of independent providers keyed by the argument. Riverpod finds the existing instance by == and hashCode, so an argument without value equality, like a fresh list, creates a new state on every call.

open as a page

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%

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.

open as a page

In Riverpod 3, a product-detail FutureProvider keeps retrying while the device is offline; what is the default retry policy, and how do you customise or disable it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Riverpod 3 retries a provider that throws while building: up to 10 times, starting at 200 ms and doubling to a 6.4 s cap, skipping Error subclasses and ProviderException. Pass retry: to the provider or to ProviderScope; returning null stops retrying.

open as a page

In Riverpod 3, what happens to a product list's provider listeners while a product-detail route covers the list, and how does pausing differ from disposal?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

Riverpod 3 pauses the ref.watch subscriptions of consumers whose TickerMode is off, as on a route hidden under an opaque one. The provider keeps its state and is not disposed, even if autoDispose, but stops notifying them until they resume.

open as a page