Riverpod を使った Dependency Injection(DI)超入門
Flutter で 「依存関係を外から差し込む」= Dependency Injection を行うと、
- 再利用性 が上がる
- テストしやすく なる
- 実装と実装をつなぐ 結合度 が ぐっと下がる
Riverpod は Provider という箱を通じて 「依存オブジェクトを生成し、必要な場所に安全に渡す」 仕組みを提供します。
ここでは 公式 Riverpod v2 系 を前提に、最小構成 → 実践パターン → テストでの差し替え、まで見てみましょう。
1. 最小サンプル ― ただのクラスを注入する
// 1) 依存クラス(例: API クライアント)
class WeatherApi {
Future<String> fetch() async => '☀️ 25°C';
}
// 2) DI コンテナに当たる provider
final weatherApiProvider = Provider<WeatherApi>((ref) {
return WeatherApi();
});
// 3) 利用側(ConsumerWidget や Consumer)で読み取る
class WeatherText extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final api = ref.read(weatherApiProvider); // 依存を取得
return FutureBuilder(
future: api.fetch(),
builder: (_, snap) => Text(snap.data ?? '...'),
);
}
}
ポイント 🔑
なに? | 役割 |
---|---|
WeatherApi |
依存オブジェクト(サービス) |
Provider |
インスタンスを 1 か所 で生成し、必要な所に渡す橋渡し |
ref.read() |
依存を取得(listen しない) |
2. “依存が依存を持つ” とき ― 階層的 DI
// 設定値を読むリポジトリ
class ConfigRepository {
String get baseUrl => 'https://api.example.com';
}
final configRepoProvider = Provider((_) => ConfigRepository());
// 依存を持つ API
class TodoApi {
TodoApi(this.config);
final ConfigRepository config;
Future<List<String>> fetchTodos() async {
// config.baseUrl を使って呼び出す想定
return ['Take out trash', 'Buy milk'];
}
}
// TodoApi も provider 化。中で依存を要求する
final todoApiProvider = Provider<TodoApi>((ref) {
final config = ref.read(configRepoProvider); // 依存解決
return TodoApi(config);
});
メリット
- どこでも
ref.read(todoApiProvider)
で取得可能 - どの画面から呼んでも 同じインスタンス が使われる(=シングルトン管理)
3. State 管理と合わせる – Notifier
/ AsyncNotifier
API → リポジトリ → ViewModel と階層を刻む場合:
// ↓ API 依存を注入した AsyncNotifier
class TodoListNotifier extends AsyncNotifier<List<String>> {
@override
Future<List<String>> build() async {
final api = ref.read(todoApiProvider); // DI
return api.fetchTodos();
}
Future<void> refresh() async { state = await AsyncValue.guard(build); }
}
final todoListProvider =
AsyncNotifierProvider<TodoListNotifier, List<String>>(TodoListNotifier.new);
使用例:
class TodoScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todoListProvider);
return todos.when(
data: (list) => ListView(children: list.map(Text.new).toList()),
loading: () => const CircularProgressIndicator(),
error: (e, s) => Text('Error: $e'),
);
}
}
4. テストでの差し替え ― ProviderScope
の override
本番環境では 実 API、テストでは スタブ を注入したいとき:
// テスト用ダミー
class FakeWeatherApi implements WeatherApi {
@override
Future<String> fetch() async => '🌧️ 12°C (fake)';
}
void main() {
testWidgets('shows fake weather', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
weatherApiProvider.overrideWithValue(FakeWeatherApi()), // ← 差し替え
],
child: const WeatherText(),
),
);
await tester.pump(); // FutureBuilder の完了待ち
expect(find.text('🌧️ 12°C (fake)'), findsOneWidget);
});
}
ポイント
- overrideWithValue / overrideWithProvider で 好きな実装 に置き換え可能
- テストだけでなく、開発フレーバー(staging / production)を切り替えるのも同じ手法で OK
5. よくある疑問 Q&A
Q | A |
---|---|
ref.read と ref.watch の違いは? |
read は「1 回だけ取得」なので ビルドをトリガーしない。watch は値の変化を監視して Widget を再ビルド。依存注入目的なら基本 read で OK。 |
シングルトンにしたいときは? |
Provider はデフォルトでアプリ生存期間中 1 インスタンス。autoDispose を付けると未使用時に破棄。 |
DI コンテナは Riverpod だけで十分? | ほとんどのケースで可。複雑な遅延ロードやスコープ切替が欲しければ Code generation (riverpod_generator) を併用。 |
まとめ
- 依存オブジェクトを Provider 化 – 生成責任を 1 箇所に閉じ込める
-
必要な場所で
ref.read()
– 明示的に取得し、テストでは override - 構造が深くなっても階層的に DI – Provider はネストが得意
- テスト/フレーバー切替も同じ override で統一
Riverpod を DI コンテナとして “当たり前” に使うことで、
Flutter アプリのテスト性と保守性は 劇的 に向上します。
ぜひ 「まず Provider を切る」 から始めてみてください! 🎉