Claude Code Plugins

Community-maintained marketplace

Feedback

riverpod-state-management

@valeriikot/flutter-cli
0
0

Expert Riverpod state management implementation for Flutter apps. Use when implementing state management with Riverpod, flutter_riverpod, hooks_riverpod, or riverpod_annotation. Covers providers, code generation, dependency injection, async data handling, and modern Riverpod 3.x+ patterns with latest features.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name riverpod-state-management
description Expert Riverpod state management implementation for Flutter apps. Use when implementing state management with Riverpod, flutter_riverpod, hooks_riverpod, or riverpod_annotation. Covers providers, code generation, dependency injection, async data handling, and modern Riverpod 3.x+ patterns with latest features.
allowed-tools Read, Edit, Write, Grep, Glob, Bash

Riverpod State Management Skill

Expert assistance for implementing state management using Riverpod in Flutter applications.

When to Use This Skill

  • Setting up Riverpod in a Flutter project
  • Creating and using providers (Provider, StateProvider, FutureProvider, StreamProvider, etc.)
  • Implementing dependency injection with Riverpod
  • Using Riverpod code generation with riverpod_generator
  • Managing async state and data fetching
  • Testing Riverpod providers
  • Migrating from Provider to Riverpod
  • Implementing complex state logic with StateNotifier or Notifier
  • Using Riverpod with hooks (hooks_riverpod)

Setup

Dependencies

dependencies:
  flutter_riverpod: ^3.0.0
  # OR for hooks support
  hooks_riverpod: ^3.0.0

dev_dependencies:
  riverpod_annotation: ^3.0.0
  build_runner: ^2.4.0
  riverpod_generator: ^3.0.0
  riverpod_lint: ^3.0.0

Project Setup

# Add dependencies
flutter pub add flutter_riverpod
flutter pub add --dev riverpod_annotation build_runner riverpod_generator

# For code generation
flutter pub run build_runner build --delete-conflicting-outputs

# Watch mode for development
flutter pub run build_runner watch --delete-conflicting-outputs

App Configuration

import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(
    const ProviderScope(  // Required root widget
      child: MyApp(),
    ),
  );
}

Core Concepts

Provider Types Overview

Provider Type Use Case Example
Provider Immutable values, DI Configuration, services
StateProvider Simple state (primitive) Counter, toggle
FutureProvider Async data (one-time) API call, file read
StreamProvider Async stream WebSocket, Firebase
StateNotifierProvider Complex state Form, list management
NotifierProvider Modern complex state Recommended for new code
AsyncNotifierProvider Async complex state Data with loading/error

Modern Patterns (Riverpod 3.x+)

Code Generation Approach (Recommended)

import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'counter.g.dart';

// Simple provider
@riverpod
int counter(CounterRef ref) {
  return 0;
}

// Async provider
@riverpod
Future<User> user(UserRef ref, String userId) async {
  final repository = ref.watch(userRepositoryProvider);
  return repository.fetchUser(userId);
}

// Family provider (parameterized)
@riverpod
Future<Post> post(PostRef ref, String postId) async {
  final repository = ref.watch(postRepositoryProvider);
  return repository.fetchPost(postId);
}

// Stateful provider (Notifier)
@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0;

  void increment() => state++;
  void decrement() => state--;
  void reset() => state = 0;
}

// Async Notifier
@riverpod
class UserList extends _$UserList {
  @override
  Future<List<User>> build() async {
    return _fetchUsers();
  }

  Future<void> addUser(User user) async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
      final users = await _fetchUsers();
      return [...users, user];
    });
  }

  Future<List<User>> _fetchUsers() async {
    final repository = ref.read(userRepositoryProvider);
    return repository.fetchUsers();
  }
}

Manual Approach (Legacy but still valid)

// Simple provider
final counterProvider = Provider<int>((ref) => 0);

// State provider
final counterStateProvider = StateProvider<int>((ref) => 0);

// Future provider
final userProvider = FutureProvider.family<User, String>((ref, userId) async {
  final repository = ref.watch(userRepositoryProvider);
  return repository.fetchUser(userId);
});

// StateNotifier
class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);

  void increment() => state++;
  void decrement() => state--;
}

final counterNotifierProvider = StateNotifierProvider<CounterNotifier, int>(
  (ref) => CounterNotifier(),
);

// Notifier (Modern)
class Counter extends Notifier<int> {
  @override
  int build() => 0;

  void increment() => state++;
  void decrement() => state--;
}

final counterNotifierProvider = NotifierProvider<Counter, int>(Counter.new);

Consuming Providers

In Widgets

// ConsumerWidget (recommended)
class CounterWidget extends ConsumerWidget {
  const CounterWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);

    return Column(
      children: [
        Text('Count: $count'),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).increment(),
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

// Consumer (for part of widget tree)
class MyWidget extends StatelessWidget {
  const MyWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, ref, child) {
        final count = ref.watch(counterProvider);
        return Text('Count: $count');
      },
    );
  }
}

