Androidアプリアーキテクチャ (1)
はじめに
この記事では、私がAndroidアプリを開発する際によく使うアーキテクチャについて連載形式で解説していきたいと思っています。
第1回は、アーキテクチャの全体像とサンプルアプリをご紹介します。
大前提として設計は正解の無いもので、また用途や制約に応じて変化させるべきものです。ここで紹介する設計についても、一つの例として捉えベースラインくらいに考えていただければ幸いです。
それとは別にアプリのアーキテクチャにも流行というものがあります。
MVC, MVP, MVVM, Fluxなどデザインパターンは様々ありますが、AndroidではGoogleが推奨しているMVVMが主流になりつつあるのではないかと思います。
cf. https://developer.android.com/jetpack/docs/guide?hl=ja#recommended-app-arch
iOSアプリでも長らくCocoaMVCというアーキテクチャがベースになってきましたが、SwiftUIとCombine Frameworkの登場により今後数年でMVVMベースのアーキテクチャが主流になりそうな気配があります。
cf. https://developer.apple.com/documentation/swiftui
cf. https://developer.apple.com/documentation/combine
必要な知識
この記事では、モバイルアプリの基本的な開発知識、MVCやMVPなどのアーキテクチャに対する基本的な理解を前提としています。
アーキテクチャの概要
今回ご紹介するアーキテクチャはできる限り学習コストを抑えつつ、どんな処理をどこに記述するべきか迷わず、誰でも一貫性のあるコーディングができることを目指しています。
簡単に概要を紹介しておくと、このアーキテクチャはMVPをベースにしており、ModelについてはDDD(ドメイン駆動設計)の考え方を適宜用いて更にレイヤーを分けた構成にしています。
画面構成はシングルActivity、マルチFragmentをベースとし、アプリにActivityは一つだけというのが原則になります。そして、画面遷移は全てこの一つのActivityが制御するというのが、基本的な考え方になります。
言語はKotlinで実装し、非同期処理にCoroutineのasync/awaitパターンを採用しています。
大規模で複雑な処理をアプリ内で実装しているようなプロジェクトでは、このアーキテクチャは耐えられないかもしれません。ビジネスロジックをできる限りサーバーサイドに配置し、モバイルアプリからはAPI経由で呼び出すような、あくまで小・中規模のアプリに有用であると考えています。
アーキテクチャの必要性
アーキテクチャとは、アプリを実装しようと思ったときに、その実装の仕方にある程度ルールを与える役割がありますが、なぜそのようなことをするのかについて私の考え方を述べておきます。
アーキテクチャの必要性を考えていくために、まずはアプリの実装方法に何もルールが無い場合に、どんな状態に陥る可能性があるかを想像してみます。
この場合、アプリを実装する際に特に一貫した考え方がある訳ではないため、同じ役割の処理をその時の勢いにまかせて全く異なる場所に記述してしまったり、反対に全く異なる役割の処理でも同じ場所に記述し続けて一つのソースコードが巨大になってしまうことがあります。
他にも細かい例を挙げればキリが無いですが、一言で言うと自分勝手に書かれたコードは非常に読みにくいコードになりやすいということです。
次に読みにくいコードは何が問題なのかを考えていきます。
開発したアプリに新しい機能を追加したり、不具合が発見されてバグを修正したりする状況を考えてみてほしいですが、このとき最初に行う作業は既存のソースコードを読んで理解することです。
ソースコードを理解することで、どこに処理を追加すれば意図した機能が正しく追加されるのか、どこの処理を変更すれば不具合が正しく修正されるのかを判断することができるからです。
このとき、自分が読んで理解しなければいけないソースコードが、自分勝手に記述された非常に読みにくいコードだったら、理解するのに時間がかかってしまうことに加え、意図せず新たなバグを作り出してしまう可能性も高くなってしまいます。
この問題は、特に複数人でアプリを開発する場合や、開発メンバーが変わる場合に顕著ですが、個人でアプリを開発している場合でも、少し時間が経つとどういう意図でこの実装をしたのかは案外忘れてしまうものです。
そこで、アーキテクチャという形で実装の仕方にある程度一貫した考え方を導入することで、ソースコードの基本的な構造を固定します。これにより、そもそもどこを読むべきなのかに迷わなくなるのと、モジュールが役割ごとにキレイに分割されて修正しやすいソースコードを導くことができます。
これはソースコードの読み手だけにメリットを与えるものではなく、書き手にとっても処理の記述場所に迷うことがなくなったり、複数人で開発する際にチーム全体で一貫したソースコードを記述することができるようになったりします。
つまり、アプリ開発におけるアーキテクチャとは、その実装の仕方にある程度ルールを与えることで、読みやすく修正しやすいソースコードを導くために必要というのが私の考え方になります。
アーキテクチャの全体像
それでは、今回ご紹介するアーキテクチャの全体像をまずは示しておきます。MVPにData層を加えたレイヤードアーキテクチャになっているのが全体的な構造で、各レイヤー間のインタラクションはインタフェースを介する形になっています。
簡単に各モジュールの役割について説明しておきます。
※命名は文化や慣習で変わってくるものなので、適宜読み替えてください。
<クラス>
View ・・・ Presenterへのイベントの通知と画面制御の処理を実装します。
Presenter ・・・ イベントとビジネスロジックの対応付け、処理結果に応じたViewの画面制御メソッドの呼び出しを実装します。
Service ・・・ ビジネスロジックを実装します。ほとんどの場合DataSourceを呼び出してデータの取得や加工処理を実装します。
DataSource ・・・ データソースに対するデータの作成・取得・更新・削除の処理を実装します。
<データクラス>
Entity ・・・ データソースから取得した生のデータをモデリングします。
DataModel ・・・ アプリ向けに整形したデータをモデリングします。
<インタフェース>
Contract ・・・ Viewが提供する画面制御のメソッド、Viewで発生するイベントを通知するメソッドを定義します。
ServiceProtocol ・・・ あるServiceクラスが提供するメソッドを定義します。
Repository ・・・ あるDataSourceクラスが提供するメソッドを定義します。
また、いくつか状況によって変わってくる部分を説明しておきます。
例えば、今回は全てのレイヤー間でインタフェースを設けていますが、Contractを除いたインタフェースはDI(依存性注入)やユニットテストを実装しない場合や、複数人で開発しない場合は省略することもあります。
加えて、アプリ向けに設計されたAPIを使用してデータソースにアクセスする場合は、Entityがそのままアプリで使用できる必要十分な形になっていることが多く、そのような場合はわざわざDataModelは作成しません。
最後に各レイヤー間のやりとりの方法について、ViewとPresenter間はコールバックパターンでといったように原則は示していますが、やりたいことの複雑度が高い場合はもう少し柔軟な操作が可能なパターンを用いることもあります。
クラスやインタフェースの構成も必要最低限で、各レイヤーの役割もはっきりしており、そのやり取りもコールバックやasync/awaitを用いていて、とっつきやすいアーキテクチャに仕上がっているのではないかと思います。
このあと、さらにアーキテクチャを具体化してサブテーマに分割した上で詳細に解説していきます。
サンプルアプリ
解説に入っていく前に、アーキテクチャを理解するには具体例があるとわかりやすいです。そのため、今回は note のAPIを使用してViewerアプリを実装してみました。
アプリの画面・機能は下図の通りです。これを用いてアーキテクチャの細かい部分を説明していきたいと思います。
【ノート一覧画面】
【ノート一覧画面】ローディング表示
【ノート一覧画面】Pull-to-Refresh
【ノート一覧画面】タイトル検索
【ノート詳細画面】
サンプルアプリのパッケージ構成とクラス
サンプルアプリを実装するにあたって作成したパッケージ構成と、実装したクラス一覧を下記の通り示しておきます。基本的に上記で示したアーキテクチャ図に沿った形でパッケージを分割し、各クラスを配置していることが分かるかと思います。
また、アーキテクチャ図には表れていないものに、appパッケージとextensionパッケージがありますが、それぞれアプリケーション共通の実装を行うクラスとユーティリティ系の実装を行うファイルを配置するパッケージになっています。
├── app
│ ├── App.kt
│ └── Constants.kt
├── data
│ ├── entity
│ │ ├── NoteDetailEntity.kt
│ │ └── NoteEntity.kt
│ ├── repository
│ │ └── NoteRepository.kt
│ └── source
│ └── NoteDataSource.kt
├── domain
│ ├── model
│ ├── protocol
│ │ └── NoteServiceProtocol.kt
│ └── service
│ └── NoteService.kt
├── extension
│ └── Extensions.kt
└── presentation
├── contract
│ ├── Contract.kt
│ ├── MainContract.kt
│ ├── NoteDetailContract.kt
│ └── NoteListContract.kt
├── presenter
│ ├── MainPresenter.kt
│ ├── NoteDetailPresenter.kt
│ └── NoteListPresenter.kt
└── view
├── activity
│ └── MainActivity.kt
├── component
│ └── NoteAdapter.kt
└── fragment
├── BaseFragment.kt
├── NoteDetailFragment.kt
└── NoteListFragment.kt
今回は役割ごとに垂直分割したクラスの配置にしていますが、presentationパッケージ配下は画面ごとに水平分割したクラス配置にすることもあります。この辺は好みの問題だと思います。
おわり
第1回はアーキテクチャとサンプルアプリのご紹介までになりますが、今後サンプルコードも添えながらこのアーキテクチャの設計思想や具体的なユースケースをもとに、こんなときはどう実装するべきかということを解説していきたいと思っています。
この記事が気に入ったらサポートをしてみませんか?