Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when setting up providers, combining requests, managing state disposal, passing arguments, performing side effects, or testing providers (Riverpod).
.claude/skills/evanca-riverpod/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 67% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 231% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 51% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 120% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 26% | 0% |
This skill defines how to correctly use Riverpod for state management in Flutter and Dart applications.
dartvoid main() { runApp(const ProviderScope(child: MyApp())); }
ProviderScope directly in runApp — never inside MyApp.riverpod_lint to enable IDE refactoring and enforce best practices.dart// Functional provider (codegen) @riverpod int example(Ref ref) => 0; // FutureProvider (codegen) @riverpod Future<List<Todo>> todos(Ref ref) async { return ref.watch(repositoryProvider).fetchTodos(); } // Notifier (codegen) @riverpod class TodosNotifier extends _$TodosNotifier { @override Future<List<Todo>> build() async { return ref.watch(repositoryProvider).fetchTodos(); } Future<void> addTodo(Todo todo) async { ... } }
final top-level variables.Provider, FutureProvider, or StreamProvider based on the return type.ConsumerWidget or ConsumerStatefulWidget instead of StatelessWidget/StatefulWidget when accessing providers.| Method | Use for | |---|---| | ref.watch | Reactively listen — rebuilds when value changes. Use during build phase only. | | ref.read | One-time access — use in callbacks/Notifier methods, not in build. | | ref.listen | Imperative subscription — prefer ref.watch where possible. | | ref.onDispose | Cleanup when provider state is destroyed. |
dart// In a widget class MyWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final value = ref.watch(myProvider); return Text('$value'); } } // Cleanup in a provider final provider = StreamProvider<int>((ref) { final controller = StreamController<int>(); ref.onDispose(controller.close); return controller.stream; });
ref.watch inside callbacks, listeners, or Notifier methods.ref.read(yourNotifierProvider.notifier).method() to call Notifier methods from the UI.context.mounted before using ref after an await in async callbacks.dart@riverpod Future<String> userGreeting(Ref ref) async { final user = await ref.watch(userProvider.future); return 'Hello, ${user.name}!'; }
ref.watch(asyncProvider.future) to await an async provider's resolved value.dart@riverpod Future<Todo> todo(Ref ref, String id) async { return ref.watch(repositoryProvider).fetchTodo(id); } // Usage final todo = ref.watch(todoProvider('some-id'));
autoDispose for parameterized providers to prevent memory leaks.Dart 3 records or code generation for multiple parameters — they naturally override ==.List or Map as parameters (no == override); use const collections, records, or classes with proper equality.provider_parameters lint rule from riverpod_lint to catch equality mistakes.keepAlive: true..autoDispose to enable disposal.dart// keepAlive with timer ref.onCancel(() { final link = ref.keepAlive(); Timer(const Duration(minutes: 5), link.close); });
ref.onDispose for cleanup; do not trigger side effects or modify providers inside it.ref.invalidate(provider) to force destruction; use ref.invalidateSelf() from within the provider.ref.refresh(provider) to invalidate and immediately read the new value — always use the return value.Providers are lazy by default. To eagerly initialize:
dart// In MyApp or a dedicated widget under ProviderScope: Consumer( builder: (context, ref, _) { ref.watch(myEagerProvider); // forces initialization return const MyApp(); }, )
main()) for consistent test behavior.AsyncValue.requireValue to read data directly and throw clearly if not ready.dart@riverpod class TodosNotifier extends _$TodosNotifier { Future<void> addTodo(Todo todo) async { state = const AsyncLoading(); state = await AsyncValue.guard(() async { await ref.read(repositoryProvider).addTodo(todo); return [...?state.value, todo]; }); } } // In UI: ElevatedButton( onPressed: () => ref.read(todosNotifierProvider.notifier).addTodo(todo), child: const Text('Add'), )
ref.read (not ref.watch) in event handlers.ref.invalidateSelf(), or manually updating the cache.dartclass MyObserver extends ProviderObserver { @override void didUpdateProvider(ProviderObserverContext context, Object? previousValue, Object? newValue) { print('[${context.provider}] updated: $previousValue → $newValue'); } @override void providerDidFail(ProviderObserverContext context, Object error, StackTrace stackTrace) { // Report to error service } } runApp(ProviderScope(observers: [MyObserver()], child: MyApp()));
dart// Unit test final container = ProviderContainer( overrides: [repositoryProvider.overrideWith((_) => FakeRepository())], ); addTearDown(container.dispose); expect(await container.read(todosProvider.future), isNotEmpty); // Widget test await tester.pumpWidget( ProviderScope( overrides: [repositoryProvider.overrideWith((_) => FakeRepository())], child: const MyApp(), ), );
ProviderContainer or ProviderScope for each test — never share state between tests.container.listen over container.read for autoDispose providers to keep state alive during the test.overrides to inject mocks or fakes.implements or with Mock.ProviderScope.containerOf(tester.element(...)).Other measured skills in the registry, with their headline benchmark lift.