original post: https://matakucom.medium.com
minne の Android アプリのマルチモジュール化がようやく走り始めたので、背景を記します。
なぜマルチモジュール構成にするか
ユーザに最適なアプリを届けるにあたり、Google が提供している dynamic feature module をはじめとする、モジュール前提で提供されている恩恵を受けるため というのが主な目的です。
リモートインスタンスを利用したビルドを行っていたり、チューニングをしている結果、特にビルド時間で困ってはいないので、モジュールのビルドキャッシュを利用してビルド高速化の恩恵を受けたいな!!!!というのはあったら良いなくらいの考えです。
ただ将来的に開発が進むにつれコードが増えていくことを考えると、ビルドキャッシュは長期的な面で効いてくる場面が気付かずあると思われます。
minne ではゲスト、会員登録済みユーザ、作家 とユーザ属性が 3 種類存在しており、どのユーザ属性の方でもアプリが使えるように全部入りの apk を提供しています。ですが作家属性でしか使えない機能は、作家以外の属性には必要ないように、作家属性を得たタイミングで使えるようになっていさえすれば問題ないはずです。
また新規インストールした際も、会員登録しないと使えない機能は登録していただいたタイミングで使えるようになれば問題ないはずなので、Google Play ストアから配布されるアプリサイズ削減にも繋がってきます。
モジュール構成
現状は以下を想定しています。
# ディレクトリ構成
project
|-- app
|-- core
|-- ui-component
|-- feature
|-- 機能 1 module
|-- 機能 2 module
|-- 機能 3 module
Core module
feature module 間で共通の汎用的なクラスが存在しているモジュールです。
minne には歴史的背景により BaseActivity となるクラスが存在しているので、主にそれを構成するためのコンポーネントがいます…で終わりたかったのですが、model, datasource, utility クラスもここに集約しています。
最適な分け方を考えてると始まりもしないし終わりもしないので、各feature module が動くことをまず考え、5 年ものの app から基幹部分をある程度ばーんと移した結果になっています。core を分けていく作業は feature 以上のモジュールには依存モジュールの再指定くらいであまり影響はないため、スタートとしてはこれで良いだろうという判断です。
feature module
ある 1 機能を実現するためのモジュールです。
feature module の単位をどうするかというのを考えていた時に、上記にあるユーザのコンテキストで 1 module を構成することを考えていたんですが、取り回しやすさを考えると、作品編集機能、購入履歴確認機能といった機能単位だなという結論に至っています。そのため app からひたすら切り出しです。
feature ディレクトリを作ってその中にモジュールのディレクトリを作成し、:feature:order のような名前空間をもって管理しています。Dynamic feature module で良いものは積極的に指定しますが、既存のアプリを移していくなら一旦は dist:onDemand="false"
指定でも良いと思います。
UI component module
汎用的なカスタム UI component, プロジェクトの strings.xml, dimens.xml といった UI resource を一括で管理するモジュールです。ここで一括管理することで参照元をばらけさせず管理の手間を省く目的です。
Drawable, layout に関しては共通のものしか置かないルールにして、ある機能でしか使わない CustomView は、それを使う feature module 内で持つようにしています。strings, dimens といった values を関係なく一括管理してるようにしてるのは、drawable たちに比べて管理する 1 単位が小さく、集約させたほうが似たような要素みたいなのも生まれずに管理が楽だろうという見通しです。
その他
画面遷移
Core module に抽象的に画面指定したら Intent を発行してくれる、以下のような内容のファイルを置き、クラス名の名前解決ができるようにすることで、モジュール間の画面遷移におけるクラスの参照問題を解決しています。
Class name を直書きしているせいで、画面クラスをパッケージ移動したら何もしないと普通に壊れます。直接すべての activity クラスを参照できる app module にこれに対するユニットテストを置き、アサーションの対象として activity クラスを直接指定することで変化に気付けるようにしてます。
Epoxy
Library module で epoxy を利用する場合に R クラスを用いたレイアウトの名前解決ができないなと思ってたんですが、README に書いてある通り butterknife plugin を用いて生成される R2 を用いると問題ないです。
https://github.com/airbnb/epoxy
依存関係の整理
https://blog.mataku.com/2019/04/29/mvp-architecture
上記にような取り組みや、static methods と依存関係満載の神ユーティリティクラス MinneUtilを適切な単位で切り出すことで、事前に不要な依存関係が入らないように予めモジュール分割前にリファクタリングを行っています。
パッケージ移動
9 割 Android Studio の move 機能を用いて移動しましたが、フリーズしてどうしようもないときがあり、そのときは mv(1) で対象のクラスを移動し sed(1) で対象パッケージ名を一括置換しました。
build.gradle
共通で使うであろう基幹ライブラリ、設定を毎回書かなくて済むように、必要なものは gradle ファイルとして切り出し、build.gradle 内で apply from: xxx.gradle すれば良いようにしています。
おわりに
app モジュール配下である程度ディレクトリ構成をきちんとしていたとしても、モジュール化されることでこの機能を実現するためのコンポーネントはここに存在していると分かることから、プロジェクト上で責務の認識の整理がよりされていく実感を得ています。
走り始めたばかりなので、Instant apps のようなユーザに最適なアプリを提供するというところはまだまだ未開拓です。どんどん開拓してやろうという方がおりましたらよろしくお願いします。