State — Riverpod
Irbisa · cheatsheetSeptember 13, 2026

State — Riverpod

Riverpod providers, AsyncNotifier, ref, code generation

Senior Developer20 itemscompressed for a skim
  1. 01

    Explain Riverpod: providers, WidgetRef, and AsyncNotifier.

    Hard

    Riverpod 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>> {
    …
  2. 02

    Explain Riverpod family modifier and how to override providers in tests.

    Hard

    family turns one declaration into a parametrized set of providers: productProvider(42) and productProvider(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);
    });
    …
  3. 03

    What's the difference between ref.watch, ref.read, and ref.listen?

    Medium

    All three live on Ref (in providers) and WidgetRef (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
    …
  4. 04

    How does autoDispose work, and when do you need keepAlive?

    Medium

    By 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);
      },
    …
  5. 05

    How does Riverpod code generation (riverpod_generator) work?

    Medium

    riverpod_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) => ...);
    …
  6. 06

    When should you pick Notifier vs AsyncNotifier vs StateProvider?

    Medium

    All 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>> {
    …
  7. 07

    How do you control rebuilds with select() and Equatable-style equality?

    Hard

    By 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,
    …
  8. 08

    How do ProviderScope overrides work — at app start vs at runtime?

    Hard

    Overrides 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'),
    …
  9. 09

    How do you fire side effects (navigation, snackbars, dialogs) from Riverpod state?

    Medium

    Side 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. 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?

    Medium

    The flash happens because something threw the previous value away — an AsyncValue can carry the old data through a reload, and assigning a bare AsyncLoading is 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. 11

    A provider needs data from two other async providers before it can compute anything. How do you write it without nesting when() calls?

    Medium

    Have 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. 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?

    Hard

    The 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. 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?

    Medium

    Riverpod 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. 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?

    Medium

    initState runs 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. 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?

    Medium

    Nothing 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. 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?

    Medium

    Attach a ProviderObserver to the root ProviderScope: 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. 17

    A user is halfway through editing a form held in an AsyncNotifier when the session token refreshes, and their edits vanish. What happened?

    Hard

    The notifier watched the session inside build(), so the token refresh disposed the current state and re-ran build() 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. 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?

    Hard

    Riverpod 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. 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?

    Hard

    Create the ProviderContainer yourself, use it from plain Dart, and hand that exact container to the widget tree with UncontrolledProviderScopeProviderScope builds 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. 20

    Riverpod asks you to declare providers as top-level final variables — isn't that exactly the global mutable state everyone warns about?

    Hard

    The global holds no state: a provider object is an immutable descriptor — a factory plus its modifiers — and the value it describes lives in whichever ProviderContainer reads 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();
    …