Android開発におけるモジュール分割

Yuya Kaido
Eureka Engineering
Published in
12 min readDec 18, 2019

これはEureka Advent Calendar 2019の18日目の記事です。

はじめに

こんにちは!Androidエンジニアのyuyakaidoです。

最近はアメリカ再建のために運び屋として北米大陸を横断したり、地球を救うためにアンドロイドとして機械生命体と戦ったりする毎日です。アンドロイドとして地球を救う傍らでアプリ開発もやっていて、本記事ではAndroid開発におけるモジュール分割について書いてみます。

さて、Pairsは元々シングルモジュールなプロジェクトでしたが、2019年の前半からモジュール分割に取り組んでおり、本記事ではPairsで採用しているモジュール分割のアプローチを紹介したいと思います。読者の理解促進のためにPairsよりも単純なサンプルアプリをもとにして解説していきたいと思います。

早速ですが、本記事で紹介するサンプルアプリは以下のリポジトリで公開しているので、実際のコードが気になる方は記事と合わせてご覧ください。解説で利用するのはRedditというWebサービスのクライアントアプリで、Redditのアカウントさえあれば誰でも利用できるようになっています。

モジュール分割とは

本題に入る前にモジュール分割とは何を指していて、どういう利点があるのかを整理しておきます。

Androidアプリプロジェクトを新規作成すると自動的にAppモジュールが作成されますが、このAppモジュールを複数のモジュールに分割することが可能で、マルチモジュールプロジェクトと呼ばれることもあります。

Androidアプリ開発においてモジュールは複数の意味をもっていますが、本記事ではLibrary Moduleを指します。

  • Application Module
  • Library Module
  • Dynamic Feature Module
  • Dagger Module

モジュール分割には大まかに以下の利点があります。

  • 並列コンパイルによるビルドの高速化
  • アーキテクチャ構造の強制化

ビルドの高速化については、DroidKaigi 2019にてboohbahさんが発表していた「マルチモジュールAndroidアプリケーション」にモジュール分割によるビルドの高速化についての考察があります。

  • シングルモジュール、直列マルチモジュール、並列マルチモジュールのそれぞれでビルド時間を比較
  • フルビルド、インクリメンタルビルドの両方で並列マルチモジュールが一番ビルド速度が速いという結果(フルビルドよりもインクリメンタルビルドのほうが効果が大きい)

アーキテクチャ構造の強制化についても、DroidKaigi 2018にてkgmyshinさんが発表していた「マルチモジュールのすゝめ」にパッケージ分割だけでは不可能だったアーキテクチャ構造の強制化についての考察があります。

  • シングルモジュールであってもレイヤー単位でパッケージを分割したり意味のあるまとまり単位でパッケージを分割することはできる
  • しかし、パッケージ分割は強制力があるものではないので、プログラマが注意してパッケージの境界を破らないように注意する必要がある
  • 一方で、モジュール分割を行うことでパッケージ分割だけでは実現出来なかった強制力のある構造を作ることができる

モジュールの分割方法

循環参照にならない限りはどのようにモジュールを分割しても問題ありませんが、主に以下2つの分割方法が主流です。

レイヤー単位での分割

AndroidアプリはUIを扱うクラスやドメインロジックを扱うクラス、データの取得や保存を扱うクラスといった数多くのクラスから構成されています。これらのクラス群を以下のようなレイヤーに分割するアプローチはレイヤードアーキテクチャとも呼ばれています。

  • Domain:ドメインロジックを扱うレイヤー
  • Presentation:UIを扱うレイヤー
  • Infrastructure:技術的な関心事を扱うレイヤー

これらのレイヤーをモジュールで表現すると以下のようになります。

機能単位での分割

レイヤーと同様にAndroidアプリは複数の機能が組み合わさって1つのアプリとして動作します。例えば、弊社が提供するPairsもユーザー検索やプロフィール表示、チャットなどが組み合わさって1つのアプリとして成立しています。モジュール分割を行う際はこれらの機能単位で分割を行うことも可能です。

また、全てのFeatureモジュールで利用するような共通コードを配置するようなCoreモジュールを作ることでコードの重複を防ぐのも良い方法です。

サンプルアプリ

Redditのクライアントアプリを実装する場合を考えてみます。

Reddit

主に英語圏で利用されている掲示板サービスで、様々なトピックについて誰でも自由に議論をすることができます。

Redditには多くの機能がありますが、その中でも以下の主要機能を実装する場合を考えてみます。

  • ユーザーの認証
  • 人気記事の表示
  • 記事詳細の表示
  • 記事への投票機能
  • プロフィールの表示

モジュール構造の検討

それではサンプルアプリを実現するためのモジュール構造を検討していきます。モジュール構造を検討する際は以下の2点を意識することが重要です。

  • 機能同士の関係性を反映したモジュール構造にすること
  • レイヤー単位での分割と機能単位での分割を組み合わせること

機能同士の関係性

  • 記事一覧と記事詳細は記事エンティティを通してやり取りをする
  • 記事一覧と記事詳細から投票が可能なことから、投票機能は複数モジュールからの利用を想定する必要がある
  • APIを呼び出すためにはユーザー認証が必須であり、認証機能は全てのモジュールからの利用を想定する必要がある

