Skip to content

FlutterKaigi 2022「Flutterアプリの安全な変化と拡大を支えるアーキテクチャと単体テスト」のためのサンプルアプリ

License

Notifications You must be signed in to change notification settings

tfandkusu/flutter_architecture_sample

Repository files navigation

Flutter Architecture Sample

codecov

FlutterKaigi 2022「Flutterアプリの安全な変化と拡大を支えるアーキテクチャと単体テスト」のためのサンプルアプリ

登壇資料

インストール

現在のmainブランチを配布しています。

Android

Try it on your device via DeployGate

Web

iOSユーザまたはPCユーザはこちらでご確認ください。

Webアプリを開く

ビルド

asdfを使用しています。インストールされていなければこちらを参考にインストールします。

ビルドする環境の構築

asdf plugin add flutter
asdf plugin add ruby
asdf install
gem install cocoapods

ビルドして実行する

flutter pub get
flutter pub run build_runner build
flutter run

機能

私のGitHubリポジトリ一覧を表示 詳細画面ではさらにREADME.mdを表示

リポジトリ一覧はGitHubのREST APIから取得

https://api.github.com/users/tfandkusu/repos?page=1

所謂「いいね問題」に対応

詳細画面で「いいね」を付けると 一覧画面でも「いいね」が付いている

「いいね」を付けた情報はアプリローカルに保存

使用技術

基本

Webアクセス

データ保存

表示

テスト

CI/CD

アーキテクチャ

Androidのアプリ アーキテクチャガイドに沿った2レイヤ構成のアーキテクチャです。

architecture

RemoteDataSource

  • APIアクセスなどのネットワーク通信を担当します。

LocalDataSource

  • アプリローカルへの情報の読み書きを担当します。

ModelStateNotifier

  • 別の画面で行われた更新をもれなく反映するために、複数の画面から使われるデータはRiverpodのStateNotifierに持たせています(所謂「いいね問題」対応)。

Repository

  • データレイヤを代表して各種データソースにアクセスして、その結果を返却したり、ModelStateNotifierを更新します。

UiModelStateNotifier

  • 画面固有の状態遷移を担当します。
  • 1画面に1つのUiModelクラスがあり、そのインスタンスを状態として持つStateNotifierです。

UiModelProvider

  • 画面の状態をWidgetに提供します。
  • UiModelStateNotifierとModelStateNotiferの変更を監視し、両者を合成して画面の状態を作成します。

EventHandler

  • 画面が開かれた時の処理とユーザ操作によって始まる処理は、このクラスに記述します。
  • 処理の内容はUiModelStateNotifierとRepositoryのメソッド呼び出しとなります。

ScreenWidget

  • UiModelProviderから提供されたUI状態に従いWidgetを構築します。
  • 画面が開かれた時とユーザ操作に対応して、EventHandlerのメソッド呼び出しを行います。

Tips

実プロダクトではドメインレイヤが必要

このサンプルアプリのアーキテクチャのまま実プロダクトを作成すると、EventHandlerクラスとその単体テストが巨大ファイルになり、可読性が悪くなる可能性があります。 そこでドメインレイヤを追加して、1処理を1実装クラスと1単体テストファイルにすることで、巨大ファイルの発生を防ぎます。

domain

ワンショットオペレーションを実装したい場合

このサンプルアプリには画面遷移やToastなど、ワンショットオペレーションを取り扱う場合の対応も実装されています。ホーム画面から詳細画面の遷移を状態ホルダから制御しています。

まず画面の状態に、ワンショットオペレーションの呼び出しパラメータをnullableなフィールドとして追加します。

@freezed
class HomeUiModel with _$HomeUiModel {
  /// ホーム画面の状態
  ///  
  /// [callDetailScreen] 【追加】詳細画面を呼び出す
  const factory HomeUiModel(
      {required bool progress,
      required List<GithubRepo> repos,
      required ErrorUiModel error,
      GithubRepo? callDetailScreen}) = _HomeUiModel;
}

画面の状態のStateNotifierに、ワンショットオペレーションの呼び出しパラメータに値を設定するメソッドと、それをnullに戻すメソッドを追加します。

class HomeUiModelStateNotifier extends StateNotifier<HomeUiModel> {
  HomeUiModelStateNotifier()
      : super(const HomeUiModel(
            progress: true, repos: [], error: ErrorUiModel.noError()));

  /// 詳細画面を呼び出す
  ///
  /// [repo] 詳細画面を呼び出す対象のGitHubリポジトリ
  void callDetailScreen(GithubRepo repo) {
    state = state.copyWith(callDetailScreen: repo);
  }

  /// 詳細画面の呼び出しが完了した時に呼ばれる
  void onDetailScreenCalled() {
    state = state.copyWith(callDetailScreen: null);
  }
}

EventHandlerのメソッドからStateNotifierのメソッドを呼び出します。

/// ホーム画面のイベント処理担当クラス
class HomeEventHandler {

  // 略

  /// GitHubリポジトリ項目がクリックされたときに呼ばれる
  ///
  /// [repo] クリックされたGitHubリポジトリ
  void onClickRepo(GithubRepo repo) {
    // 詳細画面を呼び出す
    _stateNotifier.callDetailScreen(repo);
  }

  /// 詳細画面の呼び出しが完了したときに呼ばれる
  void onDetailScreenCalled() {
    _stateNotifier.onDetailScreenCalled();
  }
}

画面のWidgetではref.listenメソッドのコールバック内で checkOneShotOperation メソッドを呼び出し、そのコールバックに値が設定されたときの処理を記載します。処理が完了した後のEventHandlerメソッド呼び出しも忘れずに行います。

ref.listen(homeUiModelProvider, (previous, next) {
  checkOneShotOperation(previous, next, (state) => state.callDetailScreen,
      (repo) {
    // 詳細画面に遷移する
    Navigator.pushNamed(context, DetailScreen.routeName,
        arguments: DetailScreenArgument(
            id: repo.id,
            name: repo.name,
            defaultBranch: repo.defaultBranch));
    // 遷移した場合は完了報告を行う
    eventHandler.onDetailScreenCalled();
  });
});

描画パフォーマンスのためにWidgetのリビルド範囲を狭めたい場合

このアーキテクチャは1画面1HookConsumerWidget構成のため、画面の状態が変わるとconstを付けた要素以外のすべての要素がリビルドされます。もし画面の要素数が多く描画パフォーマンスの問題が発生したがリビルド範囲を狭めることで解決できる場合は、下図の構成にすることでリビルド範囲を限定することができます。このサンプルアプリでは詳細画面の上部分と下部分でリビルド範囲を分割しています。

header_body

hook_consumer_widget

About

FlutterKaigi 2022「Flutterアプリの安全な変化と拡大を支えるアーキテクチャと単体テスト」のためのサンプルアプリ

Resources

License

Stars

Watchers

Forks