State — Riverpod
Riverpod providers, AsyncNotifier, ref, code generation
- 01
Explain Riverpod: providers, WidgetRef, and AsyncNotifier.
HardRiverpod is a complete Provider rewrite with compile-time safety.
// ── Simple providers ────────────────────────────── final apiUrlProvider = Provider<String>((ref) => 'https://api.example.com'); final counterProvider = StateProvider<int>((ref) => 0); // ── AsyncNotifier — recommended for async state ──── class ProductsNotifier extends AsyncNotifier<List<Product>> { … - 02
Explain Riverpod family modifier and how to override providers in tests.
Hardfamilyturns one declaration into a parametrized set of providers:productProvider(42)andproductProvider(7)are separate instances with separate cached state.// ── family modifier ──────────────────────────────── // Provider parameterized by product ID final productProvider = FutureProvider.family<Product, int>((ref, id) async { final repo = ref.watch(productRepositoryProvider); return repo.getProduct(id); }); … - 03
What's the difference between ref.watch, ref.read, and ref.listen?
MediumAll three live on
Ref(in providers) andWidgetRef(in widgets), but they have very different semantics.final counterProvider = StateProvider<int>((ref) => 0); final authProvider = AsyncNotifierProvider<AuthNotifier, User?>(AuthNotifier.new); class HomePage extends ConsumerWidget { const HomePage({super.key}); @override … - 04
How does autoDispose work, and when do you need keepAlive?
MediumBy default a Riverpod provider is cached forever in the ProviderContainer.
// Screen-scoped — cached only while the screen is alive final searchResultsProvider = FutureProvider.autoDispose.family<List<Hit>, String>( (ref, query) async { // ✅ Disposed when the search screen pops return ref.watch(apiProvider).search(query); }, … - 05
How does Riverpod code generation (riverpod_generator) work?
Mediumriverpod_generator is a build_runner code-gen layer over flutter_riverpod.
// dev_dependencies: riverpod_generator, riverpod_annotation, build_runner // 1) Plain provider @riverpod String apiBaseUrl(Ref ref) => const String.fromEnvironment('API_URL'); // → final apiBaseUrlProvider = AutoDisposeProvider<String>((ref) => ...); … - 06
When should you pick Notifier vs AsyncNotifier vs StateProvider?
MediumAll three hold mutable state, so choose by how it is created: a bare value, synchronous logic, or an async load whose loading and error phases consumers have to render.
// ── StateProvider — primitive value, no logic ── final queryProvider = StateProvider<String>((ref) => ''); // Caller: ref.read(queryProvider.notifier).state = q; // ── Notifier — synchronous logic, no IO ── class FilterSet extends Notifier<Set<String>> { … - 07
How do you control rebuilds with select() and Equatable-style equality?
HardBy default, watching a provider rebuilds the consumer on ANY change to the state.
@freezed abstract class UiState with _$UiState { const factory UiState({ required ThemeMode theme, required Locale locale, required bool sidebarOpen, … - 08
How do ProviderScope overrides work — at app start vs at runtime?
HardOverrides replace a provider's implementation within a scope.
// 1) Root override — flavor-aware base URL void main() { runApp(ProviderScope( overrides: [ apiBaseUrlProvider.overrideWithValue( const String.fromEnvironment('API_URL', defaultValue: 'https://staging.example.com'), … - 09
How do you fire side effects (navigation, snackbars, dialogs) from Riverpod state?
MediumSide effects must run once per state transition, but
build()can run many times for the same state — so navigation, snackbars and dialogs never belong inside it.// State exposes a transient action — widget reacts via ref.listen class AuthNotifier extends AsyncNotifier<User?> { @override Future<User?> build() async => null; Future<void> signIn(String email, String pwd) async { state = const AsyncLoading(); … - 10
Pull-to-refresh wipes your list and flashes a spinner before the new data lands. What causes that, and how do you keep the old rows on screen?
MediumThe flash happens because something threw the previous value away — an AsyncValue can carry the old data through a reload, and assigning a bare
AsyncLoadingis what discards it.class TodosNotifier extends AsyncNotifier<List<Todo>> { @override Future<List<Todo>> build() => ref.watch(apiProvider).fetchTodos(); // ✅ list stays on screen while the server round-trip runs Future<void> toggle(Todo t) async { … - 11
A provider needs data from two other async providers before it can compute anything. How do you write it without nesting when() calls?
MediumHave the derived provider await the sources' futures instead of pattern-matching two AsyncValues against each other.
final userProvider = FutureProvider<User>((ref) => ref.watch(apiProvider).me()); final settingsProvider = FutureProvider<Settings>((ref) => ref.watch(apiProvider).settings()); // ✅ Combine by awaiting .future — one AsyncValue comes out final dashboardProvider = FutureProvider<Dashboard>((ref) async { … - 12
Your AsyncNotifier awaits a slow request and then touches ref; the user pops the screen mid-flight. What breaks, and how do you write it safely?
HardThe provider is disposed the moment its last listener goes away, so the code that resumes after the await is running on a dead Ref and throws instead of quietly doing nothing.
class UploadNotifier extends AutoDisposeAsyncNotifier<Upload> { @override Future<Upload> build() async { final token = CancelToken(); ref.onDispose(token.cancel); // ✅ abandon the request on dispose … - 13
A Notifier holds a List and
state.add(item)changes nothing on screen;state = state..add(item)doesn't help either. What is Riverpod doing?MediumRiverpod notifies listeners only when the new state is not
==to the old one, and both of those lines hand back the very same list instance — which is equal to itself.class Cart extends Notifier<List<Item>> { @override List<Item> build() => const []; // ❌ the setter is never called — nothing is notified void addBroken(Item i) => state.add(i); … - 14
A screen writes a provider's state from initState and Riverpod throws "Tried to modify a provider while the widget tree was building" — where does that write belong?
MediuminitStateruns inside the frame's build phase, and a provider changing there would have to notify widgets that this frame has already built — so Riverpod refuses instead of rendering a tree that shows two different truths.// ❌ Tried to modify a provider while the widget tree was building class _FilterScreenState extends ConsumerState<FilterScreen> { @override void initState() { super.initState(); ref.read(filterProvider.notifier).set(widget.initial); … - 15
In a unit test you call a notifier method through container.read and then assert, but the state is back to its initial value. What is missing?
MediumNothing is listening, so the autoDispose provider is disposed the moment your read returns; the next read builds a brand-new instance and your mutation went away with the old one.
class Cart extends AsyncNotifier<List<Item>> { @override Future<List<Item>> build() => ref.watch(cartRepoProvider).load(); Future<void> addItem(Item i) async { await ref.read(cartRepoProvider).save(i); … - 16
QA says the cart total goes wrong after one specific navigation and nobody can reproduce it — how do you make Riverpod tell you which provider changed and when?
MediumAttach a
ProviderObserverto the rootProviderScope: Riverpod then reports every provider that is created, updated, disposed or fails, in order, without a line of instrumentation inside the providers themselves.class StateLogger extends ProviderObserver { // Riverpod 3 signatures; 2.x passed (provider, prev, next, container) @override void didUpdateProvider( ProviderObserverContext context, Object? previousValue, … - 17
A user is halfway through editing a form held in an AsyncNotifier when the session token refreshes, and their edits vanish. What happened?
HardThe notifier watched the session inside
build(), so the token refresh disposed the current state and re-ranbuild()from scratch — the draft was state a mutator had written, and re-initialization keeps none of it.// ❌ the draft lives downstream of a value that changes on its own class EditProfileBroken extends AsyncNotifier<ProfileDraft> { @override Future<ProfileDraft> build() async { final session = ref.watch(sessionProvider); // token rotates → build() re-runs final user = await ref.watch(apiProvider).me(session.token); … - 18
After the Riverpod 3 upgrade, a screen whose request returns 400 keeps re-firing that request every few seconds. What is doing that, and how do you stop it?
HardRiverpod 3 retries a failed provider automatically — exponential backoff starting around 200 ms and doubling up to roughly 6.4 s — so an error that will never fix itself becomes an endless loop until you tell Riverpod which errors are worth retrying.
void main() { runApp(ProviderScope( // ✅ one policy for the whole app retry: (retryCount, error) { if (retryCount >= 3) return null; // give up, show the error UI … - 19
A push handler and your main() bootstrap both need the repositories the app reads through providers — how do you do that without ending up with two copies of the state?
HardCreate the
ProviderContaineryourself, use it from plain Dart, and hand that exact container to the widget tree withUncontrolledProviderScope—ProviderScopebuilds its own container, so keeping both is what duplicates the state.Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); // ✅ one container: built in main(), warmed up, then handed to the tree final container = ProviderContainer( overrides: [ … - 20
Riverpod asks you to declare providers as top-level final variables — isn't that exactly the global mutable state everyone warns about?
HardThe global holds no state: a provider object is an immutable descriptor — a factory plus its modifiers — and the value it describes lives in whichever
ProviderContainerreads it.// A key, not a box: no state is stored in this variable. final counterProvider = NotifierProvider<Counter, int>(Counter.new, name: 'counter'); void main() { final a = ProviderContainer(); …