mcpField
F
flutter-riverpod-expert

flutter-riverpod-expert

github.com/juparave開発11.1k 回インストール5 日前に更新
リポジトリ

flutter-riverpod-expert について

Riverpod を使う Flutter 開発者向けの実践ガイドスキルです。Provider の選択、コード生成、非同期状態、パフォーマンス、設計、テストを扱い、2025年の指針を中心に Riverpod 2.x からの移行にも触れます。

主な機能

  1. 01Provider 選択の指針
  2. 02コード生成の設定
  3. 03非同期状態と変更処理
  4. 04select() による再ビルド最適化
  5. 05Repository と依存関係のパターン
  6. 06Provider と Widget のテスト

活用シーン

  1. 01状態要件に応じた Provider の選択
  2. 02非同期データと変更処理の設計
  3. 03不要な Widget 再ビルドの削減
  4. 04Riverpod 依存の UI と Provider のテスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/juparave/dotfiles --skill flutter-riverpod-expert

インストール後、Claude Code で/flutter-riverpod-expertを入力して読み込まれたか確認します。

詳細

Flutter Riverpod エキスパート — 2025 年のベストプラクティス

2025 年のベストプラクティスに沿った、Flutter Riverpod の状態管理に関する専門知識を備えています。ユーザーが Riverpod または Flutter の状態管理を使用している場合は、これらのパターンとガイドラインを適用してください。

このスキルを使うタイミング

次の内容にユーザーが言及した場合は、この専門知識を有効にしてください。

  • Riverpod、プロバイダー、状態管理、または StateNotifier
  • AsyncNotifier、FutureProvider、StreamProvider、NotifierProvider
  • riverpod_generator または build_runner を使用したコード生成
  • データ取得、API 統合、ミューテーション、またはリアクティブな状態
  • 状態の同期、キャッシュ、autoDispose、またはメモリ管理
  • プロバイダーのテスト、依存性注入、またはリポジトリパターン
  • 再ビルド、プロバイダーの選択、または最適化に関するパフォーマンスの問題
  • 古い Riverpod パターンから最新のアプローチへの移行

基本原則(2025 年)

  1. コード生成を強く推奨します — @riverpod アノテーションと riverpod_generator を使用します
  2. 非同期状態には AsyncNotifierProvider を推奨します(一貫性を高めるため FutureProvider/StreamProvider を置き換えます)
  3. デフォルトで AutoDispose — コード生成ではプロバイダーが自動的に auto-dispose になります
  4. リポジトリパターン — データレイヤーと状態管理を分離します
  5. パフォーマンスを最優先 — select() を使用して再ビルドを最適化します

プロバイダー選択ガイド

クイック判断ツリー

不変値/計算値 — Provider を使用します。

@riverpod
String apiKey(Ref ref) => 'YOUR_API_KEY';

@riverpod
int totalPrice(Ref ref) {
  final cart = ref.watch(cartProvider);
  return cart.items.fold(0, (sum, item) => sum + item.price);
}

単純な同期状態 — NotifierProvider を使用します。

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

  void increment() => state++;
  void decrement() => state = max(0, state - 1);
}

ミューテーションを伴う非同期データ(2025 年の推奨) — AsyncNotifierProvider を使用します。

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

  Future<void> addTodo(String title) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      final repo = ref.read(todoRepositoryProvider);
      await repo.createTodo(title);
      return repo.fetchTodos();
    });
  }

  Future<void> deleteTodo(String id) async {
    // Optimistic update
    state = AsyncData(state.value!.where((t) => t.id != id).toList());

    try {
      await ref.read(todoRepositoryProvider).deleteTodo(id);
    } catch (e) {
      ref.invalidateSelf(); // Rollback on error
    }
  }
}

リアルタイムストリームのみ — StreamProvider を使用します。

@riverpod
Stream<User?> authState(Ref ref) {
  return FirebaseAuth.instance.authStateChanges();
}

重要なルール:一貫性とミューテーションのサポートを高めるため、FutureProvider/StreamProvider より AsyncNotifierProvider を優先してください。

コード生成のセットアップ

依存関係(pubspec.yaml)

Riverpod 3.x(現行、2025 年半ば以降の時点):

dependencies:
  flutter_riverpod: ^3.0.0
  riverpod_annotation: ^4.0.0

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

注:riverpod_lint 3.x は、単独の custom_lint パッケージではなく、analyzer_plugin に直接依存しています。custom_lint を pubspec に追加 しないでください。riverpod_lint 3.1.8 以降との依存関係の解決に失敗します。代わりに、analysis_options.yaml でプラグインを登録してください。

analyzer:
  plugins:
    - riverpod_lint

Riverpod 2.x(レガシー):

