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?
answer
- new in 3.0
- TickerMode of the hidden route
- paused still counts as a listener
- onCancel on pause, onResume after
- streams pause, state stays
basics
~20 sRiverpod 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.
solid answer
~40 sSince Riverpod 3.0, a consumer widget follows its `TickerMode`: when Flutter hides a route under an opaque one, tickers there are disabled, and the consumer pauses its `ref.watch` subscriptions. A provider whose listeners are all paused becomes inactive: `onCancel` fires, a stream-based provider pauses its `StreamSubscription`, invalidations wait, and the hidden widget stops rebuilding. It is **not** disposed: paused listeners still count, so even an autoDispose list provider keeps its state and resumes, firing `onResume`, when the list becomes visible. Disposal, by contrast, needs the listeners removed. You can pause manually with `ProviderSubscription.pause()` or a `TickerMode(enabled: false)`, but not globally switch it off; for work that must keep running, keep a listener somewhere that stays visible.
code
dart · 9 linesimport 'package:flutter_riverpod/flutter_riverpod.dart';
final productListProvider =
StreamProvider.autoDispose<List<ProductSummary>>((ref) {
ref.onCancel(() => log('list hidden or left: no active listeners'));
ref.onResume(() => log('list visible again'));
ref.onDispose(() => log('list state destroyed'));
return ref.watch(catalogApiProvider).watchProducts();
});go deeper
Remember that in Riverpod 3 a hidden screen's watched providers are paused, not destroyed, so they come back instantly.
Explain TickerMode-driven pausing, onCancel and onResume firing, stream subscriptions pausing, and why paused listeners block autoDispose.
Audit onCancel handlers written for 2.x, and keep background-critical providers listened from a visible part of the tree.
Weigh the resource savings of pausing against features that must run while unseen, and make that distinction explicit in the architecture.
## The scenario A catalogue app shows a product list watching `productListProvider`, a `StreamProvider` fed by live price updates. Tapping a product pushes a full-screen detail route. In Riverpod 2, the hidden list kept listening and kept receiving price updates nobody could see. **Riverpod 3 pauses it.** ## How Riverpod knows the list is hidden Flutter's overlay wraps each route in a `TickerMode`, and disables it for entries hidden behind an opaque route. Riverpod's consumer element listens to `TickerMode.getNotifier(context)`. When the value turns false, it calls `pause()` on the subscriptions created by `ref.watch` in that consumer; when it turns true, it calls `resume()`. In the flutter_riverpod 3.4.3 source this applies to the watch subscriptions; `ref.listen` and `listenManual` subscriptions are not paused by it. ## What pausing does A provider counts its listeners and how many of them are paused. When every listener is paused, the provider is **inactive**: 1. `ref.onCancel` callbacks run, exactly as if the last listener had left. 2. A provider built from a `Stream` pauses its underlying `StreamSubscription`. 3. The provider's own subscriptions to other providers are paused too. 4. An invalidation is not turned into a rebuild while the provider is paused. 5. The paused widget does not rebuild. When a listener resumes, `ref.onResume` runs and the consumer receives the current value. ## Pausing versus disposal | | Paused | Disposed (autoDispose) | |---|---|---| | Trigger | all listeners paused, e.g. hidden route | all listeners removed, e.g. popped route | | State | kept in memory | destroyed | | `onCancel` | fires | fires first | | `onDispose` | does not fire | fires | | Coming back | `onResume`, same state | fresh build, refetch | The key rule from the source: Riverpod's scheduled disposal skips any provider that still has non-weak listeners, and a paused listener is still a listener. So an autoDispose `productListProvider` survives the whole time the detail route is on top, and the list reappears instantly when the user goes back. ## Controlling it - **Manual pause**: `ref.listen` in a provider returns a `ProviderSubscription` with `pause()` and `resume()`. - **Force a pause**: wrapping a `Consumer` in `TickerMode(enabled: false)` pauses its watch subscriptions. - **No global switch**: the 3.0 migration guide says pausing cannot be disabled app-wide. - **Forcing resume does not work through TickerMode**: the migration guide shows `TickerMode(enabled: true)` as a way to never pause, but Flutter combines a `TickerMode` with its ancestors, so it cannot re-enable tickers under a hidden route. - **Work that must continue** - for example a provider syncing a cart - should be kept alive by a listener that stays visible, such as a widget above the `Navigator`, rather than depending on a hidden screen. ## Why it matters Pausing saves bandwidth, battery and rebuilds for invisible UI, but it changes behaviour that 2.x code may have relied on: a hidden screen's stream no longer delivers, and `onCancel` now fires on navigation even though nothing is disposed.
- Why does onCancel now fire when you merely push a route, and what breaks if code assumed otherwise?Because in Riverpod 3 onCancel means 'no active listeners', and paused listeners are inactive. Code that treated onCancel as 'about to be disposed' - for example closing a connection there - now closes it on every navigation, although the provider will resume. Put teardown in onDispose, and use onCancel only for things that should stop while unseen.
- A cart-sync provider must keep running while the user browses product details; how do you stop it being paused?Keep a listener on it from a widget that stays visible, such as one wrapping the app above the Navigator, or from another provider that such a widget watches. Pausing follows the TickerMode of the listening widget, so a listener outside any hidden route keeps the provider active.
saying these in an interview costs you the question
- A paused autoDispose provider is disposed after one frame
- Pausing destroys the state and refetches on resume
- TickerMode(enabled: true) forces a Consumer to listen under a hidden route
- onDispose fires when a route is covered by another
- Pausing can be disabled globally with a ProviderScope flag