Riverpod を使った Dependency Injection（DI）紹介

### Riverpod を使った Dependency Injection（DI）超入門

Flutter で **「依存関係を外から差し込む」＝ Dependency Injection** を行うと、  
- **再利用性** が上がる  
- **テストしやすく** なる  
- 実装と実装をつなぐ **結合度** が ぐっと下がる  

Riverpod は *Provider* という箱を通じて **「依存オブジェクトを生成し、必要な場所に安全に渡す」** 仕組みを提供します。  
ここでは **公式 Riverpod v2 系** を前提に、最小構成 → 実践パターン → テストでの差し替え、まで見てみましょう。

---

## 1. 最小サンプル ― ただのクラスを注入する

```dart
// 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

```dart
// 設定値を読むリポジトリ
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 と階層を刻む場合:

```dart
// ↓ 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);
```

使用例:

```dart
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**、テストでは **スタブ** を注入したいとき:

```dart
// テスト用ダミー
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)` を併用。 |

---

## まとめ

1. **依存オブジェクトを Provider 化** – 生成責任を 1 箇所に閉じ込める  
2. **必要な場所で `ref.read()`** – 明示的に取得し、テストでは override  
3. **構造が深くなっても階層的に DI** – Provider はネストが得意  
4. **テスト／フレーバー切替も同じ override で統一**  

> Riverpod を DI コンテナとして “当たり前” に使うことで、  
> Flutter アプリのテスト性と保守性は **劇的** に向上します。  
> ぜひ **「まず Provider を切る」** から始めてみてください！ 🎉