はじめに
Android×Gradle×MultiModule 構成において、Compsite Build を使用してビルドロジックを共通化したときのメモです。
アプリの構成
機能毎の縦割りとアーキテクチャレイヤー毎の横割りを想定しています。
app は全ての feature に依存し、各 feature は core モジュールに依存するあるあるのやつですね。
- app
- feature/feature1
- feature/feature2
- feature/feature3
- core/resource
- core/repository
- core/database
- core/network
導入準備
build-logic ディレクトリを作成
プロジェクトトップにbuild-logic
というフォルダを作成します。Android Studio で「New」→「Directory」で作成します。任意の名前で良さそうです。
build-logic ディレクトリを読み込む
プロジェクトトップレベルのsettings.gradle
に以下を追加します。引数には作成したフォルダの名前を指定します。
pluginManagement {
+ includeBuild("build-logic")
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
build-logic の settings.gradle を作成
/build-logic/settings.gradle
を作成します。
後ほど作成するconvention
ディレクトリやversion catalog
のファイルの読み込みを設定します。
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
include(":convention")
version catalog のファイルを作成
libs.versions.toml
を作成します。toml 内の定義順や書き方はいくつかありますが、基本的には takahirom さんのgradle-version-catalog-converterを使用すると良さそうです。
[versions]
androidGradlePlugin = "7.2.1"
kotlin = "1.7.0"
...
[libraries]
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
...
Plugin の読み込み
plugin はconvention
ディレクトリの下に作成します。前述の/build-logic/settings.gradle
で設定した名前です。
基本の記述は以下です。
plugins {
`kotlin-dsl`
}
group = "xxx"
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
implementation(libs.android.gradlePlugin)
implementation(libs.kotlin.gradlePlugin)
}
gradlePlugin {
plugins {
// 作成する独自Plugin(後述)を記述していく
}
}
ここまでで Composite Build を使用してビルドロジックをまとめるための下準備ができました。
あとはそれぞれやりたいことに応じて独自 Plugin を作成していく流れになります。
ライブラリモジュールのビルドロジックをまとめる。
ビルドロジックを作成する
AndroidLibraryConventionPlugin.kt
を作成します。configureKotlinAndroid
という関数で一部ロジックを分離しています。この辺りはnowinandroidを参考にしています。
import com.android.build.gradle.LibraryExtension
import xxx.configureKotlinAndroid
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
class AndroidLibraryConventionPlugin: Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.library")
apply("org.jetbrains.kotlin.android")
}
extensions.configure<LibraryExtension> {
configureKotlinAndroid(this) // 後述
defaultConfig.targetSdk = 32
}
}
}
}
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.plugins.ExtensionAware
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
internal fun Project.configureKotlinAndroid(
commonExtension: CommonExtension<*, *, *, *>
) {
commonExtension.apply {
compileSdk = 32
defaultConfig {
minSdk = 26
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
}
fun CommonExtension<*, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) {
(this as ExtensionAware).extensions.configure("kotlinOptions", block)
}
作成したビルドロジックを登録する
id はこの後使用します。implementationClass で作成したクラス名を記述します
gradlePlugin {
plugins {
+ register("androidLibrary") {
+ id = "androidsampleapp.android.library"
+ implementationClass = "AndroidLibraryConventionPlugin"
}
}
}
作成したビルドロジックを使用する
plugins ブロックで指定した ID を追加します。あとは定義済みの sdk 関連の値や option 関連もすべて削除できます。
plugins {
- id 'com.android.library'
- id 'org.jetbrains.kotlin.android'
+ id "androidsampleapp.android.library"
}
android {
- compileSdk 32
defaultConfig {
- minSdk 26
- targetSdk 32
...
}
...
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
- kotlinOptions {
- jvmTarget = '1.8'
- }
}
Hilt 依存のビルドロジックをまとめる。
モジュラーモノリスなマルチモジュール構成だと、各モジュールに hilt のプラグインの設定と依存を追加する必要があります。ここではそれらの処理を一か所にまとめる Plugin を作成します。
Plugin を作成する
Hilt で必要な Plugin を追加します。
次に Hilt で必要なライブラリを version catalog から取得して dependencies ブロックでそれぞれ追加します。
class AndroidHiltConventionPlugin: Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("kotlin-kapt")
apply("com.google.dagger.hilt.android")
}
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
add("implementation", libs.findLibrary("com.google.dagger.hilt.android").get())
add("kapt", libs.findLibrary("com.google.dagger.hilt.compiler").get())
}
}
}
}
Plugin を登録する
こちらは他と同様です。
gradlePlugin {
plugins {
+ register("androidHilt") {
+ id = "androidsampleapp.android.hilt"
+ implementationClass = "AndroidHiltConventionPlugin"
+ }
}
}
Plugin を使用する
変更は下記のようになります。
plugins {
...
- id 'kotlin-kapt'
- id 'com.google.dagger.hilt.android'
+ id "androidsampleapp.android.hilt"
}
android {
...
}
dependencies {
- implementation "com.google.dagger:hilt-android:2.44"
- kapt "com.google.dagger:hilt-compiler:2.44"
}
まとめ
今回は Composite Build を使ってマルチモジュールのビルドロジックを共通化してみました。
一度ビルドロジックを共通化できれば、ガチガチのマルチモジュールプロジェクトにおいては大きな恩恵を受けることができると思います。各build.gradle
もかなりシンプルになり見やすくなると思います。
ただし何でもそうですが、やりすぎは NG かと思います。例えば一例として紹介した、「Hilt 依存のビルドロジックをまとめる」ですが、依存関係がビルドロジックに吸収されており、モジュール内のdependencies
ブロックには現れません。これは以下のようなデメリットも発生しうるかと思います。
- 各モジュールが何に依存しているか、ぱっと見で分かりずらい
- 新たに Hilt 関連でライブラリを追加するモジュールがある場合、プラグインとして全体に追加するか、利用するモジュールだけに追加するか問題
これは結局普通のプログラムと同じで、無駄に共通化しまくると後々大変になるっていうやつです。プロジェクトの大きさやメンバーの理解度に応じて、適切に使用していきましょう。
おまけ
今回の記事作成に当たり、Gradle の公式ドキュメントだけでは理解できない部分もあり、 Now in Android のアプリを参考にしました。
例えば Now In Android では Compose 関連の依存をcore-ui
に api で定義しています。各 Feature モジュールはcore-ui
に依存することで compose の依存を引継いで利用しているようです。
Composite Build とはまた違いますが、複数のモジュールで同じ依存を書いていないという点で良い感じにビルドロジックをまとめています。
綺麗だなと少し思いつつ、やっぱり api は依存関係が汚れがちなのであまり使いたくないなとも思っちゃいます。