Featured image of post [Android] Convention Plugin は小さく作って組み合わせる

[Android] Convention Plugin は小さく作って組み合わせる

NowInAndroid・DroidKaigi の build-logic を観察し、Convention Plugin の設計指針を考える

要点

  • Convention Plugin は 単一の関心を持つ小さな Plugin に分割し、組み合わせて使うのが良い。
  • ヘルパー関数(configureXxx() / setupXxx())の使用は必要最小限に留める。
  • NowInAndroid、DroidKaigi 2024、DroidKaigi 2025 の build-logic を観察すると、ヘルパー関数への依存が徐々に減り、Plugin の組み合わせに寄っていく傾向が見える。

Convention Plugin とヘルパー関数

Android のマルチモジュールプロジェクトでは、build-logic(Convention Plugin)でビルド設定を共通化する。Convention Plugin の実装でよく使われるのが、Project の拡張関数として configureXxx() / setupXxx() を定義するパターンである 1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ヘルパー関数: 共通の Android 設定を適用する
internal fun Project.configureKotlinAndroid(
    extension: CommonExtension<*, *, *, *, *, *>,
) {
    extension.apply {
        compileSdk = 35
        defaultConfig { minSdk = 28 }
        compileOptions {
            sourceCompatibility = JavaVersion.VERSION_17
            targetCompatibility = JavaVersion.VERSION_17
        }
    }
}

// Plugin から呼び出す
class AndroidLibraryPlugin : Plugin<Project> {
    override fun apply(project: Project) = with(project) {
        pluginManager.apply("com.android.library")
        extensions.configure<LibraryExtension> {
            configureKotlinAndroid(this)
        }
    }
}

初期段階では問題ない。しかし、ヘルパー関数はプロジェクトの成長とともに 関数名から予測できない処理を抱え込みやすいconfigureKotlinAndroid に依存追加が紛れ込む、setupFlavor に flavor と無関係なライブラリ依存が含まれる — こうした副作用は、設定を変更しようとしたときに初めて発覚する。

OSS の build-logic を観察する

3 つの OSS プロジェクトの build-logic を比較する 2

NowInAndroid: ヘルパー関数を広範に使用

NowInAndroid は 16 個の Convention Plugin と 13 個のヘルパー関数を持つ。Plugin の apply() からヘルパー関数を呼び出す構成である。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// AndroidApplicationConventionPlugin.kt
class AndroidApplicationConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) = with(target) {
        pluginManager.apply("com.android.application")
        pluginManager.apply("org.jetbrains.kotlin.android")

        extensions.configure<ApplicationExtension> {
            configureKotlinAndroid(this)
            configureGradleManagedDevices(this)
            configurePrintApksTask(this)
            configureBadgingTasks(this)
        }
        configureSpotlessForAndroid()
    }
}

一方で、AndroidFeatureImplConventionPlugin は他の Convention Plugin を組み合わせている。

1
2
3
// AndroidFeatureImplConventionPlugin.kt
apply(plugin = "nowinandroid.android.library")
apply(plugin = "nowinandroid.hilt")

ヘルパー関数と Plugin の組み合わせが混在するハイブリッド構成である。configureKotlinAndroid() の中には desugaring の依存追加(coreLibraryDesugaring)が含まれており、関数名から予測しにくい副作用の一例になっている。

DroidKaigi 2024: Plugin の組み合わせ + 最小限のヘルパー関数

DroidKaigi conference-app-2024primitive / convention の 2 層構造を採用している。

1
2
3
4
build-logic/
└── src/main/kotlin/
    ├── primitive/    # 単一責務の Plugin(31 ファイル)
    └── convention/   # primitive を組み合わせる Plugin(2 ファイル)

Convention Plugin は primitive Plugin を組み合わせるだけである。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// convention/KmpFeaturePlugin.kt
with(pluginManager) {
    apply("droidkaigi.primitive.kmp")
    apply("droidkaigi.primitive.kmp.android")
    apply("droidkaigi.primitive.kmp.ios")
    apply("droidkaigi.primitive.kmp.compose")
    apply("droidkaigi.primitive.kmp.android.hilt")
    apply("droidkaigi.primitive.kmp.roborazzi")
    apply("droidkaigi.primitive.detekt")
}

ヘルパー関数は setupAndroid()setupDetekt() の 2 つのみ。中身は compileSdk / minSdk / lint 設定だけで、副作用は含まれていない。3 つの Plugin(AndroidPluginAndroidApplicationPluginKmpAndroidPlugin)から呼ばれる素朴な共通設定の再利用である。

モジュール側では、必要な primitive を選択して適用する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// feature モジュール: convention で一括適用
plugins {
    id("droidkaigi.convention.kmp-feature")
}

