🎉

Riverpod を使った Dependency Injection(DI)超入門

に公開

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 {
  
  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>> {
  
  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 {
  
  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 {
  
  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.readref.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 を切る」 から始めてみてください! 🎉

Discussion