dependencies:
  flutter_riverpod: ^2.5.0
  riverpod_annotation: ^2.3.0

dev_dependencies:
  build_runner: ^2.4.0
  riverpod_generator: ^2.4.0
  custom_lint: ^0.6.0
  riverpod_lint: ^2.3.0

この場合は、別個の依存関係として custom_lint が必要で、analysis_options.yaml は次のようになります。

analyzer:
  plugins:
    - custom_lint
ファイルテンプレート

すべてのプロバイダーファイルに必要です。

import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'filename.g.dart';  // REQUIRED

@riverpod
class MyProvider extends _$MyProvider {
  @override
  Future<Data> build() async => fetchData();
}
ジェネレーターを実行する
# Watch mode (RECOMMENDED during development)
dart run build_runner watch -d

# One-time generation
dart run build_runner build --delete-conflicting-outputs

パフォーマンス最適化パターン

特定のフィールドには ref.select() を使用する
// ❌ BAD: Rebuilds on ANY product change
final product = ref.watch(productProvider);
return Text('\$${product.price}');

// ✅ GOOD: Only rebuilds when price changes
final price = ref.watch(productProvider.select((p) => p.price));
return Text('\$$price');
ref.watch() と ref.select()、ref.read()、ref.listen() の比較

ref.watch() — 変更を購読します(build 内で使用します)。

@override
Widget build(BuildContext context, WidgetRef ref) {
  final todos = ref.watch(todoListProvider);
  return ListView(...);
}

ref.select() — 特定のプロパティを購読します(再ビルドを最適化します)。

final count = ref.watch(todoListProvider.select((todos) => todos.length));
final isAdult = ref.watch(personProvider.select((p) => p.age >= 18));

ref.read() — 購読せずに一度だけ読み取ります(イベントハンドラー内のみ)。

onPressed: () {
  ref.read(todoListProvider.notifier).addTodo('New task');
}

// ❌ NEVER use read() in build to "optimize" - it won't rebuild!

ref.listen() — 副作用(ナビゲーション、スナックバー、ログ記録)に使用します。

ref.listen<AsyncValue<List<Todo>>>(
  todoListProvider,
  (previous, next) {
    next.whenOrNull(
      error: (error, stack) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error: $error')),
        );
      },
    );
  },
);
ループ内での監視を避ける
// ❌ BAD: Causes performance issues
ListView.builder(
  itemBuilder: (context, index) {
    final todo = ref.watch(todoProvider(ids[index])); // DON'T!
    return ListTile(...);
  },
);

// ✅ GOOD: Separate widget for each item
class TodoItem extends ConsumerWidget {
  const TodoItem({required this.todoId});
  final String todoId;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todo = ref.watch(todoProvider(todoId));
    return ListTile(title: Text(todo.title));
  }
}

class TodoList extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final ids = ref.watch(todoIdsProvider);
    return ListView.builder(
      itemCount: ids.length,
      itemBuilder: (context, index) => TodoItem(todoId: ids[index]),
    );
  }
}
派生プロバイダーを作成する
// ✅ GOOD: Separate provider for computed state
@riverpod
List<Todo> filteredSortedTodos(Ref ref) {
  final todos = ref.watch(todoListProvider);
  final filter = ref.watch(filterProvider);
  final sortOrder = ref.watch(sortOrderProvider);

  final filtered = todos.where((t) => t.matches(filter)).toList();
  return filtered..sort(sortOrder.comparator);
}

リポジトリパターンのアーキテクチャ

3 層アーキテクチャ

1. データレイヤー — リポジトリ:

@riverpod
TodoRepository todoRepository(Ref ref) {
  return TodoRepository(dio: ref.watch(dioProvider));
}

class TodoRepository {
  TodoRepository({required this.dio});
  final Dio dio;

  Future<List<Todo>> fetchTodos() async {
    final response = await dio.get('/todos');
    return (response.data as List)
      .map((json) => Todo.fromJson(json))
      .toList();
  }

  Future<Todo> createTodo(String title) async {
    final response = await dio.post('/todos', data: {'title': title});
    return Todo.fromJson(response.data);
  }

  Future<void> deleteTodo(String id) async {
    await dio.delete('/todos/$id');
  }
}

2. アプリケーションレイヤー — 状態管理:

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

  Future<void> addTodo(String title) async {
    final repository = ref.read(todoRepositoryProvider);

    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      await repository.createTodo(title);
      return repository.fetchTodos();
    });
  }
}

3. プレゼンテーションレイヤー — UI:

class TodoListScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todosAsync = ref.watch(todoListProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Todos')),
      body: todosAsync.when(
        data: (todos) => ListView.builder(
          itemCount: todos.length,
          itemBuilder: (context, index) => TodoTile(todos[index]),
        ),
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (error, stack) => ErrorView(error: error),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddDialog(context, ref),
        child: const Icon(Icons.add),
      ),
    );
  }
}
依存性注入
// Services
@riverpod
Dio dio(Ref ref) {
  final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
  dio.interceptors.add(LogInterceptor());
  return dio;
}

@riverpod
Future<SharedPreferences> sharedPreferences(Ref ref) async {
  return await SharedPreferences.getInstance();
}

@riverpod
AuthService authService(Ref ref) {
  return AuthService(
    dio: ref.watch(dioProvider),
    storage: ref.watch(sharedPreferencesProvider).value!,
  );
}

// Repositories depend on services
@riverpod
UserRepository userRepository(Ref ref) {
  return UserRepository(
    dio: ref.watch(dioProvider),
    authService: ref.watch(authServiceProvider),
  );
}

// State providers depend on repositories
@riverpod
class CurrentUser extends _$CurrentUser {
  @override
  Future<User?> build() async {
    final authService = ref.watch(authServiceProvider);
    final userId = await authService.getCurrentUserId();

    if (userId == null) return null;

    final repository = ref.watch(userRepositoryProvider);
    return repository.fetchUser(userId);
  }

  Future<void> logout() async {
    final authService = ref.read(authServiceProvider);
    await authService.logout();
    ref.invalidateSelf();
  }
}

Family プロバイダー(パラメーター付き)

パラメーターを追加すると、コード生成で Family プロバイダーが自動的に作成されます。

// Simple family provider
@riverpod
Future<User> user(Ref ref, String id) async {
  final dio = ref.watch(dioProvider);
  final response = await dio.get('/users/$id');
  return User.fromJson(response.data);
}

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

// Family with AsyncNotifier
@riverpod
class UserNotifier extends _$UserNotifier {
  @override
  Future<User> build(String id) async {
    final repo = ref.watch(userRepositoryProvider);
    return repo.fetchUser(id);
  }

  Future<void> updateName(String newName) async {
    final userId = arg; // Access the parameter via 'arg'
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      await ref.read(userRepositoryProvider).updateUser(userId, name: newName);
      return ref.read(userRepositoryProvider).fetchUser(userId);
    });
  }
}

// Complex parameters need proper equality
class UserFilter {
  const UserFilter({required this.role, required this.active});
  final String role;
  final bool active;

  @override
  bool operator ==(Object other) =>
    identical(this, other) ||
    other is UserFilter &&
    role == other.role &&
    active == other.active;

  @override
  int get hashCode => Object.hash(role, active);
}

@riverpod
Future<List<User>> filteredUsers(Ref ref, UserFilter filter) async {
  return fetchUsers(filter);
}

AutoDispose とキャッシュ

コード生成では、プロバイダーがデフォルトで auto-dispose になります。

// Default: auto-dispose when no listeners
@riverpod
Future<String> data(Ref ref) async => fetchData();

// Keep alive permanently
@Riverpod(keepAlive: true)
Future<Config> config(Ref ref) async => loadConfig();

// Conditional keep alive - cache on success
@riverpod
Future<String> cachedData(Ref ref) async {
  final data = await fetchData();
  ref.keepAlive(); // Cache this result forever
  return data;
}

// Timed cache (5 minutes)
@riverpod
Future<String> timedCache(Ref ref) async {
  final data = await fetchData();
  final link = ref.keepAlive();
  Timer(const Duration(minutes: 5), link.close);
  return data;
}

// Manual disposal - cleanup resources
@riverpod
Stream<int> websocket(Ref ref) {
  final client = WebSocketClient();

  ref.onDispose(() {
    client.close(); // Cleanup when provider is disposed
  });

  return client.stream;
}

エラー処理

包括的なエラー処理
@riverpod
class TodoList extends _$TodoList {
  @override
  Future<List<Todo>> build() async {
    try {
      final repository = ref.watch(todoRepositoryProvider);
      return await repository.fetchTodos();
    } on DioException catch (e) {
      if (e.response?.statusCode == 401) {
        ref.read(authServiceProvider).logout();
        throw UnauthorizedException();
      }
      throw NetworkException(e.message);
    } catch (e) {
      throw UnexpectedException(e.toString());
    }
  }

