(Android)Composite Build でビルドロジックをまとめる

# はじめに

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`に以下を追加します。引数には作成したフォルダの名前を指定します。

```diff_gradle:/settings.gradle
pluginManagement {
+   includeBuild("build-logic")
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}
```

### build-logic の settings.gradle を作成

`/build-logic/settings.gradle`を作成します。
後ほど作成する`convention`ディレクトリや`version catalog`のファイルの読み込みを設定します。

```gradle:/build-logic/settings.gradle
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](https://github.com/takahirom/gradle-version-catalog-converter)を使用すると良さそうです。

```toml:/gradle/libs.versions.toml
[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`で設定した名前です。

基本の記述は以下です。

```gradle:/build-logic/convention/build.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](https://github.com/android/nowinandroid/tree/main/build-logic/convention/src/main/kotlin)を参考にしています。

```kotlin:build-logic/convention/src/main/java/AndroidLibraryConventionPlugin.kt

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
            }
        }
    }
}
```

```kotlin
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 で作成したクラス名を記述します

```diff_gradle:/build-logic/convention/build.gradle
gradlePlugin {
    plugins {
+       register("androidLibrary") {
+           id = "androidsampleapp.android.library"
+           implementationClass = "AndroidLibraryConventionPlugin"
        }
    }

}
```

### 作成したビルドロジックを使用する

plugins ブロックで指定した ID を追加します。あとは定義済みの sdk 関連の値や option 関連もすべて削除できます。

```diff_gradle:build.gradle(ライブラリモジュール)
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 ブロックでそれぞれ追加します。

```gradle:AndroidHiltConventionPlugin.kt
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 を登録する

こちらは他と同様です。

```diff_gradle:/build-logic/convention/build.gradle
gradlePlugin {
    plugins {
+       register("androidHilt") {
+           id = "androidsampleapp.android.hilt"
+           implementationClass = "AndroidHiltConventionPlugin"
+       }
    }

}
```

### Plugin を使用する

変更は下記のようになります。

```diff_gradle
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 は依存関係が汚れがちなのであまり使いたくないなとも思っちゃいます。

# 参考

- https://github.com/android/nowinandroid
- https://docs.gradle.org/current/userguide/composite_builds.html
- https://qiita.com/irgaly/items/b735b61ff73d43e8741d