🐘

個人的なAndroidアプリのマルチモジュールプロジェクト設定

2024/10/13に公開

はじめに

Gradleでマルチモジュール構成にする際、何かしらの方法でビルドロジックを共通化しないと各所にボイラープレートコードが存在することになり大変です。そこで、ビルドロジックの共有のための工夫をするわけですが、構成が複雑で分からなくなることがあるので、自分なりのプロジェクト設定をここにメモします。(ほぼ「nowinandroid」そのままですが...)
https://github.com/android/nowinandroid/tree/main

大まかな構成

今回想定するのはメインのルートプロジェクト(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と実装クラスの指定によって行われる。

build.gradle.kts(build-logic)
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()の際に引き継がれる。

settings.gradle.kts(build-logic)
rootProject.name = "build-logic"

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
    // バージョンカタログの作成
    versionCatalogs {
        create("libs") {
            from(files("../gradle/libs.versions.toml"))
        }
    }
}

Convention pluginの定義

Convention pluginの定義は、Plugin<Project>を継承したクラスで行う。例えば、Androidライブラリの設定を共通化するプラグインを以下に示す。

AndroidLibraryConventionPlugin
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アプリケーションモジュールの共通の設定をするプラグインや、hiltroomを利用するモジュール共通の設定をするプラグインを定義することになるだろう。具体的なところについては、「nowinandroid」の構成が参考になる。
https://github.com/android/nowinandroid/tree/main/build-logic/convention/src/main/kotlin
ここで定義したプラグインのクラス名が先程のimplementationClassに渡されることによって、プラグインが登録される。(先程のビルドスクリプトを参照)

バージョンカタログへのConvention pluginの追加

定義したプラグインはバージョンカタログに加えてしまったほうが便利だ。バージョンには"unspecified"を指定しておく。

libs.versions.toml
[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)関数を呼ぶこともできる。

settings.gradle.kts(ルートプロジェクト)
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構文を利用する。これにより、サブプロジェクト間でのプラグインの依存関係のバージョン不一致による問題を防ぐ。

build.gradle.kts(ルートプロジェクト)
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ブロックで追加する。これで、ビルドロジックの共通化が完了した。

build.gradle.kts(サブプロジェクト)
plugins{
    alias(libs.plugins.some_app.android.library)
}

おわりに

Gradleには様々な文法があり、ベストプラクティスを見つけるのが大変ですが、とりあえずこの構成で組むことにしています。何か間違いがあれば指摘していただけると助かります。

Discussion