見出し画像

Flutterを技術スタックに加えてアプリチームは加速する #2 アーキテクチャ選定編

🎍🍱 あけましておめでとうございます 🗻🦅🍆
年末年始はいかがお過ごしでしたか?
新しい抱負を持って2022年アクセル踏みっぱなしでいきましょう🚗

今回はFlutterをプロダクト採用するにあたって検討したアーキテクチャにフォーカスしてお送りしたいと思います。
Flutterを技術スタックに加えてアプリチームは加速する #1 導入編の続編となります。

プロジェクト構成

アプリケーションコードはlibディレクトリ配下に属し、以下のようなツリーになっています。

├── lib
│   ├── app
│   │   ├── app.dart
│   │   ├── entity # <------------------------- エンティティクラスを格納
│   │   ├── env    # <------------------------- 環境変数の管理モジュール
│   │   ├── model
│   │   │   ├── action
│   │   │   │   ├── creator # <---------------- 一連のaction発行ロジックをaction_creatorとして定義
│   │   │   │   ├── xxxx_action # <------------ fluxのaction定義
│   │   │   ├── navigation
│   │   │   │   └── navigation_stack.dart # <-- navigationのロジック定義
│   │   │   ├── repository
│   │   │   │   ├── xxxx_repository.dart # <--- repositoryモジュール
│   │   │   └── status
│   │   │       ├── xxxx_status # <------------ アプリケーション共通の状態を定義
│   │   ├── router # <------------------------- 画面遷移モジュール
│   │   └── view
│   │       ├── screen # <--------------------- 1画面1screenで定義(タブ内画面も)
│   │       │   ├── xxxx
│   │       │       ├── xxxx_screen.dart # <--- widget
│   │       │       ├── xxxx_store.dart # <---- store
│   │       │       ├── xxxx_viewmodel.dart# <- viewmodel
│   │       │       └── state # <-------------- screenの状態クラス
│   │       └── widget
│   ├── core
│   │   ├── flux # <--------------------------- flux抽象化クラス/モジュール群
│   │   │   ├── action_creator.dart
│   │   │   ├── dispatcher.dart
│   │   │   ├── event_action.dart
│   │   │   └── event_store.dart
│   │   ├── res # <---------------------------- リソース管理モジュール
│   │   │   ├── r.dart
│   │   │   └── text
│   │   │       ├── text_string.dart
│   │   │       └── text_string_ja.dart
│   │   ├── theme # <-------------------------- テーマ管理モジュール
│   │   └── viewmodel # <---------------------- ViewModel抽象化クラス
│   ├── external  # <-------------------------- アプリ外部のデータアクセスモジュール群
│   │   ├── api
│   │   │   ├── spm_v1_api_service
│   │   │   └── spm_v2_api_service
│   │   ├── cookie
│   │   ├── firebase
│   │   ├── secure_storage
│   │   └── shared_preferences
│   └── main.dart

ポイントとしては、

・UI構成とUIロジックに関わるものは、app/view配下
・ドメイン知識に関するものは、app/entity配下
・アプリケーションの状態やロジックに関わるものは、app/model配下
・アプリの外界に対するクライアントモジュールは、external配下としrepositoryを経由して参照する

ということを意識したディレクトリ構成としています。

Fluxの構成

上記ディレクトリのlib/core/flux以下にはFluxを実現するためのモジュールを用意しています。

モジュールのコードはこちらです。

ActionCreatorとEventActionはextendsして使うためにabstractで定義しています。

EventStoreはChangeNotifierをextendsして、新しいstateに更新されたときに notifyListeners() することで、watchしているモジュールに変更が伝播されます。

上記をもとに、BottomNavigationBarで表示を切り替えるホーム画面のStoreを作ると以下のようになります。

///
/// home_store.dart
///

// storeをwatchするためのproviderを定義
final homeStoreProvider =
    ChangeNotifierProvider.autoDispose((_) => HomeStore());

// EventStoreをextendsしたStoreクラスを定義
// 型ジェネリクスにはstoreで取り扱うstateクラスを指定
class HomeStore extends EventStore<HomeState> {
    HomeStore() : super(HomeState());
 
    // Actionに応じて新しいstateをセットする
    @override
    void onAction(EventAction action) {
        if (action is HomeActionTapTab) {
            state = state.copyWith(selectedTabIndex: action.index);
        }
    }
}

///
/// home_state.dart
///
@freezed
class HomeState with _$HomeState {
    const factory HomeState({
      @Default(0) int selectedTabIndex,
    }) = _HomeState;
}

ViewModelの構成

上記のホーム画面に対するUIロジックを切り出したViewModelは以下のようになります。

