skip to content

With riverpod_generator, why is a @riverpod provider disposed once nothing listens to it, and what does @Riverpod(keepAlive: true) change?

level: middleimportance: should knowfreq 42%

answer

  1. generated default differs from hand-written
  2. isAutoDispose: true in the .g.dart
  3. annotation's keepAlive defaults to false
  4. fixed when generated, not decided at runtime
  5. keepAlive watching auto-dispose gets a lint

basics

~10 s

riverpod_generator makes providers auto-dispose by default, because the annotation's keepAlive field defaults to false. @Riverpod(keepAlive: true) generates a provider that is not auto-disposed, matching a hand-written provider declared without autoDispose.

solid answer

~40 s

Hand-written Riverpod providers are kept alive unless you use `.autoDispose` or pass `isAutoDispose: true`. riverpod_generator flips that: `@riverpod` is `Riverpod()`, whose `keepAlive` defaults to `false`, so the generated provider is built with `isAutoDispose: true` and its state is destroyed once no widget or provider listens. Riverpod's docs say this fits its philosophy; the manual default stayed off to ease migration from package:provider. `@Riverpod(keepAlive: true)` generates `isAutoDispose: false`, which suits app-wide state such as a repository, the signed-in user or settings. It is a declaration-time choice; for a conditional one — keep a result only after a fetch succeeds — stay auto-dispose and call `ref.keepAlive()` in the body. riverpod_lint warns when a keepAlive provider reads an auto-dispose generated provider, because that dependency would never be disposed.

code

dart · 17 lines
dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'lifetime.g.dart';

class WorkoutRepository {
  Future<List<String>> history() async => ['5k run', 'Leg day'];
}

// App-wide: never auto-disposed.
@Riverpod(keepAlive: true)
WorkoutRepository workoutRepository(Ref ref) => WorkoutRepository();

// Screen-scoped: auto-dispose (the codegen default).
@riverpod
Future<List<String>> workoutHistory(Ref ref) {
  return ref.watch(workoutRepositoryProvider).history();
}

go deeper

for a junior

Remember that @riverpod providers auto-dispose by default and that @Riverpod(keepAlive: true) is how you keep app-wide state alive.

for a middle

Explain why the generated default differs from hand-written providers, what isAutoDispose the generator emits, and how the annotation differs from a conditional ref.keepAlive().

for a senior

Show you audit lifetimes: which providers deserve keepAlive, why a keepAlive provider must not watch an auto-dispose one, and how the lint catches that in review.

for a principal

Treat keepAlive as a memory budget decision the team reviews, balancing refetch cost and user-visible freshness against memory held for the life of the app.

