N予備校 Android アプリでビルドロジックを buildSrc から Composite Build に移行した話

#

[N予備校 Android](https://play.google.com/store/apps/details?id=nico.ed.nnn.zane&hl=ja&gl=US&pli=1)チームでテックリードをしている鎌田です。

`N予備校` Android チームでは、[Version Catalog](https://developer.android.com/studio/build/migrate-to-catalogs) の導入をきっかけに、ビルドロジックを buildSrc から [Composite Build](https://docs.gradle.org/current/userguide/composite_builds.html) に移行しました。

Version Catalog を導入したことで、buildSrc でバージョン管理する必要がなくなり、buildSrc が不要になったのと、マルチモジュール化しているのでビルド設定を共通化したかったためです。
本記事では、どのように buildSrc から Composite Build に移行したのかについてまとめます。

## 前提条件

前提条件は下記の通りです。

- Kotlin DSL であること
- Version Catalog 導入済みであること
- buildSrc を使用していること

## 導入手順

### 1. toml ファイルに Gradle プラグインを追加しておく

`build-logic` モジュールを追加する際に使用するため、`gradle/libs.versions.toml` に Gradle プラグインを追加しておきます。

```toml:gradle/libs.versions.toml
[versions]
androidGradlePlugin = "8.1.0"
kotlin = "1.9.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" }
...
```

### 2. build-logic モジュールを追加する

`build-logic` モジュールを追加します。
`build-logic/build.gragle.kts` を以下の通り実装します。

```kotlin:build-logic/build.gradle.kts
plugins {
    `kotlin-dsl`
}

dependencies {
    compileOnly(libs.android.gradlePlugin)
    compileOnly(libs.kotlin.gradlePlugin)
}
```

`build-logic` モジュールにて `プロジェクト名/gradle/libs.versions.toml` を参照するため、`build-logic/settings.gragle.kts` を以下の通り新規で作成します。

```kotlin:build-logic/settings.gragle.kts
dependencyResolutionManagement {
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
    }
    versionCatalogs {
        create("libs") {
            from(files("../gradle/libs.versions.toml"))
        }
    }
}

rootProject.name = "build-logic"
```

`build-logic` モジュールを参照できるよう、`settings.gragle.kts` に以下を記載します。

```kotlin:settings.gragle.kts
pluginManagement {
    includeBuild("build-logic")
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven(url = "https://jitpack.io")
    }
}
rootProject.name = "プロジェクト名"
```

上記の各種実装は基本的に [Now in Android](https://github.com/android/nowinandroid/blob/main/build-logic/convention/build.gradle.kts) の実装を参考にしました。

### 3. buildSrc からの依存を外す

もともと `N予備校` Android アプリでは、buildSrc にて各種バージョン情報を以下のように定義し、各モジュールの `build.gradle.kts` から参照していました。

```kotlin:buildSrc/src/main/java/dependency/Versions.kt
package dependency

object Versions {
    const val compileSdkVersion = 33
    const val minSdkVersion = 26
    const val versionCode = 1
    const val versionName = "1.0.0"
    const val composeCompiler = "1.4.7"
}
```

以下は `app/build.gradle.kts` の例です。

```kotlin:app/build.gradle.kts
...
android {
    ...
    compileSdk = dependency.Versions.compileSdkVersion
    ...
    defaultConfig {
        ...
        minSdk = dependency.Versions.minSdkVersion
        targetSdk = dependency.Versions.compileSdkVersion
        versionCode = dependency.Versions.versionCode
        versionName = dependency.Versions.versionName
        ...
    }
    ...
    composeOptions {
        kotlinCompilerExtensionVersion = dependency.Versions.composeCompiler
    }
    ...
}
...
dependencies {
    ...
}
```

最終的には buildSrc を削除したいため、各種バージョン情報を `libs.versions.toml` に移行します。

```toml:gradle/libs.versions.toml
[versions]
...
composeCompiler = "1.4.7"

# ライブラリ以外のバージョン情報
compileSdkVersion = "33"
minSdkVersion = "26"
versionCode = "1"
versionName = "1.0.0"
...
```

```kotlin:app/build.gradle.kts
...
android {
    ...
    compileSdk = libs.versions.compileSdkVersion.get().toInt()
    ...
    defaultConfig {
        ...
        minSdk = libs.versions.minSdkVersion.get().toInt()
        targetSdk = libs.versions.compileSdkVersion.get().toInt()
        versionCode = libs.versions.versionCode.get().toInt()
        versionName = libs.versions.versionName.get()
        ...
    }
    ...
    composeOptions {
        kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
    }
    ...
}
...
dependencies {
    ...
}
```

### 4. Plugin を追加する

一例として、ライブラリモジュール用の Plugin を追加します。

```kotlin:AndroidLibraryPlugin.kt
class AndroidLibraryPlugin : 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)
            }
        }
    }
}
```

`with(pluginManager)` ブロックが `plugins` ブロックにあたり、ここでプラグイン ID を指定します。

```kotlin
with(pluginManager) {
    apply("com.android.library")
    apply("org.jetbrains.kotlin.android")
}
```

また、他の Plugin でも使用するような共通の設定を以下のようにまとめておくと便利です。
例えば `compileSdk` と `minSdk`、Java バージョンを共通化するための関数を作成します。

```kotlin
/**
 * Android の build 共通設定を適用する
 */
fun Project.configureKotlinAndroid(
    commonExtension: CommonExtension<*, *, *, *, *>,
) {
    commonExtension.apply {
        compileSdk = libs.version("compileSdkVersion").toInt()
        defaultConfig {
            minSdk = libs.version("minSdkVersion").toInt()
        }

        compileOptions {
            sourceCompatibility = JavaVersion.VERSION_11
            targetCompatibility = JavaVersion.VERSION_11
        }

        tasks.withType<KotlinCompile>().configureEach {
            kotlinOptions {
                jvmTarget = JavaVersion.VERSION_11.toString()
            }
        }
    }
}

/**
 * VersionCatalog の取得
 */
val Project.libs
    get(): VersionCatalog = extensions.getByType<VersionCatalogsExtension>().named("libs")
```

`libs.versions.toml` から `[versions]` の値を参照したい場合、`libs.version("compileSdkVersion").toInt()` という形で指定します。

### 5. 追加した Plugin を該当のモジュールに適用する

任意のライブラリモジュールにて、`build.gradle.kts` の実装が以下の通りだったとします。

```kotlin:build.gradle.kts
plugins {
    id(libs.plugins.android.library.get().pluginId)
    id(libs.plugins.kotlin.android.get().pluginId)
}

android {
    namespace = "sample.library"
    compileSdk = libs.versions.compileSdkVersion.get().toInt()

    defaultConfig {
        minSdk = libs.versions.minSdkVersion.get().toInt()
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles("consumer-rules.pro")
    }
    buildFeatures {
        buildConfig = true
    }
    buildTypes {
        debug {
            isMinifyEnabled = false
        }
        release {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility(JavaVersion.VERSION_11)
        targetCompatibility(JavaVersion.VERSION_11)
    }
    kotlinOptions {
        jvmTarget = "11"
    }
    ...
}

dependencies {
    ...
}
```

`AndroidLibraryPlugin` を適用するため、`build-logic/build.gradle.kts` に以下を記載します。

```kotlin:build-logic/build.gradle.kts
...
gradlePlugin {
    plugins {
        register("androidLibrary") {
            id = "sample.android.library"
            implementationClass = "sample.buildlogic.AndroidLibraryPlugin"
        }
    }
}
```

`register` に指定する名前は外部から参照することはないため、識別できれば何でも OK です。
`id` に参照する際のプラグイン ID、`implementationClass` に Plugin クラスのパッケージ名およびクラス名を指定します。

そして、任意のライブラリモジュールにて、`build.gradle.kts` を以下のように書き換えます。

```kotlin:build.gradle.kts
plugins {
    id("sample.android.library")
}

android {
    namespace = "sample.library"

    defaultConfig {
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles("consumer-rules.pro")
    }
    buildFeatures {
        buildConfig = true
    }
    buildTypes {
        debug {
            isMinifyEnabled = false
        }
        release {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    ...
}

dependencies {
    ...
}
```

`AndroidLibraryPlugin` を適用したことで、`AndroidLibraryPlugin` に記載されている内容を削除し、`build.gradle.kts` を簡略化できました。

### 導入時のポイント

導入にあたって以下の点を心がけました。

- 共通化しすぎない
  - 共通化しすぎてしまうと、Plugin の修正時に影響範囲が大きくなるなどのリスクがあるので、明らかに全モジュールで共通化するべきもののみ共通化しました
- モジュール構成によって最適な共通化を行うようにした
  - 当アプリでは ui モジュール、domain モジュールにおいて、さらに機能ごとにモジュールが分かれていて、各機能のモジュールを共通化するための Plugin を作成、適用しました
  - モジュール構成図の詳細は[こちら](https://blog.nnn.dev/entry/2022/08/03/110000#%E3%83%9E%E3%83%AB%E3%83%81%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%82%92%E5%B0%8E%E5%85%A5%E3%81%99%E3%82%8B2022-%E5%B9%B4-1-%E6%9C%88--)

![module_configuration_diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93963/b3de7bd2-02ab-2815-613d-e58b43af80d1.png)

## まとめ

本記事では、`N予備校` Android チームでビルドロジックを buildSrc から Composite Build に移行した件についてまとめました。
Composite Build に移行することで、バージョン管理を Version Catalog にまとめることができたのと、ビルド設定が共通化できたため、共通設定を変更する際に少ない変更で済むなどメリットがありました。

## 参考URL

- [Migrate your build to version catalogs  |  Android Studio  |  Android Developers](https://developer.android.com/build/migrate-to-catalogs)
- [android/nowinandroid: A fully functional Android app built entirely with Kotlin and Jetpack Compose](https://github.com/android/nowinandroid)
- [Gradle: Composite Build でマルチモジュールプロジェクトの build.gradle.kts 設定を共通化する - Qiita](https://qiita.com/irgaly/items/b735b61ff73d43e8741d)