final homeTabViewModelProvider = ChangeNotifierProvider.autoDispose(
    (ref) => HomeTabViewModel(
        ref.watch(homeStoreProvider),
        ref.read(homeActionCreatorProvider),
    ),
);

abstract class _Input {
    void onTapBottomNavigationBar(int index);
}

abstract class _Output {
    int get selectedTabIndex;
}

class HomeTabViewModel extends ViewModel with _Input, _Output {
    final HomeStore _homeStore;
    final HomeActionCreator _homeActionCreator;
    final TrackingManager _trackingManager;

    HomeTabViewModel(
        this._homeStore,
        this._homeActionCreator,
        this._trackingManager,
    );

    @override
    void onTapBottomNavigationBar(int index) {
        _homeActionCreator.setTab(index);
    }

    @override
    int get selectedTabIndex => _homeStore.state.selectedTabIndex;
}

riverpodのChangeNotifierProviderを使用して、ViewModelを監視できるproviderを作成しています。(使い方は次のセクションで紹介します)
生成時に必要なstoreやActionCreatorへの依存性はそれぞれのproviderをwatchまたはreadすることで解決しています。

ViewModelに対するinputとoutputをそれぞれabstractで定義して、見通しを良くしています。

ViewModelはinputに応じてActionCreatorのメソッドを実行したり、outputに基づいてstoreから必要な値を読み取るという仕事をしています。

riverpodを使った状態監視とDI

今回DI(Dependency Injection)を実現するためにriverpodのProviderを使用しました。

repositoryモジュールなど外部モジュールから参照されるようなモジュールは以下のようにProviderをまとめています。
repositoryでも依存しているクライアントモジュールを取得するために必要なProviderをreadしています。

final calendarRepositoryProvider = Provider(
    (ref) => CalendarRepository(
        ref.read(sharedPreferencesManagerProvider),
        ref.read(spmV1ApiServiceProvider),
    ),
);

final reservationRepositoryProvider = Provider(
    (ref) => ReservationRepository(
         ref.read(sharedPreferencesManagerProvider),
         ref.read(spmV1ApiServiceProvider),
         ref.read(spmV2ApiServiceProvider),
    ),
);

final roomRepositoryProvider = Provider(
    (ref) => RoomRepository(
         ref.read(sharedPreferencesManagerProvider),
         ref.read(spmV1ApiServiceProvider),
    ),
);

そしてrepositoryを使いたいモジュールでは、そのrepositoryのProviderをreadすることで参照することができます。

final reservationActionCreatorProvider = Provider(
    (ref) => ReservationActionCreator(
      ref.read(reservationRepositoryProvider),
      ref.read(userRepositoryProvider),
    ),
);

依存性の方向としては

・ユーザーのアクションに対して何か行う場合
View → ViewModel → ActionCreator → Repository → 外界(API等)

・アプリケーションの状態を表示する場合
View → ViewModel → Store

という方向になることを意識しています。

widgetの設計思想

Flutterのwidgetは宣言的UIであり、Reactでのベストプラクティスを活かすことができます。

スペースマーケットのフロントエンド開発では、こちらの記事での紹介をベースに、componentは以下の思想でディレクトリ設計しています。

・関連するコンポーネント(サブコンポーネント)をcomponentsディレクトリに格納する
・componentsディレクトリ内の構造は再帰的な構造とする

これをFlutterのwidgetにも適用することにしました。

例えば以下のようなツリーになります。
そのwidgetで必要な子孫widgetはwidgetディレクトリを再起的に定義することで見通し良く、かつ影響範囲を最小限にしています。

├── todo
    ├── todo_screen.dart
    ├── todo_screen_viewmodel.dart 
    └── widget
        └── message_list
            ├── message_list.dart    
            ├── message_list_viewmodel.dart      
            └── widget
                └── message_bubble
                    └── message_bubble.dart
                    └── widget
                        ├── bubble_image.dart
                        ├── bubble_text.dart
                        └── bubble_opponent.dart

簡単ではありましたが、以上が今回のアプリ開発で設計面でポイントとなった部分でした。

まだまだ細かい点でご紹介できるので、気になる方はお気軽に聞いていただければと思います。

今回のまとめ

プロジェクト構成の紹介
Flux・ViewModelの設計、riverpodの活用方法について紹介
widgetのディレクトリ設計の紹介
次回「運用編」でチームビルディングやナレッジ共有など紹介します。

お知らせ

モバイルアプリエンジニア採用中です!
いっしょに指先の体験をアップデートしていきませんか?


この記事が気に入ったらサポートをしてみませんか?