mcpField
R
riverpod

riverpod

github.com/evanca開発663284 回インストール3 週間前に更新
リポジトリ

riverpod について

Flutter と Dart アプリの状態管理に取り組む開発者向けの Riverpod ガイドです。Provider の設計、ライフサイクル、非同期状態、テスト方法を扱います。

主な機能

  1. 01Provider のセットアップと定義
  2. 02Ref の使用と Provider の組み合わせ
  3. 03ライフサイクルと破棄の指針
  4. 04非同期状態と副作用のパターン
  5. 05ユニット・ウィジェットテスト

活用シーン

  1. 01Provider によるアプリ状態の構成
  2. 02非同期データとエラーの管理
  3. 03Provider のライフサイクル漏れの防止
  4. 04Provider とウィジェットのテスト

インストール

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

$npx skills add https://github.com/evanca/flutter-ai-rules --skill riverpod

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

詳細

Riverpod スキル

このスキルでは、Flutter および Dart アプリケーションで状態管理に Riverpod を正しく使用する方法を定義します。


1. セットアップ

void main() {
  runApp(const ProviderScope(child: MyApp()));
}
  • runApp 内で、アプリを直接 ProviderScope でラップします。MyApp 内でラップしないでください。
  • riverpod_lint をインストールして使用し、IDE のリファクタリングを有効にしてベストプラクティスを適用します。

2. プロバイダーの定義

// 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 変数として定義します。
  • 戻り値の型に応じて、Provider、FutureProvider、または StreamProvider を使用します。
  • プロバイダーにアクセスする場合は、StatelessWidget/StatefulWidget ではなく ConsumerWidget または ConsumerStatefulWidget を使用します。

3. Ref の使用

メソッド用途
ref.watchリアクティブに監視します。値が変わると再ビルドされます。ビルドフェーズでのみ使用します。
ref.read一度だけアクセスします。コールバック/Notifier のメソッド内で使用し、ビルド内では使用しません。
ref.listen命令型のサブスクリプションです。可能な場合は ref.watch を優先します。
ref.onDisposeプロバイダーの状態が破棄される際にクリーンアップします。
// 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;
});
  • コールバック、リスナー、Notifier のメソッド内で ref.watch を絶対に呼び出さないでください。
  • UI から Notifier のメソッドを呼び出すには、ref.read(yourNotifierProvider.notifier).method() を使用します。
  • 非同期コールバックで await の後に ref を使用する前に、context.mounted を確認します。

4. プロバイダーの組み合わせ

@riverpod
Future<String> userGreeting(Ref ref) async {
  final user = await ref.watch(userProvider.future);
  return 'Hello, ${user.name}!';
}
  • 非同期プロバイダーの解決済みの値を待つには、ref.watch(asyncProvider.future) を使用します。
  • プロバイダーは一度だけ実行され、結果をキャッシュします。同じプロバイダーを監視する複数のウィジェットは、1つの計算結果を共有します。

5. 引数の受け渡し(Families)

@riverpod
Future<Todo> todo(Ref ref, String id) async {
  return ref.watch(repositoryProvider).fetchTodo(id);
}

// Usage
final todo = ref.watch(todoProvider('some-id'));
  • メモリーリークを防ぐため、パラメーター付きプロバイダーでは常に autoDispose を有効にします。
  • 複数のパラメーターには Dart 3 records またはコード生成を使用します。これらは == を自然にオーバーライドします。
  • 通常の List や Map をパラメーターとして渡すのは避けてください(== がオーバーライドされません)。代わりに const コレクション、レコード、または適切な等価性を持つクラスを使用します。
  • 等価性に関する誤りを検出するには、riverpod_lint の provider_parameters リントルールを使用します。

6. 自動破棄と状態のライフサイクル

  • コード生成を使用する場合、監視されなくなると状態はデフォルトで破棄されます。keepAlive: true で破棄を無効にできます。
  • コード生成を使用しない場合、状態はデフォルトで保持されます。破棄を有効にするには .autoDispose を使用します。
  • プロバイダーが再計算されると、状態は常に破棄されます。
// keepAlive with timer
ref.onCancel(() {
  final link = ref.keepAlive();
  Timer(const Duration(minutes: 5), link.close);
});
  • クリーンアップには ref.onDispose を使用します。その中で副作用を起こしたり、プロバイダーを変更したりしないでください。
  • 強制的に破棄するには ref.invalidate(provider) を、プロバイダー内からは ref.invalidateSelf() を使用します。
  • 無効化して新しい値をすぐに読み取るには ref.refresh(provider) を使用し、必ず戻り値を使います。

7. 即時初期化

プロバイダーはデフォルトで遅延初期化されます。即時に初期化するには、次のようにします。

// In MyApp or a dedicated widget under ProviderScope:
Consumer(
  builder: (context, ref, _) {
    ref.watch(myEagerProvider); // forces initialization
    return const MyApp();
  },
)
  • テスト時の動作を一貫させるため、即時初期化は main() ではなく、公開ウィジェットに配置します。
  • データを直接読み取り、準備ができていない場合に明確な例外をスローするには、AsyncValue.requireValue を使用します。

8. 副作用の実行

@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.watch ではなく ref.read を使用します。
  • 副作用の後は、状態を直接設定する、ref.invalidateSelf() を呼び出す、またはキャッシュを手動で更新することで状態を更新します。
  • UI では常にローディング状態とエラー状態を処理します。
  • プロバイダーのコンストラクターやビルドメソッド内で副作用を実行しないでください。

9. プロバイダーオブザーバー

class 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()));

10. テスト

// 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 または ProviderScope を作成します。テスト間で状態を共有しないでください。
  • autoDispose プロバイダーの状態をテスト中に保持するには、container.read ではなく container.listen を使用します。
  • モックやフェイクを注入するには overrides を使用します。
  • Notifier を直接ではなく、依存関係(リポジトリー)をモックすることを優先します。
  • Notifier をモックする必要がある場合は、implements や with Mock を使用せず、元のクラスをサブクラス化します。
  • コード生成を使用する場合、Notifier のモックは Notifier と同じファイルに配置します。
  • ウィジェットテストでコンテナーを取得するには、ProviderScope.containerOf(tester.element(...)) を使用します。

References

コメント