Free sample · Pack 01

Read a full rule before buying.

Below is the complete text of riverpod-patterns.mdc — one of the ten rules in Pack 01. This is the exact file your editor loads. Nothing is hidden; nothing is paraphrased. If this reads like the kind of policy you want enforced, the rest of the pack reads the same way.

The file

Frontmatter sets alwaysApply: false with a glob so the rule only fires on Dart files. The body is the policy the agent reads as project-level engineering opinion.

.cursor/rules/riverpod-patterns.mdc.mdc
---
description: Riverpod 2.4+ patterns — providers, notifiers, AsyncValue,
             ref.invalidate, scope. Apply when editing Dart files.
globs: ["**/*.dart"]
alwaysApply: false
---

# Riverpod 2.4+ patterns

You are editing a Flutter project that uses Riverpod 2.4+. Follow these
rules unless the user explicitly opts out.

## Providers

- Prefer code-generated providers (`@riverpod`) over manual `Provider`,
  `StateProvider`, `FutureProvider`, etc., unless the surrounding file
  already uses the manual style consistently.
- Choose the narrowest provider type that fits:
  - Pure-derived sync state → `Provider`.
  - Async one-shot fetch → `FutureProvider` (or `@riverpod` async function).
  - Mutable state with actions → `AsyncNotifier` / `Notifier` (codegen
    preferred: `@riverpod class Foo extends _$Foo`).
- Never expose a raw `Stream`/`Future` to widgets — wrap it in
  `StreamProvider` / `FutureProvider` so consumers see `AsyncValue`.

## AsyncValue discipline

When a widget consumes async state, it must render all three states:

  ```dart
  return value.when(
    data:    (v) => FooContent(v),
    loading: () => const FooSkeleton(),
    error:   (e, st) => FooError(error: e, onRetry: () => ref.invalidate(fooProvider)),
  );
  ```

- Loading must be a skeleton or shimmer, not a centered
  CircularProgressIndicator, unless the screen is < 100ms of work.
- Error must include a retry affordance that calls `ref.invalidate(...)`.
- Don't swallow errors into "—" or empty strings. The error state
  exists; surface it.

## ref.invalidate vs ref.refresh

- `ref.invalidate(p)` — marks stale, refetches when next read. Use
  this from a button "Retry" or a pull-to-refresh.
- `ref.refresh(p)` — refetches now and returns the new value.
  Use this when the caller needs the new value synchronously (rare).
- Never call `ref.refresh` purely for its side effect — it always
  returns a value you should consume or annotate `// ignore: unused_result`.

## Scoping

- Default to a single app-root `ProviderScope`.
- Use feature-scoped `ProviderScope`s only when a subtree needs
  override of a specific provider — e.g., tests, modal flows with
  isolated state.
- Never instantiate a `ProviderContainer` in app code. That's a test-only
  hook.

## Notifier mutation

- Mutations live on the notifier class, not on widgets.
  Widgets call `ref.read(provider.notifier).doX()`.
- Notifier state must be immutable. Use `freezed` or `copyWith`.
- Don't mutate `state.list.add(x)` — that's a reference mutation that
  Riverpod cannot detect. Reassign: `state = state.copyWith(list: [...state.list, x])`.

## Testing

- Override providers with `ProviderScope(overrides: [...])` in widget
  tests; never reach into a real network or real disk from a test.
- Test notifier classes directly with `ProviderContainer.test` — assert
  on emitted `AsyncValue` sequence.

## Counter-examples to refuse

- A widget that `await`s in `build()`. Never.
- A `StatefulWidget` storing data that a `Provider` could own.
- A `Provider` whose `build` reads from `SharedPreferences` directly
  inside a synchronous body. Use `FutureProvider`.
- `ref.watch(p)` inside a callback. Callbacks `ref.read`; only `build`
  watches.

## Output expectation

When asked for a Riverpod feature, generate:

1. The provider/notifier file with codegen annotation if the project
   uses build_runner.
2. The widget that consumes it, with the full `.when` block and a
   skeleton loader.
3. A matching widget test using `ProviderScope(overrides: [...])`.
What this looks like in practice. A Cursor session with this rule active turns "add a profile screen that loads user data" into a generated AsyncNotifier + a widget with full .when + a working widget test in one prompt. The same prompt without the rule typically produces a half-implemented .when with a centered spinner, a forgotten error case, and no test.

Why this works

What's in Pack 01 besides this

This is rule 02 of 10. The others cover Material 3 foundation, async discipline (no UI-thread blocking), state immutability via freezed, widget + golden testing, i18n (AZ/EN/RU), feature-folder architecture, typed error handling, const + RepaintBoundary performance, and Semantics-based accessibility.

See full Pack 01 ($29 / $99) All packs Compare packs