【Flutter】RiverpodのDI機能を用いてレイヤードアーキテクチャを構築してみた

Flutter大学 の 共同勉強会に登壇しました!

本記事のテーマ

RiverpodのDI機能を用いてレイヤードアーキテクチャを構築してみた

で登壇した動画・スライドを以下の場所に置いています。

※以降の内容は、発表動画・スライドを記事にしたものになります。

Riverpodとは?

  • 状態管理パッケージ
  • Providerをグローバルに定義できる
  • Provierパッケージの上位互換
  • DI機能を使用することでコンストラクタでのバケツリレーが不要になる + テストが容易になる

公式のドキュメントはこちらにあります。
日本語にも対応しているためとても分かり易いです。

DI(依存性注入)とは?

オブジェクト間で生じる依存関係をそのオブジェクト内に直接記述せず、外部から何らかの形で与えるようにするデザインパターン

のことです。

注入方法には3種類あります。

インタフェース注入

注入用のインタフェースを定義して注入を行う方法

setter 注入

setterメソッドを用意して注入を行う方法

コンストラクタ注入

コンストラクタを定義して注入を行う方法

DIを強制することができるので1番おすすめ

レイヤードアーキテクチャとは?

プログラムを役割ごとに、プレゼンテーション層・アプリケーション層・ドメイン層・インフラ層に分割し責任を持たせるアーキテクチャ

こちらの画像はAndreaさんのサイトからお借りしました。

Andreaさんのアーキテクチャを少しだけ変更してみました

変更点

  • Presentation層をViewとViewModelにした
  • Application層を削除してViewModelに厚みを持たせた
  • Domain層にRepositoryのインタフェースを追加してViewModelからはそのインタフェースに対して依存関係を持たせるようにした = 依存関係逆転の原則

RiverpodのDI機能の紹介

通常のDI(依存性注入)よりも簡易的に書くことができるため、コードがかなりスッキリします。

/// PostPageViewModelのインスタンスを返却するプロバイダ
final postPageViewModelProvider = Provider<PostPageViewModel>((ref) {
  return PostPageViewModel(
      postRepository: ref.watch(postRepositoryProvider), ref: ref);
});

class PostPageViewModel {
  final IPostRepository postRepository;
  final ProviderRef ref;

  /// constructor
  /// ViewModelインスタンス化時にViewModel内で使用するRepositoryインスタンスをDI
  PostPageViewModel({required this.postRepository, required this.ref});

  // 各Providerのgetter(viewとのバインド用)
  String get pageTitle => ref.read(postTitleProvider).toString();
  String get contentLabel => ref.read(contentLabelProvider).toString();
  String get accountIdLabel => ref.read(accountIdLabelProvider).toString();
  TextEditingController get contentController => ref.watch(contentControllerStateProvider.state).state;
  TextEditingController get accountIdController => ref.read(accountIdControllerStateProvider.state).state;

  /// 投稿ボタン押下時
  Future<void> onPost(BuildContext context) async {
    await addPost(Post(content: contentController.text, accountId: accountIdController.text));
    Navigator.pop(context);
  }

  /// 投稿コレクションにドキュメント追加
  Future&lt;void&gt; addPost(Post newPost) async => await postRepository.addPost(newPost);
}

4行目で呼び出している、postRepositoryProviderの定義は以下のようになっています。

final postRepositoryProvider = Provider<IPostRepository>(
  (_) => throw UnimplementedError(),
);

型自体はIPostRepository(Repositoryのインタフェース)になっています。

さらにValueとしてはこの時点ではまだ未実装エラーを投げるようにしておきます。

この未実装のProviderをあるタイミングでoverrideしてあげる必要があります。

あるタイミングとは

  1. 通常のアプリ起動時(mainメソッド処理時)
  2. テスト時(今回の例ではUnitTestを想定)

mainメソッドでの未実装Provider上書き方法

void main() {
  runApp(
    ProviderScope(
      overrides: [
        postRepositoryProvider
            .overrideWithProvider(firebasePostRepositoryProvider),
      ],
      child: const MyApp(),
    ),
  );
}

ProviderScopeのoverridesプロパティで複数Providerを上書き(override)することができます。

今回は上記で定義した未実装のpostRepositoryProviderをoverrideしてあげます。

実際にFirebaseに通信しに行く処理を持たせたPostRepositoryを持つProviderをoverrideしてあげます。(6行目)

そうすることで以降、postRepositoryProviderを取得した際は、実質overrideしたfirebasePostRepositoryProviderが取得できるようになるというわけです。

Unit_Test時の未実装Provider上書き方法

void main() {

  final container = ProviderContainer(
  overrides: [
    postRepositoryProvider.overrideWithValue(MockPostRepository()),
  ],
);
  
  final PostPageViewModel vm = container.read(postPageViewModelProvider);
  final MockPostRepository mockRepo = vm.postRepository as MockPostRepository;

  test('投稿ボタン押下時', () async =&gt; {
    await vm.addPost(const Post(content: 'contentです', accountId: 'accountIdです')),
    expect(mockRepo.data['0']!.accountId, 'accountIdです')
  });
}

ProviderContainerのoverridesプロパティで複数Providerを上書き(override)することができます。

今回も通常時と同じく最初に定義した未実装のpostRepositoryProviderをoverrideしてあげます。

Unit_TestではFirebaseの通信は影響させたくないためクライアント上だけでダミーデータの更新を行う処理を持たせたMockPostRepositoryをoverrideしてあげます。(5行目)

そうすることで以降、postRepositoryProviderを取得した際は、実質overrideしたMockPostRepositoryが取得できるようになるというわけです。

ちなみに、Providerに対するoverrideには2種類のやり方があります。

  1. overrideWithProvider:ProviderをProviderで上書きする
  2. overrideWithValue:Providerを中身の値で上書きする

このように依存性注入用のProviderを持たせておくことで、通常アプリ起動時の処理や、テスト用の処理を自由に差し替えることができます。

TDDやDDDでの開発とも相性が良さそうですね。

もっといい書き方があればコメント頂けたら嬉しいです!

ありがとうございました。

サンプルコード