riverpod について
Flutter と Dart アプリの状態管理に取り組む開発者向けの Riverpod ガイドです。Provider の設計、ライフサイクル、非同期状態、テスト方法を扱います。
主な機能
- 01Provider のセットアップと定義
- 02Ref の使用と Provider の組み合わせ
- 03ライフサイクルと破棄の指針
- 04非同期状態と副作用のパターン
- 05ユニット・ウィジェットテスト
活用シーン
- 01Provider によるアプリ状態の構成
- 02非同期データとエラーの管理
- 03Provider のライフサイクル漏れの防止
- 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
コメント
Markdown 対応