  Future<void> addTodo(String title) async {
    state = const AsyncLoading();

    state = await AsyncValue.guard(() async {
      final repository = ref.read(todoRepositoryProvider);
      await repository.createTodo(title);
      return repository.fetchTodos();
    });
  }
}
UI のエラー処理
// Using .when()
todosAsync.when(
  data: (todos) => ListView.builder(...),
  loading: () => const Center(child: CircularProgressIndicator()),
  error: (error, stack) {
    if (error is NetworkException) {
      return ErrorView(
        message: 'Network error. Check your connection.',
        onRetry: () => ref.invalidate(todoListProvider),
      );
    }
    if (error is UnauthorizedException) {
      return const ErrorView(message: 'Please log in again.');
    }
    return ErrorView(message: 'Error: $error');
  },
);

// Listen for errors (side effects)
ref.listen<AsyncValue<List<Todo>>>(
  todoListProvider,
  (previous, next) {
    next.whenOrNull(
      error: (error, stack) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(error.toString()),
            backgroundColor: Colors.red,
          ),
        );
      },
    );
  },
);

テスト

プロバイダーのテスト
test('TodoList fetches todos correctly', () async {
  final container = ProviderContainer.test(
    overrides: [
      todoRepositoryProvider.overrideWithValue(MockTodoRepository()),
    ],
  );

  final todos = await container.read(todoListProvider.future);

  expect(todos.length, 2);
  expect(todos[0].title, 'Test Todo 1');
});

test('TodoList adds todo correctly', () async {
  final mockRepo = MockTodoRepository();
  final container = ProviderContainer.test(
    overrides: [
      todoRepositoryProvider.overrideWithValue(mockRepo),
    ],
  );

  await container.read(todoListProvider.notifier).addTodo('New Todo');

  verify(() => mockRepo.createTodo('New Todo')).called(1);
});
ウィジェットのテスト
testWidgets('TodoListScreen displays todos', (tester) async {
  await tester.pumpWidget(
    ProviderScope(
      overrides: [
        todoRepositoryProvider.overrideWithValue(MockTodoRepository()),
      ],
      child: const MaterialApp(home: TodoListScreen()),
    ),
  );

  await tester.pumpAndSettle();

  expect(find.text('Test Todo 1'), findsOneWidget);
  expect(find.text('Test Todo 2'), findsOneWidget);
});

よくあるアンチパターン(避けること)

パフォーマンス上の落とし穴
// ❌ Using ref.read() to avoid rebuilds
final todos = ref.read(todoListProvider); // Won't rebuild!

// ✅ Use ref.watch() or ref.select()
final count = ref.watch(todoListProvider.select((todos) => todos.length));
メモリーリーク
// ❌ Not disposing resources
@riverpod
Stream<int> badWebsocket(Ref ref) {
  final client = WebSocketClient();
  return client.stream; // Never closed!
}

// ✅ Dispose resources
@riverpod
Stream<int> goodWebsocket(Ref ref) {
  final client = WebSocketClient();
  ref.onDispose(() => client.close());
  return client.stream;
}
複数の信頼できる情報源
// ❌ BAD: Which is the source of truth?
class BadWidget extends StatefulWidget {
  int localCount = 0; // Local state

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final providerCount = ref.watch(counterProvider); // Provider state
    return Text('$localCount vs $providerCount'); // Confusing!
  }
}

// ✅ GOOD: Single source of truth
class GoodWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Text('$count');
  }
}
依存するプロバイダーを無効化しない
// ❌ BAD
Future<void> logout() async {
  state = null;
  // Other providers still have old user data!
}

// ✅ GOOD: Invalidate dependent providers
Future<void> logout() async {
  state = null;
  ref.invalidate(userProfileProvider);
  ref.invalidate(userSettingsProvider);
  ref.invalidate(userNotificationsProvider);
}

// ✅ EVEN BETTER: Make providers watch auth
@riverpod
Future<UserProfile> userProfile(Ref ref) async {
  final user = ref.watch(authProvider);
  if (user == null) throw UnauthenticatedException();
  return fetchUserProfile(user.id); // Auto-refetches when user changes
}

使用方法

ユーザーが Riverpod を使用している場合:

  1. コード生成を常に推奨します。@riverpod アノテーションを使用してください
  2. 非同期状態には、FutureProvider/StreamProvider よりも AsyncNotifierProvider を優先してください
  3. 特定のフィールドを監視する場合は、select() を提案してパフォーマンスを最適化してください
  4. クリーンアーキテクチャのために、リポジトリパターンに従ってください
  5. AsyncValue.guard() と .when() を使用して、適切にエラー処理を行ってください
  6. autoDispose とキャッシュ戦略について注意を促してください
  7. ユーザーのコードにアンチパターンがあれば指摘してください
  8. 適切なインポートを含む、完全で動作する例を提示してください

参考資料

詳細や高度なパターンについては、以下を参照してください: /Users/pablito/EVOworkspace/flutter/CesarferPromotoresFlutter/promotores/RIVERPOD_2025_BEST_PRACTICES.md

コメント