はじめに
こんにちは。令和トラベルでスマートに海外旅行を予約できるアプリ「NEWT(ニュート)」のAndroidアプリ開発を担当している赤池です。
NEWTはローンチから半年以上(2022年12月時点)が経過し、開発組織も少しスケールして現在は2チーム体制で開発をおこなっています。今回はそんなNEWTのAndroidアプリ開発を支えるアーキテクチャを紹介したいと思います。
アーキテクチャ導入の目的
アーキテクチャ導入の主な目的は、制約が生まれることによって一貫性が保ちやすくなり、メンバー間の共通認識も醸成され、堅牢性や開発効率などが向上することと考えています。
- まとまりごとの責務が明確になり、何をどこに書くべきか迷わなくなる
- 依存の方向性や公開の制約などにより、思わぬバグが仕込まれにくくなる
- 注目すべき点が限定され、コードレビューの負担が軽減される
- 複数チーム・メンバーでも互いの影響・競合を抑えた開発を進められる
- OSのライフサイクルなどからの影響を局所化できる
- 差分ビルドが効いてビルド速度が向上する
- プラットフォームが提供する機能(Play Feature Deliveryなど)が利用できるようになる
一方でコード量や冗長性が増す面はあるので、個人開発でリリースして終わりのアプリケーションなどであれば導入の必要性は薄いです。どれくらいの制約をかけるべきかはバランスを取りつつですが、スケールを見込んだプロダクトであれば土台となる部分は最初から導入しておくのが良いかと思います。
アーキテクチャ概要
NEWTはClean Architectureなどを参考にしつつ、独自の課題に対応する形で構築しています。大きくは、UI/Domain/Dataでレイヤーを区切って責務を定義し、データとイベントが単方向に流れるようにしています。
UI層
- 表示とイベントハンドリングを担当
- ScreenはJetpack Composeで構築
- ViewModelで画面の状態(UiState)を保持
Domain層
- アプリケーション固有のロジックを担当
- エンティティやデータ取得のインターフェース(Repository)を定義
- NEWTでは今の所UseCaseは設けず、Model内でバリデーション等を処理
Data層
- データの入出力を担当
- APIなどとのリクエスト/レスポンスを処理
- 必要に応じてキャッシュや永続化を処理
現在はAndroid Developersからアーキテクチャガイドが提供されているので、これから新規で開発する場合はこちらをベースにするのも良さそうです。NEWTでは改修が多い箇所への依存をできる限り小さくするため、Interfaceによる抽象化で依存を逆転(Domain←Data)させたりもしています。その辺りなどアーキテクチャガイドで紹介されている内容とは少し異なる作りになっています。
モジュール構成
ローンチ時点では単一モジュールでしたが、負債の蓄積やチーム分割などもあり、現在はマルチモジュール構成に移行しています。
- feature(UI)・data・domainのレイヤーごと水平方向で分割
- featureは機能のまとまり(NavGraph)ごと垂直方向でも分割
- dataは子レイヤーごとでも分割
- baseには環境設定やログなどの全体が利用するクラスを配置
- domainはやbase以外のモジュールやAndroidへ依存させずプレーンに
- data内の実装クラスはinternalにしてDIを通して外部に提供
- apiは抽象化しGraphQLへ依存させない
移行作業を行った中で、ドキュメントで定義していた制約に違反している箇所(UIが直接Dataに依存してたり)がいくつか見つかったりもしました。コードレビューを行なっていても見逃されてしまうことはあるので、internal修飾子とDIで意図しない参照が行えないよう制限してしまうことが有効です。こうした利点も多いので、Android Developersからガイドも出ている現時点では、スタートからマルチモジュール構成で始めても良いかもしれません。
なお、NEWTの場合は複雑性が高いfeature(UI)のみ意味のまとまりでの分割も行っていますが、分割の方法や粒度などに関しては、他社の事例を参考にしつつ自社プロダクトの特性や課題に応じて判断する必要があるかと思います。
GraphQLに関しては、NEWT-Androidでは新規立ち上げ時の不安定さなどを考慮し、依存させずに変更や拡張がしやすい形でスタートさせています。バックエンドチームと連携したスキーマドリブンな開発をしていく場合などは、GraphQLに依存した構成にしてしまうのも一つかもしれません。
画面の状態管理
NEWT-AndroidのUIはJetpack Composeで構築し、画面構成・遷移はNavigation Composeで定義しています。画面の状態はViewModelにUiStateを保持させる形をとっています。
上記図にあるように、状態ホルダーであるViewModelは以下いずれかにスコープしています。
- 単一のScreen
- 複数Screenを子に持つNavGraph
- NavHostの親Activity
Navigation Composeで構築したナビゲーショングラフ内では、lifecycle-viewmodelを継承したViewModelをhilt-navigation-composeを利用して取得することで、ナビゲーションのDestinationであるScreenやNavGraphをViewModelStoreOwnerにすることができます。
このようにスコープを使い分けることで、複数画面共通で保持させたい状態や、単一の画面をとじるごとに破棄したい状態などに対応しています。
おわりに
以上、NEWT-Androidのアーキテクチャの概要紹介でした。
NavigationやGraphQL周りなどで、もうちょっとこういう作りにしておいた方が良さそうといったポイントもいくつかあるので、今後も機をみて改善していきたいと思っています。
そんな令和トラベルでは、一緒にNEWTを発展させていく仲間を募集しています。ご興味がある方はぜひ採用ページをご覧ください。
そして、12月21日に令和トラベル、カウシェ、10X、の3社合同でアプリ開発についてイベントを開催します。
このイベントでは、この記事の内容はもちろん、ここでは書ききれなかった部分についてもお話しする予定です。ぜひ興味を持ってここまで読んでくださった方は、イベントの参加申し込みもよろしくお願いします!
【令和トラベル x カウシェ x 10X スタートアップ3社合同】モバイルアプリエンジニアのリアル
それでは次回のブログもお楽しみに!