Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

大規模なAndroidアプリにおけるマルチモジュールの導入事例

ミラティブのAndroidエンジニアの chocomelon です。

MirrativのAndroidアプリでは、Androidアプリケーションの内部品質向上のために開発で以下を取り入れています。

  • データフローの単方向化(Flux導入)
  • Viewからのロジック切り離し(Flux、Jetpack Compose導入など)
  • 責務分割、依存方向の強制(Flux、マルチモジュール導入など)

今回の記事では特にマルチモジュールについてお話します。

MirrativのAndroidアプリは、配信/視聴機能、アバター機能、リッチなアニメーションなど複雑な要素を一つの画面で表現することが多々あります。 これらを考えなしに実装すると、可読性の悪化を招いたり、不具合が発生しやすく調査しづらいコードにしてしまいがちです。

複雑なアニメーションやアバター機能などを持つ配信画面

ミラティブでは多くのメンバーがネイティブアプリケーションのコードを触ります。バックエンドエンジニアが触ることもありますし、最近ではインターン生や業務委託のメンバーが増え、多くのメンバーが関わっています。そのため、コードの可読性を維持・向上すること、不具合が発生したときに影響範囲を限定できることなどが開発速度を向上させるためにクリティカルに影響すると感じています。

以下では、実際にどういうモチベーションで、どのようにマルチモジュールを進めていったかをお話します。

新規開発部分をコンテキストごとのfeatureモジュールを作成し分割

MirrativのAndroidアプリでは、初手からキレイにモジュール分割するのは困難でした。 Fluxを導入してデータフローの単方向化などの取り組み(下記記事)で、ある程度処理は追いやすくなってきたものの、ViewのところどころにUtilやHelperの参照やロジックがベタ書きされていたり、既存のものを組み替えるのはかなり骨が折れそうでした。

tech.mirrativ.stream

Mirrativでは、「〇〇周年イベント」「〇〇キャンペーン」などサイクルが短い新規開発も多く、その新規開発をするときに他の影響を気にせず開発できるところから始めようということで、元々あったmirrorman(当時の開発コードネーム)というモノリスのモジュールとは別に、新規開発部分をfeatureモジュールで開発する構成からはじめました。

featureモジュールはある程度のコンテキスト単位でまとめていて、例えば配信系の機能であればstreamモジュール、視聴系であればplayerといったように作成しています。新規開発時に必要であれば新規にモジュールを作成し、既存のmirromanやfeatureモジュールから参照されない形で開発を行いました。

画面遷移はrouterモジュールを介して行う

画面遷移などで、別のfeatureモジュールのActivityに遷移したい場合は、routerを介して起動を行います。

routerを使ってモジュール間の画面遷移を実現

具体的にはrouterモジュールにIntentResolverというInterfaceを持ちます

// routerのIntentResolver
interface IntentResolver {
    fun createFeature1Activity(context: Context): Intent
    fun createFeature2Activity(context: Context): Intent
}

appモジュールがrouter、mirrorman、featureモジュールの参照を持ち、各モジュールで参照しているIntenteResolverのInterfaceに対して実装(IntentResolverImpl)を注入します。

// appのbuild.gradle
dependencies {
    implementation project(':router')
    implementation project(':mirrorman')
    implementation project(':feature1')
    implementation project(':feature2')
}
// appのIntentResolverImpl
// すべてのfeatureモジュールの参照があるため各モジュールのActivityなどの参照が可能
class IntentResolverImpl() : IntentResolver {
    fun createFeature1Activity(context: Context): Intent = Intent(context, Feature1Activity::class.java)
    fun createFeature2Activity(context: Context): Intent = Intent(context, Feature2Activity::class.java)
}

mirrormanやfeatureモジュールはDIで注入されたIntentResolverの実装を使い、画面遷移を実現します。