共通コード

  • Entity / Value Objectなどのドメインオブジェクト
  • BaseActivity / BaseFragmentなどのUI基盤
  • OkHttp / Retrofitなどのネットワーク基盤

これらのコードは全てのモジュールが必要とするため、共通コードとしてCoreモジュールに配置します。

最終的なモジュール構造

機能同士の関係性と共通コードの扱いを踏まえて、以下のようなモジュール構造を組んでみました。

ポイントはAuthモジュールやArticleモジュールは他のモジュールからも利用される可能性があるので階層化してあるところと、Coreモジュールをレイヤー毎に分割しているところです。

ユースケース

さて、ここまででAndroidアプリを開発する上で、どのようにモジュール分割を行っていけばいいのかが何となくご理解いただけたと思うので、もう少し踏み込んだユースケースを実際のコードとともに解説していきます。

機能同士の連携

アプリを開発する中で色々な画面から利用するような機能を実装することは多くあると思います。シングルモジュール環境では何も難しいことはないですが、マルチモジュール環境では機能同士を連携させるには少しコツが必要です。

例えば、今回のサンプルアプリでは記事一覧や記事詳細から記事への投票を行うことができます。投票は記事エンティティ(Article)に紐付く機能なのでArticleモジュールで実装されています。また、記事一覧はArticle-Listモジュール、記事詳細はArticle-Detailモジュールで実装されており、これらのモジュールはArticleモジュールを親モジュールとして以下のような構造になっています。

投票機能はArticleモジュールのArticleRepositoryで実装します。

class ArticleRepository @Inject constructor(
private val authApi: RedditAuthApi
) {
suspend fun vote(target: VoteTarget) {
authApi.vote(id = target.article.name, dir = target.dir)
}
}

記事一覧や記事詳細はArticleRepositoryを経由して投票機能を利用します。

class ArticleListViewModel @Inject constructor(
application: Application,
override val repository: ArticleRepository
) : AndroidViewModel(application) {
private fun vote(target: VoteTarget) {
viewModelScope.launch {
repository.vote(target = target)
}
}
}

このように色々な画面から利用される機能は共通のモジュールに切り出すことで、モジュールを分割しつつも色々な画面から同一の機能を利用することができます。

画面遷移

機能単位のモジュール分割はビルドの高速化に寄与しますが、実装上は少し困ったことが起きてしまいます。それはモジュール同士の依存関係が組めないことで直接的な画面遷移ができないという問題です。

AndroidアプリではActivityやFragmentを利用して画面を構築し、IntentやFragmentManagerなどを使って画面遷移を実装すると思います。例えば、シングルモジュール環境で記事一覧から記事詳細に遷移する場合は以下のようなコードになるでしょう。

class ArticleListActivity : AppCompatActivity() {
private fun onClickArticle(article: Article) {
startActivity(ArticleDetailActivity.createIntent(this, article))
}
}

シングルモジュール環境では問題なくコンパイル可能ですが、マルチモジュール環境ではArticle-ListモジュールはArticle-Detailモジュールに依存していないのでコンパイル不可能です。

そこで、Coreモジュールに画面遷移を抽象化したインターフェースを用意し、全てのFeatureモジュールへの依存関係を持っているAppモジュールにて実装クラスを用意します。

// Coreモジュール
interface AppRouterType {
val application: Application
fun newArticleDetailActivity(article: Article): Intent
}
// Appモジュール
class AppRouter @Inject constructor(
override val application: Application
) : AppRouterType {
override fun newArticleDetailActivity(article: Article): Intent {
return ArticleDetailActivity.createIntent(application, article)
}
}

AppRouterTypeはCoreモジュールに配置されており、どのFeatureモジュールからも参照可能になっているので、モジュール間の画面遷移はこのAppRouterTypeを経由して行います。FeatureモジュールにAppRouterTypeを配布するのはDaggerなどのDIライブラリを使うと良いでしょう。

class ArticleListActivity : AppCompatActivity() {
@Inject
internal lateinit var appRouter: AppRouterType

private fun onClickArticle(article: Article) {
startActivity(appRouter.newArticleDetailActivity(article))
}
}

このようにモジュールを横断するような画面遷移はCoreモジュールを経由することで実装することができます。

おわりに

本記事ではAndroid開発におけるモジュール分割を取り上げました。

モジュール分割とは

モジュール分割とは、1つのアプリを複数のモジュールに分解しながら開発することを指しており、大まかに以下の利点があります。

  • 並列コンパイルによるビルドの高速化
  • アーキテクチャ構造の強制化

モジュールの分割方法

モジュール分割は以下のアプローチで行うのが一般的です。

  • レイヤー単位での分割
  • 機能単位での分割

サンプルアプリでのモジュール分割

モジュール構造を検討する場合は以下のポイント

  • 機能同士の関係性を反映したモジュール構造にすること
  • レイヤー単位での分割と機能単位での分割を組み合わせること

本記事で取り上げたAndroidにおけるモジュール分割は、DroidKaigi 2020にて「実践マルチモジュール」というタイトルで発表予定です。この発表ではより詳細なコードまで踏み込んだ内容で発表予定ですので、興味のある方はご参加ください!

--

--