## Two defaults for the same flag Every Riverpod provider carries an `isAutoDispose` flag. **Hand-written providers default to `false`**: `Provider`, `FutureProvider` and `NotifierProvider` keep their state for the life of the `ProviderContainer` unless you write `.autoDispose` or pass `isAutoDispose: true`. **riverpod_generator defaults to `true`.** The annotation class `Riverpod` has a `keepAlive` field that defaults to `false`, and the bare `@riverpod` constant is simply `Riverpod()`, so the generated provider class is constructed with `isAutoDispose: true` — you can read it in the `.g.dart` file. Riverpod's documentation explains the flip: auto-disposal better aligns with Riverpod's philosophy, and the hand-written variant kept it off only to accommodate users migrating from `package:provider`. ## What auto-dispose means for a generated provider An auto-dispose provider's state is destroyed once nothing listens to it — no `ref.watch` or `ref.listen` from a widget or another provider. The next read creates it again from scratch. In a fitness app: - a workout-history fetch runs again when the user returns to the history screen; - a heart-rate stream subscription is cancelled when no screen shows it, then reopened; - a set log typed into a form is lost once the form's widgets are gone. The finer mechanics — paused listeners, `onCancel`/`onResume`, timed caches — belong to the auto-dispose and families topic. What codegen changes is the **default**. ## `@Riverpod(keepAlive: true)` Passing `keepAlive: true` makes the generator emit `isAutoDispose: false`. The provider is created on first read and its state survives losing all listeners; it is released when its container is disposed (invalidating it recomputes the value). Typical candidates: - app-wide services: an API client, a database handle, a repository; - the signed-in user or session; - settings loaded once at start-up, such as a units preference; - an object returned through `Raw<T>` whose lifetime should match the app. The annotation form is a **static, declaration-time decision**: it cannot depend on data. ## Annotation versus a runtime keep-alive | | `@Riverpod(keepAlive: true)` | `ref.keepAlive()` inside an auto-dispose provider | |---|---|---| | When decided | when the code is generated | while the provider runs | | Conditional | no | yes, e.g. only after a successful fetch | | Reversible | no | the returned `KeepAliveLink` can be closed | | Visible to lints | yes, rules read the annotation | not as a lifetime declaration | A common pattern keeps a provider auto-dispose and calls `ref.keepAlive()` after an `await` succeeds, so failed results are still thrown away. A provider that calls `ref.keepAlive()` unconditionally as its first statement is a keepAlive provider written the long way; the pinned riverpod_lint changelog lists an unreleased rule, `prefer_keep_alive_annotation`, that will suggest the annotation there. ## The trap: keepAlive depending on auto-dispose If a `keepAlive: true` provider `ref.watch`es a plain `@riverpod` provider, the watcher never goes away, so the auto-dispose provider **always has a listener and is never disposed** — it silently behaves as keepAlive, and its memory is never reclaimed. riverpod_lint reports this for generated providers; the rule id in the 3.1.9 source is `only_use_keep_alive_inside_keep_alive` (the README heading calls it `avoid_keep_alive_dependency_inside_auto_dispose`). Resolve it deliberately: 1. mark the dependency `keepAlive: true` as well, if it genuinely is app-wide; or 2. drop `keepAlive` from the watcher, if recreating it is cheap. ## Checking what the generator decided Because the lifetime is baked into generated code, it is easy to verify rather than guess: - open the `.g.dart` file and read the `isAutoDispose:` argument passed to the provider's constructor — `true` for plain `@riverpod`, `false` for `@Riverpod(keepAlive: true)`; - in a test, create a `ProviderContainer`, read the provider once through a subscription, close the subscription, and let the container settle: an auto-dispose provider re-runs its body on the next read, a keepAlive one does not; - in review, search for `keepAlive: true` and ask, for each hit, what would break if it were recreated. The same annotation also works on class-based providers: `@Riverpod(keepAlive: true)` above a Notifier class keeps both its state and its notifier instance, so methods such as `logSet` keep operating on the same state across screens. ## Choosing Leave the default for screen-scoped and parameterised data, where reclaiming memory and fetching fresh data are what you want. Reach for `keepAlive: true` when recreating the value is expensive or loses state users expect to persist, and keep the dependency graph consistent so the lint stays quiet. Treat every `keepAlive: true` as a small, reviewed decision rather than a reflex, because each one is memory the app never gives back.

  • Is it fine for a plain @riverpod provider to watch a keepAlive: true provider?
    Yes. The auto-dispose provider can still be disposed when its own listeners leave, and the keepAlive dependency simply outlives it. Only the opposite direction is a problem: a keepAlive provider watching an auto-dispose one keeps that dependency listened to for good, which riverpod_lint flags for generated providers.
  • When would you keep a provider auto-dispose and call ref.keepAlive() instead of using the annotation?
    When keeping the state depends on what happens at runtime, such as keeping a fetched result only after the request succeeds so failures are discarded, or when the returned KeepAliveLink should be closed later, for example after a timer. The annotation cannot express a condition or be undone.

A generated provider is like a gym locker rented by the visit: when the last person using it leaves, it is emptied. keepAlive: true is a permanent locker; and if a permanent locker holder keeps a key to a by-the-visit locker, that one is never emptied either.

saying these in an interview costs you the question

  • Generated providers are kept alive by default, just like hand-written ones.
  • The annotation takes an autoDispose parameter to switch disposal off.
  • A keepAlive provider watching an auto-dispose one lets that one dispose normally.
  • keepAlive: true means the provider can never be recomputed.
  • ref.keepAlive() and @Riverpod(keepAlive: true) are interchangeable in every case.