// ConsumerStatefulWidget
class CounterPage extends ConsumerStatefulWidget {
  const CounterPage({super.key});

  @override
  ConsumerState<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends ConsumerState<CounterPage> {
  @override
  void initState() {
    super.initState();
    // Can use ref here
    ref.read(counterProvider);
  }

  @override
  Widget build(BuildContext context) {
    final count = ref.watch(counterProvider);
    return Text('Count: $count');
  }
}

Ref Methods

// Watch: Rebuilds when value changes
final value = ref.watch(provider);

// Read: One-time read, no rebuild
final value = ref.read(provider);

// Listen: Execute side effects on change
ref.listen(provider, (previous, next) {
  if (next.hasError) {
    showErrorSnackBar(context, next.error);
  }
});

// Invalidate: Force provider refresh
ref.invalidate(provider);

// Refresh: Get new value immediately
final newValue = ref.refresh(provider);

Async Data Handling

AsyncValue Pattern

@riverpod
class TodoList extends _$TodoList {
  @override
  Future<List<Todo>> build() async {
    return _fetchTodos();
  }

  Future<List<Todo>> _fetchTodos() async {
    final repository = ref.read(todoRepositoryProvider);
    return repository.fetchTodos();
  }
}

// In widget
class TodoListWidget extends ConsumerWidget {
  const TodoListWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncTodos = ref.watch(todoListProvider);

    return asyncTodos.when(
      data: (todos) => ListView.builder(
        itemCount: todos.length,
        itemBuilder: (context, index) => TodoItem(todo: todos[index]),
      ),
      loading: () => const CircularProgressIndicator(),
      error: (error, stack) => Text('Error: $error'),
    );
  }
}

// Alternative pattern matching
asyncTodos.when(
  data: (todos) => /* success */,
  loading: () => /* loading */,
  error: (error, stack) => /* error */,
);

// Map for custom handling
asyncTodos.maybeWhen(
  data: (todos) => /* success */,
  orElse: () => const SizedBox(),
);

Manual State Updates

@riverpod
class TodoList extends _$TodoList {
  @override
  Future<List<Todo>> build() async {
    return _fetchTodos();
  }

  Future<void> addTodo(Todo todo) async {
    // Set loading
    state = const AsyncValue.loading();

    // Use AsyncValue.guard to handle errors
    state = await AsyncValue.guard(() async {
      final repository = ref.read(todoRepositoryProvider);
      await repository.addTodo(todo);
      return _fetchTodos();
    });
  }

  // Or handle manually
  Future<void> removeTodo(String id) async {
    final previousState = state;

    state = const AsyncValue.loading();

    try {
      final repository = ref.read(todoRepositoryProvider);
      await repository.removeTodo(id);
      state = AsyncValue.data(await _fetchTodos());
    } catch (error, stackTrace) {
      state = AsyncValue.error(error, stackTrace);
      // Optionally restore previous state
      // state = previousState;
    }
  }

  Future<List<Todo>> _fetchTodos() async {
    final repository = ref.read(todoRepositoryProvider);
    return repository.fetchTodos();
  }
}

Dependency Injection

// Repository provider
@riverpod
TodoRepository todoRepository(TodoRepositoryRef ref) {
  final client = ref.watch(httpClientProvider);
  return TodoRepository(client);
}

// Service provider depending on repository
@riverpod
class TodoService extends _$TodoService {
  @override
  void build() {}

  Future<void> syncTodos() async {
    final repository = ref.read(todoRepositoryProvider);
    await repository.sync();
    ref.invalidate(todoListProvider);
  }
}