// core/model: 必要な primitive だけ選択
plugins {
    id("droidkaigi.primitive.kmp")
    id("droidkaigi.primitive.kmp.ios")
    id("droidkaigi.primitive.spotless")
}

DroidKaigi 2025: ヘルパー関数ゼロ

DroidKaigi conference-app-2025 では、ヘルパー関数が完全に消えた。precompiled script plugin(.gradle.kts)に移行し、各ファイルが直接設定を記述する。

1
2
3
4
5
6
gradle-conventions/
└── src/main/kotlin/
    ├── droidkaigi/
    │   ├── convention/   # 組み合わせ Plugin
    │   └── primitive/    # 単一責務の Plugin(12 ファイル)
    └── util/             # DSL アクセサのみ

primitive / convention の 2 層構造は維持されているが、util/ に残っているのは Version Catalog のアクセサや Compose の DSL ラッパーのみで、setup / configure 系の関数はゼロである。

3 プロジェクトの対比

観点 NowInAndroid DroidKaigi 2024 DroidKaigi 2025
Plugin 数 16 33(primitive 31 + convention 2) 13(primitive 12 + convention 1)
ヘルパー関数 13 個(広範に使用) 2 個(最小限) 0 個
Plugin の組み合わせ 一部のみ 全面的 全面的
primitive / convention 分離 なし あり あり
副作用の混入 あり なし なし

提案: 小さな Plugin を組み合わせる

OSS の観察から導かれる設計指針は 2 つある。

1. 単一責務の小さな Plugin を作る

Plugin は 1 つの関心だけを持つ。Kotlin 設定、Compose 設定、Hilt 設定をそれぞれ独立した Plugin にする。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class ComposePlugin : Plugin<Project> {
    override fun apply(project: Project) = with(project) {
        pluginManager.apply("org.jetbrains.kotlin.plugin.compose")
        extensions.configure<ApplicationExtension> {
            buildFeatures { compose = true }
        }
        dependencies {
            add("implementation", platform(libs.library("composeBom")))
        }
    }
}

Plugin は apply() が冪等であることが期待されるため、「ついでにこの依存も」という追加に対して心理的な抵抗が生まれやすい。ヘルパー関数にはこの制約がない。

2. ヘルパー関数は必要最小限に留める

ヘルパー関数を禁止する必要はないが、複数の Plugin から呼ばれる internal fun Project.configureXxx() は Plugin への昇格を検討すべきである。

残してよいケース:

  • Plugin 内部の private fun として処理を整理する場合
  • Version Catalog のアクセサなど、設定を持たない純粋なユーティリティ

避けるべきケース:

  • 複数の Plugin から呼ばれ、中身が膨らみやすい internal fun Project.configureXxx()
  • 関数名の範囲を超える処理(依存追加、タスク登録など)を含んでいる場合

判断基準は「その関数を削除したとき、呼び出し元が意図しない影響を受けるか」である。影響を受けるなら、Plugin に昇格させて責務を明示する。

libs.versions.toml の変数名を camelCase にする

DroidKaigi のプロジェクトでは、libs.versions.toml の変数名を camelCase で記述している。

1
2
3
4
5
# ケバブケース(NowInAndroid)
compose-bom = { ... }  # → libs.compose.bom(ドット区切りに変換される)

# camelCase(DroidKaigi)
composeBom = { ... }    # → libs.composeBom(そのまま使える)

camelCase にすると、Kotlin 側で libs.composeBom とそのままアクセスできる。ケバブケースの場合は libs.compose.bom とドット区切りに変換されるため、名前空間の階層が意図せず生まれる。小さな違いだが、build-logic 内で Version Catalog を頻繁に参照する場合は地味に便利である。

参考

本記事の設計指針に基づいたサンプルプロジェクトを公開している。

Jetpack Compose + Hilt + マルチモジュール構成のサンプルアプリ。build-logic は小さな Plugin の組み合わせで構成

まとめ

  • Convention Plugin は 小さく、単一責務 に作る。
  • 小さな Plugin を 組み合わせ て、モジュールの要件に応じた設定を組み立てる。
  • ヘルパー関数(configureXxx() / setupXxx())は必要最小限に留める。複数の Plugin から呼ばれるヘルパー関数は、Plugin への昇格を検討する。

  1. NowInAndroid は configureXxx() 、DroidKaigi 2024 は setupXxx() を命名規則として使っている。本記事では便宜上ヘルパー関数と呼ぶ。 ↩︎

  2. 各プロジェクトの参照時点は本記事執筆時点の最新コミットに基づく。 ↩︎

Hugo で構築されています。
テーマ StackJimmy によって設計されています。