個人的なAndroidアプリのマルチモジュールプロジェクト設定
はじめに
Gradleでマルチモジュール構成にする際、何かしらの方法でビルドロジックを共通化しないと各所にボイラープレートコードが存在することになり大変です。そこで、ビルドロジックの共有のための工夫をするわけですが、構成が複雑で分からなくなることがあるので、自分なりのプロジェクト設定をここにメモします。(ほぼ「nowinandroid」そのままですが...)
大まかな構成
今回想定するのはメインのルートプロジェクト(some_app)とその子プロジェクト(app, core, data等)、そしてビルド設定共有用のbuild-logicモジュールがあり、ルートプロジェクトからインクルードされるような構成である。
Root project 'some_app'
+--- Project ':app'
+--- Project ':core'
| +--- Project ':core:aaa'
| \--- Project ':core:bbb'
\--- Project ':data'
+--- Project ':data:xxx'
\--- Project ':data:yyy'
Included builds:
\--- Included build ':build-logic'
some_app
├── build-logic
│ ├── build.gradle.kts
│ ├── settings.gradle.kts
│ └── src
├── app
│ ├── build.gradle.kts
│ └── src
├── core
│ ├── aaa
│ │ ├── build.gradle.kts
│ │ └── src
│ └── bbb
│ ├── build.gradle.kts
│ └── src
├── data
│ ├── xxx
│ │ ├── build.gradle.kts
│ │ └── src
│ └── yyy
│ ├── build.gradle.kts
│ └── src
├── build.gradle.kts
└── settings.gradle.kts
build-logicモジュール
サブプロジェクト間でビルド設定を共有するためのモジュールとして、build-logicモジュールを定義する。いわゆるConvention pluginを定義するためのモジュールである。
ビルドスクリプト
build-logic
モジュールのビルドスクリプトでは、独自で作成したConvention pluginの登録を行う。登録はgradlePlugin
->plugins
ブロック内で、idと実装クラスの指定によって行われる。
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
// Gradleプラグイン作成のため、kotlin-dslプラグインを追加
plugins {
`kotlin-dsl`
}
dependencies {
compileOnly(libs.android.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.ksp.gradlePlugin)
compileOnly(libs.room.gradlePlugin)
compileOnly(libs.compose.gradlePlugin)
}
// build-logicのプラグインのJavaバージョンを設定(アプリのJDKバージョンではない)
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
}
// 独自Convention pluginの登録
gradlePlugin{
plugins{
register("androidLibrary"){
id = "some_app.android.library"
implementationClass = "AndroidLibraryConventionPlugin"
}
/* ...and more */
}
}
設定ファイル
build-logic
モジュールはメインのアプリモジュールより先に評価させるため、ここでバージョンカタログの作成を行う。バージョンカタログはincludeBuild()
の際に引き継がれる。
rootProject.name = "build-logic"
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
// バージョンカタログの作成
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
Convention pluginの定義
Convention pluginの定義は、Plugin<Project>
を継承したクラスで行う。例えば、Androidライブラリの設定を共通化するプラグインを以下に示す。
class AndroidLibraryConventionPlugin: Plugin<Project> {
override fun apply(target: Project) {
with(target){
pluginManager.apply{
apply("com.android.library")
apply("org.jetbrains.kotlin.android")
}
extensions.configure<LibraryExtension> {
configureKotlinAndroid(this)
defaultConfig.targetSdk = 34
}
dependencies{
add("implementation",libs.findLibrary("kotlin-result").get())
add("implementation",libs.findLibrary("kotlin-result-coroutines").get())
add("testImplementation",libs.findLibrary("junit").get())
add("testImplementation",libs.findLibrary("junit-jupiter").get())
add("androidTestImplementation",libs.findLibrary("junit").get())
add("testImplementation", kotlin("test"))
}
}
}
}
ここでは、Androidライブラリモジュールとして適用するプラグイン、Android Sdkバージョン、依存するライブラリ等の設定を一括で管理している。他にも、Androidアプリケーションモジュールの共通の設定をするプラグインや、hilt
やroom
を利用するモジュール共通の設定をするプラグインを定義することになるだろう。具体的なところについては、「nowinandroid」の構成が参考になる。
ここで定義したプラグインのクラス名が先程のimplementationClass
に渡されることによって、プラグインが登録される。(先程のビルドスクリプトを参照)
バージョンカタログへのConvention pluginの追加
定義したプラグインはバージョンカタログに加えてしまったほうが便利だ。バージョンには"unspecified"
を指定しておく。
[plugins]
some_app-android-library = { id = "some_app.android.library", version = "unspecified" }
ルートプロジェクト
設定ファイル
build-logic
モジュールをincludeBuild()
によってビルドに含めることでConvention Pluginを追加する。また、pluginManagement
でプラグインの、dependencyResolutionManagement
でライブラリの依存関係の解決方法を記述しておく。プロジェクトのrepository
ブロックからリポジトリの設定を上書きされることを防ぐために、repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
関数を呼ぶこともできる。
pluginManagement {
includeBuild("build-logic") // build-logicモジュールの読み込み
repositories {
google{
// 正規表現によって検索時間を減らす
content{
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
// プロジェクト単位でのリポジトリ設定の禁止
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
gradlePluginPortal()
maven (url ="https://jitpack.io")
}
}
rootProject.name = "some_app"
// サブプロジェクトのインクルード
// app
include(":app")
// core
include(":core:aaa")
include(":core:bbb")
// data
include(":data:xxx")
include(":data:yyy")
ビルドスクリプト
ここでは、alias
関数によってバージョンカタログからプラグインの依存関係を追加し、プラグインの適用ではなく解決のみを行うためにapply false
構文を利用する。これにより、サブプロジェクト間でのプラグインの依存関係のバージョン不一致による問題を防ぐ。
plugins{
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.android.test) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.hilt) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.room) apply false
alias(libs.plugins.jetbrains.kotlin.android) apply false
}
サブプロジェクト
ビルドスクリプト
サブプロジェクトのビルドスクリプトでは、独自で定義したConvention pluginを直接plugins
ブロックで追加する。これで、ビルドロジックの共通化が完了した。
plugins{
alias(libs.plugins.some_app.android.library)
}
おわりに
Gradleには様々な文法があり、ベストプラクティスを見つけるのが大変ですが、とりあえずこの構成で組むことにしています。何か間違いがあれば指摘していただけると助かります。
Discussion