Loading...
Loading...
Enforces feature state as one Notifier/AsyncNotifier/StreamNotifier ViewModel over an immutable state value with value equality, private mutable state mutated only through intent methods, derive-don't-store, a single write path through a repository, and unidirectional data flow; Riverpod 3.x is DI (providers-as-collaborators, ProviderScope overrides per flavor, throwing seams), reads split ref.watch/.select for display vs ref.read(p.notifier) in callbacks vs ref.listen for side effects, async modeled as AsyncValue, per-entity state family-keyed + autoDispose, and stale-closure captures / legacy StateProvider-StateNotifierProvider-ChangeNotifierProvider / get_it / package:provider are banned. Use when adding state to a screen, writing a feature controller/ViewModel, wiring providers or DI, deriving a read model from a stream, or reviewing rebuild/state-leak/disposal/write-path issues.
npx skill4agent add zakariaf/flutter-skills state-management-riverpodNotifierAsyncNotifierStreamNotifierConsumerWidgetreferences/ownership-and-lifecycle.mdreferences/reads-and-side-effects.mdvoidref.mountedBuildContextreferences/riverpod3-api-and-testing.mdoverrideWithValueProviderContainer.testscripts/ban-legacy-providers.shfreezedcopyWithnotifier.rename(id, name)copyWithnewBuildContextAsyncValueisLoadinghasErrorDateTime.now()Clockpackage:clockclockProviderclockProvider.overrideWithValue(Clock.fixed(t))DateTime.now()ClockServiceservice-boundary-and-nativeclockProviderreferences/ownership-and-lifecycle.md| Owns | Shape |
|---|---|
| Per-feature presentation state over a live query | |
| Per-feature presentation state, non-stream | |
| A reactive projection of the DB | |
An injected collaborator (repo, service, | plain |
| Per-entity/per-session state | |
StreamNotifierProviderAsyncNotifierProvider(...).autoDispose.family@riverpodNotifierAsyncNotifierStreamNotifierautoDisposefamilyAutoDispose**Family// features/task — one StreamNotifier ViewModel: build() returns the LIVE repo stream,
// so a committed write re-emits and the UI updates with no manual republish (D6).
class TaskListNotifier extends StreamNotifier<TaskListState> {
Stream<TaskListState> build() {
final filter = ref.watch(taskFilterProvider); // in-session filter, its own Notifier
return ref.watch(taskRepositoryProvider).watchAll() // the live source of truth
.map((tasks) => TaskListState(tasks: tasks, filter: filter));
}
// Durable act routes through the single write path. void so the call site never drops
// the Future; no `state = ...` — the committed write makes watchAll() re-emit.
void complete(TaskId id) =>
unawaited(ref.read(taskRepositoryProvider).markComplete(id).catchError(_report));
void _report(Object error, StackTrace stack) {/* surface via a logger — never swallow */}
}
final taskListNotifierProvider =
StreamNotifierProvider.autoDispose<TaskListNotifier, TaskListState>(
TaskListNotifier.new);
// The reversible in-session filter is its own tiny Notifier (manual NotifierProvider).
class TaskFilterNotifier extends Notifier<TaskFilter> {
TaskFilter build() => TaskFilter.all;
void set(TaskFilter f) => state = f; // watched by build() above; re-projects the stream
}
final taskFilterProvider =
NotifierProvider<TaskFilterNotifier, TaskFilter>(TaskFilterNotifier.new);// A seam: throws until a flavor main() overrides it. One live impl per flavor.
final databaseProvider = Provider<AppDatabase>(
(ref) => throw UnimplementedError('override databaseProvider in main()'),
);
final taskRepositoryProvider = Provider<TaskRepository>(
(ref) => TaskRepository(ref.watch(databaseProvider)), // inject, never `new` a live DB here
);// main.dart — the ONLY place live collaborators are constructed.
void main() {
final db = AppDatabase.open();
runApp(ProviderScope(
overrides: [databaseProvider.overrideWithValue(db)],
child: const App(),
));
}familyreferences/reads-and-side-effects.mdref.watch(p)ref.watch(p.select((s) => s.field))build().selectwatchref.read(p.notifier)onTaponPressedreadbuild()watchref.listen(p, ...)build()build()class TaskCounter extends ConsumerWidget {
const TaskCounter({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final n = ref.watch(taskListNotifierProvider.select((s) => s.value?.openCount ?? 0));
return Text('$n');
}
}ref.watchonTapref.read// WRONG — acts on a stale item after a re-render.
onTap: () => ref.read(p.notifier).complete(task),
// RIGHT — id is stable; resolve now.
onTap: () => ref.read(p.notifier).complete(task.id),voidonTap: () => notifier.doThing()Futurediscarded_futuresunawaited_futuresVoidCallbackvoidunawaited(_run().catchError(_report))async-safety.family@freezed.autoDisposeProviderautoDisposeawaitref.mounteduse_build_context_synchronouslyref.onDisposeStreamProviderMediaQuery.boldTextOf(context).textScalerOf(context).highContrastOf(context)MediaQueryInheritedWidgetBuildContextBuildContextaccessibility-as-codeStateProviderStateNotifierProviderChangeNotifierProviderflutter_riverpod/legacy.dartget_itpackage:providerref.watch.selectConsumerWidgetsetState()setStateStatenewBuildContextfamilyautoDisposekeepAlive()autoDisposeNotifierAsyncNotifierStreamNotifierAsyncValueref.watch.selectref.read(p.notifier)ref.listenmainBuildContextDateTime.now()familyautoDisposeref.onDisposelegacy.dartget_itpackage:providerscripts/ban-legacy-providers.shflutter-architectureerror-handling-typed-resultsasync-safetymountedpersistence-driftStreamProviderservice-boundary-and-nativeclockProviderpackage:clockDateTime.now()widget-compositionConsumerWidgetaccessibility-as-codeMediaQueryBuildContextvalue-objects-money-and-unitsClockapp-startup-and-bootstrapProviderScopepackage:providerChangeNotifierChangeNotifiernotifyListeners()_privateList.unmodifiable(...)ProviderProxyProviderget_itcontext.selectConsumerSelectorcontext.readdidChangeDependenciesinitState_disposednotifyListeners()awaitdispose()enum ViewStatus { idle, loading, ready, error }
class TaskListViewModel extends ChangeNotifier {
TaskListViewModel(this._repo); // injected abstraction
final TaskRepository _repo;
ViewStatus _status = ViewStatus.idle;
ViewStatus get status => _status;
List<Task> _tasks = const [];
List<Task> get tasks => List.unmodifiable(_tasks); // immutable view
int get openCount => _tasks.where((t) => !t.done).length; // derive, don't store
Future<void> load() async {
_status = ViewStatus.loading; notifyListeners();
try {
_tasks = await _repo.load();
_status = ViewStatus.ready;
} catch (_) {
_status = ViewStatus.error;
}
notifyListeners(); // exactly one, after all fields set
}
Future<void> complete(TaskId id) async {
await _repo.markComplete(id); // single write path
_tasks = [for (final t in _tasks) t.id == id ? t.copyWith(done: true) : t];
notifyListeners();
}
}