// Using in widget
class SyncButton extends ConsumerWidget {
  const SyncButton({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ElevatedButton(
      onPressed: () => ref.read(todoServiceProvider).syncTodos(),
      child: const Text('Sync'),
    );
  }
}

Advanced Patterns

Combining Providers

@riverpod
Future<UserProfile> userProfile(UserProfileRef ref, String userId) async {
  final user = await ref.watch(userProvider(userId).future);
  final settings = await ref.watch(userSettingsProvider(userId).future);

  return UserProfile(
    user: user,
    settings: settings,
  );
}

// Select specific parts
@riverpod
String userName(UserNameRef ref, String userId) {
  return ref.watch(
    userProvider(userId).select((user) => user.name),
  );
}

Provider Scope Override

// For testing or feature-specific overrides
void main() {
  runApp(
    ProviderScope(
      overrides: [
        todoRepositoryProvider.overrideWith(
          (ref) => MockTodoRepository(),
        ),
      ],
      child: const MyApp(),
    ),
  );
}

Auto-dispose

// Automatically disposed when no longer watched
@riverpod
Future<Data> data(DataRef ref) async {
  // Auto-disposed by default with code generation

  final link = ref.keepAlive();  // Keep alive

  // Cancel after 30 seconds
  Timer(const Duration(seconds: 30), link.close);

  return fetchData();
}

// Manual approach
final dataProvider = FutureProvider.autoDispose<Data>((ref) async {
  return fetchData();
});

Family Providers (Parameterized)

// Code generation (automatic)
@riverpod
Future<User> user(UserRef ref, String userId) async {
  return fetchUser(userId);
}

// Manual
final userProvider = FutureProvider.family<User, String>((ref, userId) async {
  return fetchUser(userId);
});

// In widget
final user = ref.watch(userProvider('123'));

Testing

import 'package:flutter_test/flutter_test.dart';
import 'package:riverpod/riverpod.dart';

void main() {
  test('Counter increments', () {
    final container = ProviderContainer();
    addTearDown(container.dispose);

    // Initial value
    expect(container.read(counterProvider), 0);

    // Increment
    container.read(counterProvider.notifier).increment();

    // Check new value
    expect(container.read(counterProvider), 1);
  });

  test('Async provider test', () async {
    final container = ProviderContainer(
      overrides: [
        userRepositoryProvider.overrideWith(
          (ref) => MockUserRepository(),
        ),
      ],
    );
    addTearDown(container.dispose);

    final user = await container.read(userProvider('123').future);

    expect(user.id, '123');
  });

  test('Listen to changes', () async {
    final container = ProviderContainer();
    addTearDown(container.dispose);

    final listener = Listener<int>();

    container.listen(
      counterProvider,
      listener.call,
      fireImmediately: true,
    );

    verify(listener.call(null, 0));

    container.read(counterProvider.notifier).increment();

    verify(listener.call(0, 1));
  });
}

class Listener<T> extends Mock {
  void call(T? previous, T next);
}

Project Structure

lib/
├── core/
│   ├── providers/         # Core providers (DI, services)
│   └── utils/
├── features/
│   └── todos/
│       ├── data/
│       │   ├── models/
│       │   ├── repositories/
│       │   └── providers/  # Data providers
│       ├── domain/
│       │   └── entities/
│       └── presentation/
│           ├── providers/  # State providers
│           ├── pages/
│           └── widgets/
└── main.dart

Common Patterns

Repository Pattern

@riverpod
TodoRepository todoRepository(TodoRepositoryRef ref) {
  return TodoRepositoryImpl();
}

@riverpod
class TodoList extends _$TodoList {
  @override
  Future<List<Todo>> build() {
    final repository = ref.watch(todoRepositoryProvider);
    return repository.fetchTodos();
  }

  Future<void> addTodo(String title) async {
    final repository = ref.read(todoRepositoryProvider);
    await repository.addTodo(title);
    ref.invalidateSelf();  // Refresh data
  }
}

Form State

@riverpod
class LoginForm extends _$LoginForm {
  @override
  LoginFormState build() {
    return const LoginFormState();
  }

  void updateEmail(String email) {
    state = state.copyWith(email: email);
  }

  void updatePassword(String password) {
    state = state.copyWith(password: password);
  }

  Future<void> submit() async {
    if (!state.isValid) return;

    state = state.copyWith(isLoading: true);

    try {
      final auth = ref.read(authServiceProvider);
      await auth.login(state.email, state.password);
      state = state.copyWith(isLoading: false, isSuccess: true);
    } catch (e) {
      state = state.copyWith(
        isLoading: false,
        error: e.toString(),
      );
    }
  }
}

@freezed
class LoginFormState with _$LoginFormState {
  const factory LoginFormState({
    @Default('') String email,
    @Default('') String password,
    @Default(false) bool isLoading,
    @Default(false) bool isSuccess,
    String? error,
  }) = _LoginFormState;

  const LoginFormState._();

  bool get isValid => email.isNotEmpty && password.length >= 6;
}

Migration from Provider

// Old (Provider)
class CounterProvider extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

final counterProvider = ChangeNotifierProvider((ref) => CounterProvider());

// New (Riverpod)
@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0;

  void increment() => state++;
}

Best Practices

  • Use code generation for new projects (cleaner syntax, less boilerplate)
  • Prefer Notifier over StateNotifier for new code
  • Use ConsumerWidget instead of StatelessWidget for reactive UI
  • Keep providers small and focused (single responsibility)
  • Use ref.listen for side effects (navigation, snackbars)
  • Use AsyncValue.guard for error handling in async operations
  • Override providers in ProviderScope for testing
  • Use family providers for parameterized providers
  • Leverage auto-dispose to prevent memory leaks
  • Use select to optimize rebuilds (watch only specific fields)
  • Invalidate providers to refresh data instead of manual updates
  • Separate business logic from UI (providers should be UI-agnostic)

Common Issues

Issue: Provider not found

Solution: Ensure ProviderScope wraps your app

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

Issue: Cannot read provider during build

Solution: Use ref.watch instead of ref.read

// Wrong
final value = ref.read(provider);

// Correct
final value = ref.watch(provider);

Issue: Provider not updating

Solution: Check if you're reading instead of watching

// Use watch for reactive updates
final value = ref.watch(provider);

Resources

Notes

This skill focuses on Riverpod 3.x+ patterns with emphasis on code generation and latest features. For Flutter-specific UI patterns, use the flutter-developer skill. For alternative state management, see the bloc-state-management skill.