skip to content

In Riverpod 3, when do you choose AsyncNotifierProvider over FutureProvider, and how do an AsyncNotifier's build, state, future and update work?

level: middleimportance: should knowfreq 45%

answer

  1. a FutureProvider with methods
  2. build may return a Future
  3. starts as AsyncLoading
  4. future skips loading states
  5. update keeps the old data visible

basics

~20 s

Choose AsyncNotifierProvider when asynchronously loaded state must also be changed, like a user profile the user can rename. build() loads it, state is an AsyncValue, future resolves to the first non-loading value, and update() changes data without passing through loading.

solid answer

~40 s

`FutureProvider` is read-only: it can only re-run. When the UI must also edit the loaded value, an `AsyncNotifier<T>` exposed by `AsyncNotifierProvider` is effectively a `FutureProvider` with methods. Its `build()` returns `FutureOr<T>` - typically the profile fetched from a repository - and `state` is an `AsyncValue<T>` that starts as `AsyncLoading` before `build()` runs; if `build()` throws or its future fails, `state` becomes `AsyncError` instead of the error escaping. Inside methods, `future` gives a `Future<T>` resolving with the first non-loading state, and `update((previous) async => ...)` applies a change to the current data without switching `state` to loading, then sets `AsyncData` with the result; if the callback fails, `update` completes with that error and leaves `state` as it was. Widgets watch the provider as `AsyncValue<T>` and call methods through `.notifier`.

code

dart · 19 lines
dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

final profileProvider =
    AsyncNotifierProvider<ProfileNotifier, UserProfile>(ProfileNotifier.new);

class ProfileNotifier extends AsyncNotifier<UserProfile> {
  @override
  Future<UserProfile> build() {
    return ref.watch(chatRepositoryProvider).fetchProfile();
  }

  Future<void> rename(String displayName) async {
    await update((profile) async {
      final updated = profile.copyWith(displayName: displayName);
      await ref.read(chatRepositoryProvider).saveProfile(updated);
      return updated;
    });
  }
}

go deeper

for a junior

Recall that AsyncNotifier is the class-based, editable version of FutureProvider and that watching it gives an AsyncValue.

for a middle

Explain build's FutureOr return, the initial AsyncLoading, errors becoming AsyncError, and the difference between future and update.

for a senior

Choose between update and explicit state assignments based on the UX you want during a save, and migrate StateNotifier<AsyncValue> code cleanly.

for a principal

Define how a codebase models asynchronous edits - optimistic, pessimistic or with explicit progress - so screens behave consistently.

## Why a class for async state In **Riverpod 3**, `FutureProvider` is the simplest way to load something once: its function returns a `Future`, widgets watch an `AsyncValue`. But it has no methods, so a screen that edits the loaded value has nowhere to put the edit. **`AsyncNotifier<T>`** fills that gap. Riverpod's own migration guide describes it as a `FutureProvider` that can expose ways to be modified from the UI. | Need | `FutureProvider` | `AsyncNotifierProvider` | |---|---|---| | Load data asynchronously | yes | yes, in `build()` | | Watched as | `AsyncValue<T>` | `AsyncValue<T>` | | Methods that change the data | no | yes | | Access to `ref` | in its function | as a property of the notifier | ## build() `build()` is declared as `FutureOr<T> build()`, so it can be `async` and return a `Future`, or return a value synchronously. For a messaging app's profile: - `build()` watches the repository provider and returns `repository.fetchProfile()`; - before it runs, `state` is `AsyncLoading`; - if it throws, or its future fails, the error is caught and `state` becomes `AsyncError` - the provider does not crash; - when a provider it watches changes, `build()` runs again on the same notifier instance. ## state, future and update Inside the notifier: - **`state`** is an `AsyncValue<T>`. You can assign it directly - for example `state = AsyncData(updated)`. - **`future`** is a `Future<T>` that resolves with the first state that is not `AsyncLoading`. It does not necessarily wait for `build()`: if `state` is assigned first, `future` resolves with that. It fails if the state is an error. - **`update(cb, {onError})`** awaits `future`, passes the current value to `cb`, and assigns `AsyncData` with the result. It deliberately **does not** set `state` to loading while `cb` runs, and **does not** set `state` to an error if `cb` fails - the returned future fails instead. If `state` was already an error, `cb` is skipped unless `onError` is given. That makes `update` a good fit for an edit where the old profile should stay on screen while saving: 1. The user submits a new display name. 2. `rename(name)` calls `update`, which saves through the repository and returns the new profile. 3. On success, `state` becomes `AsyncData(newProfile)` and watchers rebuild. 4. On failure, the old profile stays visible and the method's future fails, so the caller can show a message. If the UI should instead show a spinner during the save, assign a loading state before the work and a data or error state after it. ## Migrating from StateNotifier<AsyncValue<T>> Older code often modelled the same thing as a `StateNotifier<AsyncValue<T>>` with hand-written `try`/`catch` blocks. Riverpod's guide lists the steps to an `AsyncNotifier<T>`: - put the initialization logic into `build()`; - remove the `try`/`catch` blocks around initialization, since errors in `build()` already become `AsyncError`; - remove `AsyncValue.guard` from `build()`. ## Pitfalls - **Using `FutureProvider` and then trying to modify it** - there is no setter; move to `AsyncNotifier`. - **Doing initialization in the constructor** - `ref` and `state` are not available until the notifier is attached. - **Expecting `update` to show a loading state** - it does not, by design. - **Forgetting that `update` fails its own future** - a caller that does not catch it loses the error, since `state` never shows it.

  • What does state hold before build() of an AsyncNotifier has finished?
    `AsyncLoading`. Riverpod initializes asynchronous notifiers to a loading state before calling `build()`, which is why reading `state` inside an async `build()` is safe, unlike in a synchronous `Notifier` where it can throw before a value exists.
  • When is assigning state directly better than update()?
    When the UI should reflect progress or failure of the operation itself - for example setting a loading state so a spinner appears, then data or error afterwards. `update` hides both: it keeps the old data during the work and never turns `state` into an error.

An AsyncNotifier is like a shop window with a price tag being reprinted: update() leaves the old tag in the window while the new one prints, and swaps it only when the new tag is ready. If the printer jams, the old tag simply stays.

saying these in an interview costs you the question

  • Tries to assign a new value to a FutureProvider from a button handler.
  • Wraps build() in try/catch and sets AsyncError by hand.
  • Expects update() to put the provider into a loading state.
  • Believes a failing build() crashes the app instead of producing AsyncError.
  • Loads the initial data in the AsyncNotifier's constructor.