// あるモジュールでfeature1のActivityに遷移する場合
class HogeActivity: AppCompatActivity() 
    private val intentResolver: IntentResolver by inject()
    ...
    fun onClickMoveFeature1Button() {
        startActivity(intentResolver.createFeature1Activity(tc))
    }
}

※以上のIntentResolverの実装はSansanさんの山本さんのスライドを参考にしています🙏

余談ですが、先ほどrouterのために示した依存の図ですが、featureモジュールがmirrormanへの参照を持っています。featureモジュールからmirrormanへの参照も極力避けたかったのですが、mirrormanにある共通処理を既存機能からはがして別モジュールに分割するのが即時にできなかったため、一旦mirrormanへの依存がある形で分割を始めています。mirromanへの依存は残っていますが、新規開発はfeatureモジュールに分割するだけでも他モジュールからの参照がなくなり考えることが減るため、以後の開発はやりやすくなりました。

再利用性の高いComponentをJetpack Composeで再実装

新規のfeatureモジュールの開発を高速化させるために、今まで作ったComponentは再利用したいですし、そのためにComponent自体の再利用性を高くすることが必要でした。 直近ミラティブでは、新規画面の開発を高速化させるためにデザイナーチームと協力して、Styleの共通化やComponentの共通化を進めています。

デザイナーチームと協力してやっている共通化、既存のComponentが良い感じにつかってしまっているUtilやHelperを参照削除、Componentにロジックが書かれている箇所を剥がしたいことも相まって、新規で作成する画面についてはなるべくJetpack Composeで新規実装、あるいは既存実装をJetpack Composeで置き換えたComponentを利用することにしました。

以下記事でも紹介していますが、今後旧Android Viewがメンテナンスモードと言われており、Jetpack Composeへの移行も必要だったことからちょうど良いタイミングでした。 tech.mirrativ.stream

componentを追加する

マルチモジュール化の現状

現状では以下のような形になっています。(非常に見づらくて申し訳ないです)

※依存グラフの吐き出しにはJakeWhartonさんのスクリプトをお借りしています🙏

基本的にはある程度のコンテキストでfeatureモジュールを作成し、また、customview、api(client)、loggerなど共通で使うモジュールを作成しています。 歴史的経緯で一部不自然なコンテキストの切り方をされているものもありますが、長くなりそうなので割愛します😇

今の課題は元々モノリス部分だったmirromanがまだまだ巨大、かつ各featureモジュールからmirrormanへの参照があることです。

  • mirromanが巨大かつ共通部分をまだまだ持っているため、結局mirrormanに支配的な処理が残りがち
  • mirrormanに変更が入るたびに参照しているすべてのfeatureモジュールで再ビルドが必要

これらを解消するために新規開発で共通に使う部分は積極的に引き剥がしています。

まとめ

大規模、かつ複雑なモノリスアプリケーションでのマルチモジュール化の事例について紹介しました。 まだまだ理想的な形ではないですが、クォーターごとにある程度分割の方針を立て、日々改善しています。

個人的にはマルチモジュール化しなくて良いならしない方が楽だと思います。 相当成熟したチームでない限り、分割時の議論やGradleのメンテなどである程度時間を要してしまう可能性があります。

MirrativのAndroidアプリでは、プロジェクトの規模や関わるメンバーを総合的に判断した結果分割する方針にしました。 キレイに分割することが目的ではなく、ある程度可読性の向上と影響範囲を限定できれば良いので、今のところは垂直/水平方向をガチガチにモジュールに切るところまでは考えていません。

まずはできるところから、メリットを感じるところから徐々に改善しつづけていきたい気持ちです。 少しでも参考になった方が居ますと幸いです。私も色々な会社のマルチモジュール化の事例参考にさせていただきました、ありがとうございます!

We are hiring!

ミラティブでは一緒に開発する仲間を募集中です!TwitterのDMなどでお気軽にご連絡ください! www.mirrativ.co.jp

speakerdeck.com