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
- Specific over general. "Use Riverpod 2.4 idioms" is too vague. Naming AsyncNotifier, .when, ref.invalidate, and listing counter-examples gives the model something to refuse.
- Counter-examples named. A rule that lists "what to refuse" is twice as effective as one that lists only "what to do."
- Output expectation explicit. The last section tells the agent exactly what to generate in response to the typical prompt class. No interpretation gaps.
- Scoped via globs. The rule fires only on .dart files. It never pollutes Python or TypeScript work.
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