■イベント DroidKaigi https://droidkaigi.jp/2019/
■登壇概要 タイトル: マルチモジュールAndroidアプリケーション
登壇者: Sansan株式会社 Eight事業部 山本純平
▼Sansan Builders Box https://buildersbox.corp-sansan.com/
マルチモジュールAndroidアプリケーションDroidKaigi2019 day2 room2 15:40Jumpei Yamamoto
View Slide
山本純平Sansan株式会社Eight事業部 EngineeringgroupEight Androidアプリの開発「Kotlinイン・アクション」の翻訳に参加
山本純平twitter: @boohbahgithub: yamamotoj
Eight Android版• 40個のモジュールにて構成されたプロジェクト• この経験からアプリケーションのマルチモジュール化についてお話したいと思います。
Agenda• なぜマルチモジュールにするのか?• マルチモジュールでビルドを高速化する• アプリの設計とマルチモジュール化• マルチモジュールアプリケーションを実装する
モジュール• Androidのプロジェクト内でコンパイル単位を分割することができる• 分割したモジュール間での依存関係を定義できる• 循環した依存は定義できない
モジュールの種類Application ModuleAPKとしてリリース可能Library ModuleAARとしてリリース可能Dynamic Feature Moduleインストール後に追加できる
なぜマルチモジュールにするのか?
ビルドを高速化• マルチモジュールにすることでビルドを高速化することができる。• 後ほど詳しく説明します。
ソースコードの依存関係を強制できる• モジュール間の循環依存は許されないため、うまく使うことでソースコードの依存関係を強制することができる。• 後ほど詳しく説明します。
モジュール毎にテストを実行できる• モジュール毎にUnit Test, Android Testを独立に実行できる• テストの度に全体をビルドする必要がなく、必要なモジュールのみをビルドするので、実装→テストのサイクルを高速に回すことができる
複数のアプリケーションの管理を行えるproduct avorによる複数アプリケーションの管理flavorDimensions "sourceSet"productFlavors {demo {dimension "sourceSet"}production {dimension "sourceSet"}}• build variantをdemoにすると、productionのソースは管理外になってしまう• リファクタリングが効かない
複数のアプリケーションの管理を行える• すべてのコードでリファクタリングが適用される:app_demo:app_production:library_demo:library_production:library_common
複数のアプリケーションの管理を行える:app_demo:app_production:library_demo:library_production:library_common:app_instantinstant appとして一部機能を提供
Kotlinのinternal宣言• Kotlinで新たに追加された可視性修飾子• internalで宣言されたクラス、メソッド、プロパティなどは同じモジュール内からのみ参照可能• (packageではなく)モジュールを使った可視性の制御が可能になったinternal class Hoge { … }
Dynamic Feature Module• インストール時に使用しない機能を、必要になったタイミングで追加モジュールとしてダウンロードすることができる• インストール時に容量を削減:base_module:dynamic_moduleあとからダウンロード
まとめ• ビルドを高速化• モジュールごとにテストを実行できる• モジュールを使って複数アプリケーションの管理を行える• ソースコードの依存関係を強制できる• Kotlinではモジュールでのコードの可視性を定義できる• Dynamic Feature Moduleでインストール時の容量を削減できる
マルチモジュールによるビルドの高速化
マルチモジュールによるビルドの高速化• Android Gradle Plugin3.0以降の動作のしくみ• ビルド時間の計測実験• pluginやannotation processorがある場合の高速化テクニック• モジュール分割によって効果的に高速化する方法
Android Gradle Plugin 3.0以降の動作の仕組み
Android Gradle Plugin 3.0.0• マルチモジュールでのビルドが高速化• 依存関係の定義の方法が• 旧) compile• 新) implementation, api
compile指定(before 3.0)
compile指定 (before3.0)dependencies{compile: module1}dependencies{compile: module2}:app:module1:module2“compile”ͰґଘؔΛࢦఆ
compile指定 (before3.0)dependencies{compile: module1}dependencies{compile: module2}:app:module1:module2参照可能
compile指定 (before3.0)dependencies{compile: module1}dependencies{compile: module2}:app:module1:module2 มߋ
compile指定 (before3.0)dependencies{compile: module1}dependencies{compile: module2}:app:module1:module2 変更コンパイル
compile指定 (before3.0)dependencies{compile: module1}dependencies{compile: module2}:app:module1:module2 変更コンパイルコンパイル
implementation指定(after3.0)
implementation指定 (after3.0)dependencies{implementation: module1}dependencies{implementation: module2}:app:module1:module2“implementation”ͰґଘؔΛࢦఆ
implementation指定 (after3.0)dependencies{implementation: module1}dependencies{implementation: module2}:app:module1:module2参照可能
implementation指定 (after3.0)dependencies{implementation: module1}dependencies{implementation: module2}:app:module1:module2参照不可
implementation指定 (after3.0)dependencies{implementation: module1}dependencies{implementation: module2}:app:module1:module2 変更
implementation指定 (after3.0)dependencies{implementation: module1}dependencies{implementation: module2}:app:module1:module2 変更コンパイル
implementation指定 (after3.0)dependencies{implementation: module1}dependencies{implementation: module2}:app:module1:module2 変更コンパイルコンパイルされない
api指定(after3.0)
api指定 (after3.0)dependencies{api: module1}dependencies{api: module2}:app:module1:module2“api”ͰґଘؔΛࢦఆ
api指定 (after3.0)dependencies{api: module1}dependencies{api: module2}:app:module1:module2参照可能
api指定 (after3.0)dependencies{api: module1}dependencies{api: module2}:app:module1:module2 มߋ
api指定 (after3.0)dependencies{api: module1}dependencies{api: module2}:app:module1:module2 変更コンパイル
api指定 (after3.0)dependencies{api: module1}dependencies{api: module2}:app:module1:module2 変更コンパイルコンパイル
挙動の違いcompile(before3.0)api(after3.0)implementation(after3.0)モジュールの参照間接的に依存しているすべてのモジュールも参照できる直接依存しているモジュールのみが参照可能ビルドの伝播間接的に依存するすべてのモジュールのコンパイルが実行される直接依存するモジュールのみがコンパイルされる
Android Gradle Pluginまとめ• 依存関係の指定を”compile”から”implementation”にすることで無駄な再コンパイルを抑えることができる• “compile”と同等の挙動で使いたければ”api”指定を使う
ビルド時間の計測実験
シングルモジュール vs マルチモジュールでビルド時間の比較をしてみる• 1Activity + 2000クラス• 1クラスあたり6メソッド• dagger2によるAnnotation processing• @Injectアノテーション• それぞれのクラスに対応するFactoryとInjectorクラスがビルド時に生成される• ソースコードは以下に公開• https://github.com/yamamotoj/android_multi_module_experimentclass Foo00000 @Injectconstructor(){fun method0() {}fun method1() { method0() }fun method2() { method1() }fun method3() { method2() }fun method4() { method3() }fun method5() { method4() }}
モジュール構成の違う3種類のアプリを作ってビルドにかかる時間を比較• フルビルド• インクリメンタルビルド
ケース1: シングルモジュール• ひとつのモジュールにMainActivityと2000クラスが全て含まれている:appMainActivity2000class
ケース2: 直列マルチモジュール• :appモジュールにMainActivity• それ以外に4つのモジュールを持ちそれぞれに500クラスずつ含まれている• 各モジュールは直列でそれぞれが次のモジュールに依存する:app:module4:module3:module2:module1
ケース3: 並列マルチモジュール• :appモジュールにMainActivity• それ以外に4つのモジュールを持ちそれぞれに500クラスずつ含まれている• :app以外のモジュールは互いに依存することなく:appモジュールのみが各モジュールに依存している:app:module1 :module2 :module3 :module4
ビルド条件• macbook pro 2017 13inch• Core i7 (4core)• メモリ32GB• Android Studio 3.3• Gradle 5.1.1• Android Gradle Plugin 3.3.0• gradle.propertiesの設定• org.gradle.parallel = true• org.gradle.daemon = true• org.gradle.caching = true• kotlin.incremental.sePreciseJavaTracking = true
フルビルドで比較
フルビルド• それぞれのビルド毎にcleanしてビルドを実行• 5回試行した結果の平均をとる./gradlew assembleDebug —no-build-cache —scan
フルビルド時間(秒)Ϗϧυ࣌ؒʢඵʣγϯάϧϞδϡʔϧ ྻϚϧνϞδϡʔϧ ฒྻϚϧνϞδϡʔϧTTT
フルビルド時間(秒)Ϗϧυ࣌ؒʢඵʣγϯάϧϞδϡʔϧ ྻϚϧνϞδϡʔϧ ฒྻϚϧνϞδϡʔϧTTT 若干速い
ビルド時に何が起こったかを確認してみる• gradle build scan• build結果をgradleのサーバに送信し解析• 送信先は scans.gradle.org• build時に —scan オプションを追加し、利用規約に同意することで利用可能./gradlew assembleDebug —no-build-cache —scan
シングルモジュールのビルド結果36shttps://scans.gradle.com/s/5v3vnsh4lmepg
シングルモジュールのビルド結果:appモジュールのkapt stub作成6.8s36shttps://scans.gradle.com/s/5v3vnsh4lmepg
シングルモジュールのビルド結果:appモジュールのkapt stub作成6.8s :appモジュールのAnnotationProcessing8.4s36shttps://scans.gradle.com/s/5v3vnsh4lmepg
シングルモジュールのビルド結果:appモジュールのkapt stub作成6.8s :appモジュールのAnnotationProcessing8.4s:appモジュールのKotlinコンパイル7.0s36shttps://scans.gradle.com/s/5v3vnsh4lmepg
シングルモジュールのビルド結果:appモジュールのkapt stub作成6.8s :appモジュールのAnnotationProcessing8.4s:appモジュールのKotlinコンパイル7.0sclassからdexへの変換7.0s36shttps://scans.gradle.com/s/5v3vnsh4lmepg
直列マルチモジュールのビルド結果39shttps://scans.gradle.com/s/o4pvtp3dx5nvi
直列マルチモジュールのビルド結果:module1kapt stub作成 AnnotationProcessing Kotlinコンパイル9.3s39shttps://scans.gradle.com/s/o4pvtp3dx5nvi
直列マルチモジュールのビルド結果:module1kapt stub作成 AnnotationProcessing Kotlinコンパイル9.3s:module2kapt stub作成 AnnotationProcessing Kotlinコンパイル7.0s39shttps://scans.gradle.com/s/o4pvtp3dx5nvi
直列マルチモジュールのビルド結果:module1kapt stub作成 AnnotationProcessing Kotlinコンパイル9.3s:module2kapt stub作成 AnnotationProcessing Kotlinコンパイル7.0s:module3kapt stub作成 AnnotationProcessing Kotlinコンパイル7.0s39shttps://scans.gradle.com/s/o4pvtp3dx5nvi
直列マルチモジュールのビルド結果:module1kapt stub作成 AnnotationProcessing Kotlinコンパイル9.3s:module2kapt stub作成 AnnotationProcessing Kotlinコンパイル7.0s:module3kapt stub作成 AnnotationProcessing Kotlinコンパイル7.0s:module4kapt stub作成 AnnotationProcessing Kotlinコンパイル6.9s39shttps://scans.gradle.com/s/o4pvtp3dx5nvi
並列マルチモジュールの結果31shttps://scans.gradle.com/s/dhvaghy7in2vq
並列マルチモジュールの結果:module1-4のkapt stub作成約4.7s31shttps://scans.gradle.com/s/dhvaghy7in2vq
並列マルチモジュールの結果:module1-4のkapt stub作成約4.7s :module1-4のAnnotationProcessing約8.4s31shttps://scans.gradle.com/s/dhvaghy7in2vq
並列マルチモジュールの結果:module1-4のkapt stub作成約4.7s :module1-4のAnnotationProcessing約8.4s:module1-4のKotlinコンパイル約5.4s31shttps://scans.gradle.com/s/dhvaghy7in2vq
シングル vs 並列シングルモジュールのビルド並列マルチモジュールのビルド36s31s
フルビルドの結果• モジュール分割により、コンパイルやAnnotationProcessingの順序は大きく変わる• コンパイルやAnnotation Processingを並列に実行することでビルド速度は若干向上• シングルモジュールでもコンパイルやAnnotationProcessingの並列処理はなされており、マルチモジュールで並列度を上げたところで劇的に速度が向上するわけではない
インクリメンタルビルドで比較
インクリメンタルビルド• それぞれのプロジェクトでソースコードを一行だけ変更して、ビルド時間を比較する• 5回試行した結果の平均をとる./gradlew assembleDebug —scanclass Foo00000 @Injectconstructor(){fun method0() {}fun method1() { method0() }fun method2() { method1() }fun method3() { method2() }fun method4() { method3() }fun method5() { method4() }fun method(){}}クラスにメソッドを追加したり削除したりしてDaggerのAnnotation Processingが動作するように
変更箇所: シングルモジュール• :appモジュールのコードを一行変更:appMainActivity2000class
変更箇所: 直列マルチモジュール• 依存の最も深い :module1のコードを一行変更する:app:module4:module3:module2:module1
変更箇所: 並列マルチモジュール• :appモジュールが依存する:module1を一行変更:app:module1 :module2 :module3 :module4
インクリメンタルビルド時間ビルド時間(秒)シングルモジュール 直列マルチモジュール 並列マルチモジュール7s10.2s14.2s
シングルモジュールのビルド14.8shttps://scans.gradle.com/s/32k6o3avjen7i
シングルモジュールのビルド14.8s:appモジュールのkaptstub作成2.9shttps://scans.gradle.com/s/32k6o3avjen7i
シングルモジュールのビルド14.8s:appモジュールのkaptstub作成2.9s:appモジュールのAnnotationProcessing8.3shttps://scans.gradle.com/s/32k6o3avjen7i
シングルモジュールのビルド14.8s:appモジュールのkaptstub作成2.9s:appモジュールのAnnotationProcessing8.3s:appモジュールのKotlinコンパイル1.3shttps://scans.gradle.com/s/32k6o3avjen7i
直列マルチモジュールのビルド10shttps://scans.gradle.com/s/v2xy6oultdawm
直列マルチモジュールのビルド10shttps://scans.gradle.com/s/v2xy6oultdawm:module1 kapt stubの作成 Annotationprocessing3.7s
直列マルチモジュールのビルド10shttps://scans.gradle.com/s/v2xy6oultdawm:module2 kapt stubの作成 Annotationprocessing3.7s:module1 kapt stubの作成 Annotationprocessing3.7s
直列マルチモジュールのビルド変更されたモジュールと直接依存しているモジュールのみが再コンパイル+Annotation processing されている:app:module4:module3:module2:module1:module1 3.7s:module2 3.7s
並列マルチモジュールのビルド7.4s
並列マルチモジュールのビルド:module1 kapt stubの作成 Annotation processing4.3s7.4s
並列マルチモジュールのビルド:module1 kapt stubの作成 Annotation processing4.3s:app Annotationprocessing1.2s7.4s
並列マルチモジュールのビルド:app:module1 :module2 :module3 :module4:app 1.9s:module1 4.3s:appモジュールの AnnotationProcessingが再実行
Google I/O 2017でのお話• Google I/O 2017 speeding up your build• https://www.youtube.com/watch?v=7ll-rkLCtyk
Google I/O 2017でのお話• JavacはインクリメンタルだがAnnotation processingはインクリメンタルなビルドには対応していない• Annotation processingを使っている場合は一つのクラスを変更しただけでも全てを再コンパイルしなければならない:app
Google I/O 2017でのお話• 小さなモジュールに分割することでAnnotation processingの実行範囲を最小限におさえることができる:app:module1:module2
インクリメンタルビルドの結果• 変更されたモジュールとそれを直接参照しているモジュールのみのAnnotation processingとコンパイルが実行される• Annotation processingは実行時に影響するモジュール全体で再実行されるため、影響するもモジュールが小さいほうがビルドが速い
特殊なpluginがある場合の高速化テクニック
• モバイルアプリ向けデータベース、ORM• Androidでも利用可能
Realmのオブジェクト定義public class Article extends RealmObject {@PrimaryKeypublic String id;public String content;}JAVA
Realmのオブジェクト定義public class Article extends RealmObject {@PrimaryKeypublic String id;public String content;}JAVA永続化されるデータをJavaのフィールドとして定義
RealmObjectのデータ取得Article article = realm.where(Article.class).equals(“id”, “1”).findFirst();String content = article.content;JAVA
RealmObjectのデータ取得Article article = realm.where(Article.class).equals(“id”, “1”).findFirst();String content = article.content;JAVAこの時点では空のオプジェクトのみが生成され、データはロードされていない
RealmObjectのデータ取得Article article = realm.where(Article.class).equals(“id”, “1”).findFirst();String content = article.content;JAVAフィールドにアクセスされた時点でcontentのデータが遅延ロードされる
なぜこんなことが可能か?public class Article extends ReamObject {@PrimaryKeypublic String id;public String content;}JAVA
なぜこんなことが可能か?public class Article extends ReamObject {@PrimaryKeypublic String id;public String content;public String realmGet$content() {return row.getString(2);}/* ... */}JAVAコードの追加
なぜこんなことが可能か?String content = article.content;JAVA
なぜこんなことが可能か?String content = article.content;JAVAString content = article.realmGet$content();JAVA呼び出し元のコードを書き換え
Realmのコード生成 + 書き換え• RealmObjectの定義クラスに、データベースから値をロードするメソッドを追加• RealmObjectのフィールドを参照している箇所を、データベースから値をロードするコードに書き換え
Realmのコード生成 + 書き換え• RealmObjectの定義クラスに、データベースから値をロードするメソッドを追加• RealmObjectのフィールドを参照している箇所を、データベースから値をロードするコードに書き換えBytecode weaving
Bytecode weaving• javaファイルのコンパイル後、classファイルのバイトコードを書き換える.java.kt.classόΠτίʔυͷॻ͖͑
realm gradle pluginbuildscript {dependencies {classpath "io.realm:realm-gradle-plugin:5.8.0"}}ϧʔτͷbuild.gradleapply plugin: ‘com.android.application’apply plugin: ‘kotlin-android’apply plugin: ‘realm-android’:appͷbuild.gradle
realm gradle pluginbuildscript {dependencies {classpath "io.realm:realm-gradle-plugin:5.8.0"}}ϧʔτͷbuild.gradleapply plugin: ‘com.android.application’apply plugin: ‘kotlin-android’apply plugin: ‘realm-android’:appͷbuild.gradle ここでpluginを指定することでBytecodeweavingが実行される
モジュールの視点で考える• RealmObjectにメソッドを追加• RealmObjectのフィールドを参照している箇所をデータをロードするメソッドに書き換える:appapply plugin: 'realm-android'
モジュールの視点で考える• RealmObjectにメソッドを追加• RealmObjectのフィールドを参照している箇所をデータをロードするメソッドに書き換える:app参照箇所の検索に時間がかかるapply plugin: 'realm-android'
モジュールの切り出しで高速化
STEP1: Realm専用のモジュールを作成し、Realm関連のファイルを移動:app
STEP1: Realm専用のモジュールを作成し、Realm関連のファイルを移動:app:realm
STEP2: RealmObjectの定義クラスにgetter/setterを定義し、フィールドをprivateにする:realmpublic class Article extends ReamObject {@PrimaryKeypublic String id;public String content;}:app
STEP2: RealmObjectの定義クラスにgetter/setterを定義し、フィールドをprivateにする:realmpublic class Article extends ReamObject {@PrimaryKeyprivate String id;private String content;public Strig getId(){return id;}public void setId(String id){this.id = id;}.. ..getter/setterを定義:app
STEP2: RealmObjectの定義クラスにgetter/setterを定義し、フィールドをprivateにする:realmpublic class Article extends ReamObject {@PrimaryKeyprivate String id;private String content;public Strig getId(){return id;}public void setId(String id){this.id = id;}.. ..データベースからロードするメソッドへの書き換えはgetter/setterの内部のみに限られる:app
STEP3: realm-pluginの指定をRealm専用モジュールに限定:realmapply plugin: ‘com.android.application’apply plugin: ‘realm-android’:appͷbuild.gradle:app
STEP3: realm-pluginの指定をRealm専用モジュールに限定:realmapply plugin: ‘com.android.application’apply plugin: ‘realm-android’:appͷbuild.gradleapply plugin: ‘com.android.library’apply plugin: ‘realm-android’:realmͷbuild.gradle:app
Bytecode weavingが適用される範囲:app:app:realmbefore after
Eightアプリのフルビルドにかかる時間:realmbefore after6m46s 4m27s29%減少
まとめ• ビルド時に実行する特殊なpluginがある場合に、そのpluginを必要とする部分だけモジュールとして切り出しすことで、ビルド時にかかる時間を最小限に抑えることができる• pluginだけでなくannotation processorが必要なライブラリでも同様
効果的にビルドを高速化する
再掲: インクリメンタルビルドの結果• 変更されたモジュールとそれを直接参照しているモジュールのみのAnnotation processingとコンパイルが実行される• Annotation processingは実行時に影響するモジュール全体で再実行されるため、影響するもモジュールが小さいほうがビルドが速い
再掲: インクリメンタルビルドの結果• 変更されたモジュールとそれを直接参照しているモジュールのみのAnnotation processingとコンパイルが実行される• Annotation processingは実行時に影響するモジュール全体で再実行されるため、影響するもモジュールが小さいほうがビルドが速いモジュールの依存関係の構造によってビルドの速度が変わってくる
ビルドが高速化しない構造
大きなモジュールが依存• サイズの大きなモジュールが小さなモジュールに依存している• :module1が変更されてもサイズが大きいのでビルドは遅い• :module2が変更されても:module1の再ビルドが走ってしまう:module1:module2
被リンクモジュールの数が多い• 依存元モジュールの数が多いほどビルドに時間がかかる• めったに変更しないフレームワーク的なモジュールやユーティリティならあり:module1:module2:module3:module4:module5:module6
プロジェクト内の依存構造がスパゲッティ• 依存関係が多く存在しているプロジェクトは、いくらモジュール分割をしてもビルドは効果的に高速化しない:module1:module2:module3:module4:module5:module6
ビルドの高速化に効果的な構造
小さなモジュールが依存している:small:module1
依存元が1つ• 依存元が1つの構造であればその関係が増えても局所的な再コンパイルで済む:module4 :module6 :module9:module5 :module7:module3:module2 :module8:modle1:small
シンプルな依存関係の構造• 依存元のモジュールが一つであれば、再コンパイルは最小限に抑えられる:module1:module2:module3:module4:module5:module6:module7
効果的なビルドの高速化まとめ• モジュール分割によって効果的にビルドを高速化するためにはモジュール間の依存関係をシンプルに保ち、できるだけ少ない(一つの)モジュールからのみ依存される構造を作り出すことが重要
もう一つ、ビルドの高速化で重要なこと• Android Studio, gradle, kotlin pluginなどは常に最新のをものをつかう• ビルド時のマルチモジュールの最適化は日々進化• 並列ビルド• ビルドキャッシュ• 不要な処理の最適化
Gradle 4.7のリリースノート• Incremental annotation processingをサポート• https://docs.gradle.org/4.7/release-notes.html?_ga=2.231409431.904362546.1523996697-559636575.1501515251#incremental-annotation-processing
dagger2.18のリリースノート• https://github.com/google/dagger/releases/tag/dagger-2.18
kaptはまだ未対応(issueのみ)• Let’s vote!• https://youtrack.jetbrains.com/issue/KT-23880
アプリの設計とマルチモジュール化
モジュール分割の方向性• 水平方向の分割• レイヤーごと• 垂直方向の分割• 機能ごと
モジュール分割の方向性• 水平方向の分割• レイヤーごと• 垂直方向の分割• 機能ごとプレゼンテーション層ビジネスロジック層データ層
モジュール分割の方向性• 水平方向の分割• レイヤーごと• 垂直方向の分割• 機能ごと機能1 機能2 機能3
水平方向のモジュール分割
Architecting Android...The clean way?https://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/
アプリケーションを3つの層に分割• プレゼンテーション層• ドメイン層• データ層プレゼンテーション層ドメイン層データ層
プレゼンテーション層• ビューやアニメーションに関するロジック• Activity, Fragment• MVP, MVVMプレゼンテーション層ドメイン層データ層
ドメイン層• ビジネスルールに関するロジック• Androidに依存しない純粋なJava(Kotlin)のモジュール• 他のレイヤと接続するためにはインターフェイスを用いるプレゼンテーション層ドメイン層データ層
データ層• アプリケーションが必要とするデータを提供• ドメイン層のインターフェイスに基づきリポジトリパターンで実装プレゼンテーション層ドメイン層データ層
クリーンアーキテクチャ• 円の内側にドメインモデル• 円の外側にUI、データベース• ソースコードの依存性は内側にだけ向かっていなければならない
レイヤ構造をモジュールで表現:app:domain:data
依存関係:app:domain :dataドメイン層はAndroidに依存しないデータ層はドメイン層のインターフェイスを使用
依存関係:app:domain :dataUIはdomainのみに依存するべき
依存関係:ui:domain:data:app
依存関係:ui:domain:data:app:app͔ΒUIؔ࿈ͷίʔυΛಠཱ
依存関係:ui:domain:data:app:app͔ΒDIʹΑͬͯ:domainͷΠϯλʔϑΣΠεʹରͯ͠:dataͷ࣮Λೖ
水平方向のモジュール分割まとめ• 一般的なAndroidのレイヤードアーキテクチャをモジュールに当てはめて、上位モジュールが下位モジュールに依存しないことを保証できる• プレゼンテーション層のモジュールを :app モジュールから分離し、DIを導入することによってより独立性を保つモジュールの構造を作り出すことができる
垂直方向のモジュール分割
垂直方向のモジュール分割• 機能ごとにモジュールを分割する機能1 機能2 機能3
Eightの例メイン画面 投稿詳細画面 人物詳細画面
Eightの例メイン画面 投稿詳細画面 人物詳細画面:component_main:component_post_detail:component_person_detail
機能別にモジュール分割するメリット• 機能別にコードがまとまっているので理解しやすい• 修正の影響範囲を最低限にまとめることができるメイン画面 投稿詳細画面 人物詳細画面:component_main:component_post_detail:component_person_detail
機能別モジュールの依存関係投稿詳細画面 人物詳細画面
機能別モジュールの依存関係投稿詳細画面 人物詳細画面人物をタップして詳細を開く
機能別モジュールの依存関係投稿詳細画面 人物詳細画面その人物の投稿
機能別モジュールの依存関係投稿詳細画面 人物詳細画面その人物の投稿をタップして詳細を開く
機能別モジュールの依存関係投稿詳細画面 人物詳細画面循環依存
依存関係が循環依存するケース投稿詳細画面 人物詳細画面:component_post_detail:component_person_detail人物詳細画面の起動投稿詳細画面の起動
相互のIntentを取得するインターフェイスを共通のモジュールで定義投稿詳細画面 人物詳細画面:component_post_detail:component_person_detailinterface IntentResolver{fun getPersonDetailActivityIntent(): Intentfun getPostDetailActivityIntent(): Intent}:common
app:モジュールにて実装クラスを定義:component_post_detail:component_person_detailclass IntentResolverImpl: IntentResolver {override fun getPersonDetailActivityIntent():Intent = Intent(…)override fun getPostDetailActivityIntent():Intent = Intent(…)}app:お互いのモジュールを参照
DIにて解決:component_post_detail:component_person_detailapp::commoninterface IntentResolver{}class IntentResolverImpl: IntentResolverinject inject
Eightの各機能の依存関係:component_main:component_post_detail:component_person_detail:component_create_post:component_chat:component_my_page:component_camera:component_search:component_on_boarding
DIを導入• 循環依存を解決• 各機能の依存関係を atに配置• 各機能間で本当に必要な連携のみを抽出できる• ビルド速度面でもメリット:component_main:component_post_detail:component_person_detail:component_create_post:component_chat:component_my_page:component_camera:component_search:component_on_boarding:app
垂直方向の機能分割まとめ• 機能別にモジュールを分割することによって、コードの影響範囲を最小限にとどめることができる• 機能間の連携に必要なAPIをDIをつかって外部から注入することによって、各機能間で直接依存関係をもつことなく、互いに疎な関係を保つことができる
Eightの例メイン画面 人物詳細画面:component_main:component_post_detail:domain_main:repository_main:domain_post_detail:repositorypost_detail:app
既存のアプリをマルチモジュール化する
シングルモジュールのアプリ:app
既存のモジュールの分割• yak shaving• ある問題を解こうと思ったら別の問題が出てきて、それを解こうと思ったらさらに別の問題が出てきて…ということが延々と続く状況
詳しくは…
細かくアプリを切り出す?• モノリシックなアプリからモジュールを切り出していくのは大変• モジュールとして切り出してもビルドの高速化には寄与しないかも:app:module :module :module
Realmを使用している場合• realmを使用している場合は、RealmObjectの定義クラスをモジュールとして切り出すことでビルドが高速化する:app:realm
まず最小限のApplicationモジュールとそれ以外のコードを切り離す• 最小限のコードを残して、全ての実装を別モジュールに移動する• :appモジュールにはApplication classとApplication scopeのDI Component:legacy:app
:appモジュールの主な機能• Applicationクラス• サブモジュール間の依存関係を解決する• ドメイン層を介したプレゼンテーション層とデータ層の実装の解決• 各機能間の連携を抽象化して解決• サブモジュールのビルドが速くなるようにできるだけ軽く:legacy:app
dagger2を使用してるケースclass MainActivity : AppCompatActivity(){override fun onCreate(savedInstanceState: Bundle?){super.onCreate(savedInstanceState)(application as MyApplication).component.mainActivityComponentBuilder().build().inject(this)}}
dagger2を使用してるケースclass MainActivity : AppCompatActivity(){override fun onCreate(savedInstanceState: Bundle?){super.onCreate(savedInstanceState)(application as MyApplication).component.mainActivityComponentBuilder().build().inject(this)}}ApplicationclassΛࢀর͍ͯ͠Δ
:legacy:appApplication, Activity, Componentの関係MyApplicationAppComponent MainActivityComponentMainActivity
:legacy:appApplication, Activity, Componentの関係MyApplicationAppComponent MainActivityComponentMainActivity循環参照
Dagger2 Android SupportMyApplicationAppComponent MainActivityComponentMainActivityこの参照を断ち切ることができる
:legacy:appdagger2 Android supportMyApplicationAppComponentMainActivityComponentMainActivityMainActivityBindingModule
シンプルな:appモジュールの分離:legacy:app
シンプルな:appモジュールの重要性
複数アプリケーションの管理を行うために• 機能を含まず、アプリの構成を決定するためのモジュールが必要:app_demo:app_production:library_demo:library_production:library_common
クリーンアーキテクチャを保証するために• 依存関係を解決するための上位モジュールが必要:ui:domain:data:app
分割した機能の連携を解決するために• 連携の解決をするための上位モジュールが必要:component_main:component_post_detail:component_person_detail:component_create_post:component_chat:component_my_page:component_camera:component_search:component_on_boarding:app
:appモジュールを分離したあとは?:legacy:app
:appモジュールを分離したあとは?:app• 最低限必要となる共通ライブラリのモジュールは切り出す:legacy:common
新機能だけを別モジュールとして実装• 新機能の開発時はビルドが早くて快適• できるだけ:legacyモジュールには触らないのがベター:legacy:app:new_feature:common
既存のアプリをマルチモジュール化するまとめ• まず、最小限の実装のみを含む:appモジュールとそれ以外に分割する• dagger2を使っている場合はAndroid supportの機能を使ってApplicationクラスと各Activityとの循環依存を解決する• 既存のモジュールの分割はつらいので、できるだけ触らないで進める方法を考える
まとめ• Annotation processingが存在する場合、モジュール間の依存関係を適切に設定することによってマルチモジュール化によってビルドを高速化することができる• Incremental annotation processingの実装によってマルチモジュールはビルドの高速化に対してあまり意味を持たなくなるかも• 水平方向、垂直方向にモジュールを分割することによってレイヤの依存関係を強制し、関心事を分離することができる• 既存のアプリケーションをマルチモジュール化する場合はまず最小限の機能のみをもつトップの:appモジュールを分離することを心がける
ご清聴ありがとうございました