💡はじめに
こんにちは。ニフティ株式会社のLinです。
台湾出身のモバイルアプリエンジニアとして、社内で「マイ ニフティ」のAndroidおよびiOS版の開発を担当しております。
今回は、マイニフティ Android 2.0.4 で導入した、Gradle の Groovy DSL → Kotlin DSL → Version Catalog への移行をご紹介します。
🐘 Gradleの進化
Gradleは、Androidアプリの構築とビルドを管理するビルドシステムで、下記の機能があります:
- プロジェクトの構成管理
- ライブラリの依存関係管理
- リソースファイルの管理
- プロダクションビルド、デバッグビルドの切り替え
- APKファイルのビルド
マスコットはGradle君という名前の可愛い象です。
Android 開発における Gradle Script の記述方法には、Groovy DSL と Kotlin DSL の2つのパターンがあります。
Groovy DSL は従来からよく使われている記法で、シングルクォーテーションと括弧なしのダブルクォーテーションが特徴的です。
1 |
implementation 'androidx.room:room-rxjava2:$room_version' |
1 |
implementation "androidx.datastore:datastore-preferences:1.0.0" |
そして、2020年4月の Android Gradle Plugin 4.0.0 から、Kotlin DSL が導入されました。
括弧付きのダブルクォーテーションが特徴で、静的型付けによる型安全などのメリットがあります。
1 |
implementation("androidx.room:room-rxjava2:$room_version") |
しかし、Android の発展に伴い、Gradle の既存機能にはいくつかの課題がありました:
- 異なる記法がbuild.gradleファイルに混在しており、保守運用がやや複雑になってしまっている
- マルチモジュールの場合、複数のbuild.gradleに重複するコードを記述しなければならない
- バージョンアップする際に、変更箇所が散在してしまう
どうしよう…
Gradle君の悩みはどんどん大きくなった。
🧩 課題解決
課題解決のため、Gradle君はライブラリバージョン管理の改善を試し、二つ改善方法を見つけました:
- BuildSrc
buildSrc/build.gradle
に共通の部分を集約することができ、その中身は以前の build.gradle と同様の書き方です- どのGradleバージョンにも使えます
- ただし、下記理由のため、一時対策として使われています
- 機能ごとの複数の依存関係をまとめるのは手間がかかります
- pluginsの集約はできません
- モジュール間のコンフリクトを完全に避けるのは難しい
- 拡張性に制限があります
- Version Catalog
libs.versions.toml
に共通のバージョン、依存関係、プラグインを集約することができ、その中身は以前の build.gradle とは異なります- Gradle 7.4以上で導入された機能のため、バージョン制限があります
- Gradle 7.0 – 7.4 では Feature Preview、Gradle 7.0 以下では使えません
- 下記理由のため、恒久対策として使われています
- 機能ごとの複数の依存関係をまとめることができます
[bundles]
- pluginsの集約もできます
[plugins]
- Kotlin DSLのみで記述できるため、型安全などの利点があります
- Android Studioの新規プロジェクトでもVersion Catalogがデフォルトになったため、これからも業界の標準となっていくでしょう
- 機能ごとの複数の依存関係をまとめることができます
では、Gradle君と一緒にVersion Catalogを使ってみよう!
💻 Groovy DSL → Kotlin DSL → Version Catalog
移行作業には以下の2つのステップがあります:
- Kotlin DSLへの移行
- 下記ファイルの書き換え
→settings.gradlesettings.gradle.kts
- Groovy DSL → Kotlin DSL
include ':app'
↓include(":app")
- pluginManagementの追加
- @Incubatingの警告が出る場合は、
@file:Suppress("UnstableApiUsage")
も追加してください
- Groovy DSL → Kotlin DSL
→build.gradle(Project層)build.gradle.kts(Project層)
- Groovy DSL → Kotlin DSL
kotlin_version = '1.9.23'
↓val kotlinVersion: String by extra("1.9.23")
- Groovy DSL → Kotlin DSL
→build.gradle(Module層)build.gradle.kts(Module層)
- Groovy DSL → Kotlin DSL
- buildConfigの移行(AGP 9.0で廃止予定)
- 下記ファイルの書き換え
- Version Catalogへの移行
[versions]
:すべてのライブラリのバージョンを集約します- 定義した変数は
version.ref
で使用されます
- 定義した変数は
[libraries]
:すべてのライブラリのパスを集約します- module (group:name) より group + name の方がおすすめです
library = { module = "... : ...", version.ref = "..." }
↓library = { group = "...", name = "...", version.ref = "..." }
- BOMの集約もできます
- module (group:name) より group + name の方がおすすめです
[bundles]
:機能ごとの複数のライブラリをまとめます[libraries]
で定義したライブラリの変数名を、[bundles]
ごとにまとめます
[plugins]
:pluginsのclasspathとidを集約します[plugins]
で定義したものは、build.gradleのpluginsブロックでalias(...)
で呼び出せます- 例:Compose Compilerの移行
– libs.versions.toml
– build.gradle.kts(Project層)
– build.gradle.kts(Module層)
[bundles]
を活用すれば、必要な依存関係を1回のimplementationで一括して指定できます。
1 2 3 4 5 6 7 8 9 10 11 12 |
// libs.versions.toml [versions] room = "2.6.1" [libraries] androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } [bundles] room = ["androidx-room-runtime", "androidx-room-ktx"] |
1 2 3 4 5 6 7 8 9 |
// build.gradle.kts(Module層) ... dependencies { ... // Room implementation(libs.bundles.room) ksp(libs.androidx.room.compiler) } |
そして、移行作業が完了したら、Gradle Sync
を忘れずに行いましょう。
💫 成果
これまでは各 Gradle ファイルに散在していたライブラリバージョン情報は、下記のようにVersion Catalogに集約しました:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
// libs.versions.toml [versions] activityCompose = "1.9.2" agp = "8.4.2" appcompat = "1.7.0" browser = "1.8.0" coilCompose = "2.7.0" commonsValidator = "1.9.0" composeBOM = "2024.09.02" constraintlayoutCompose = "1.0.1" coreKtx = "1.13.1" coreSplashscreen = "1.0.1" dagger = "2.52" datastorePreferences = "1.1.1" firebaseBOM = "33.3.0" firebaseCrashlyticsGradle = "3.0.2" googleServices = "4.4.2" hiltCommon = "1.2.0" junit = "4.13.2" junitExtensions = "1.2.1" kotlin = "1.9.24" kotlinxCoroutines = "1.8.1" kotlinxSerialization = "1.6.3" ksp = "1.9.24-1.0.20" ktlint = "1.3.1" leakcanaryAndroid = "2.14" lifecycle = "2.8.6" material = "1.12.0" navigationCompose = "2.8.1" okHttp = "4.12.0" ossLicensesPlugin = "0.10.6" perfPlugin = "1.4.2" playServicesAuth = "21.2.0" playServicesAuthApiPhone = "18.1.0" playServicesOssLicenses = "17.1.0" retrofitBOM = "2.11.0" reviewKtx = "2.0.1" room = "2.6.1" spotless = "6.25.0" swiperefreshlayout = "1.1.0" timber = "5.0.1" uiTestJunit4 = "1.7.2" [libraries] androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "browser" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBOM" } androidx-constraintlayout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "constraintlayoutCompose" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" } androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastorePreferences" } androidx-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" } androidx-hilt-common = { group = "androidx.hilt", name = "hilt-common", version.ref = "hiltCommon" } androidx-hilt-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltCommon" } androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltCommon" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitExtensions" } androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" } androidx-material = { group = "androidx.compose.material", name = "material" } androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" } androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coilCompose" } commons-validator = { group = "commons-validator", name = "commons-validator", version.ref = "commonsValidator" } converter-kotlinx-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization" } coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" } firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBOM" } firebase-config = { group = "com.google.firebase", name = "firebase-config" } firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" } firebase-dynamic-links = { group = "com.google.firebase", name = "firebase-dynamic-links" } firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" } firebase-perf = { group = "com.google.firebase", name = "firebase-perf" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "dagger" } hilt-android-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "dagger" } junit = { group = "junit", name = "junit", version.ref = "junit" } kotlin-stdlib-jdk8 = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" } ktlint-cli = { group = "com.pinterest.ktlint", name = "ktlint-cli", version.ref = "ktlint" } leakcanary-android = { group = "com.squareup.leakcanary", name = "leakcanary-android", version.ref = "leakcanaryAndroid" } logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okHttp" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okHttp" } play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "playServicesAuth" } play-services-auth-api-phone = { group = "com.google.android.gms", name = "play-services-auth-api-phone", version.ref = "playServicesAuthApiPhone" } play-services-oss-licenses = { group = "com.google.android.gms", name = "play-services-oss-licenses", version.ref = "playServicesOssLicenses" } retrofit = { group = "com.squareup.retrofit2", name = "retrofit" } retrofit-bom = { group = "com.squareup.retrofit2", name = "retrofit-bom", version.ref = "retrofitBOM" } review-ktx = { group = "com.google.android.play", name = "review-ktx", version.ref = "reviewKtx" } timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "uiTestJunit4" } [bundles] androidx = ["androidx-core-ktx", "androidx-appcompat", "material", "androidx-constraintlayout-compose", "androidx-activity-compose", "androidx-lifecycle-runtime-ktx", "androidx-lifecycle-runtime-compose", "androidx-lifecycle-viewmodel-ktx", "androidx-lifecycle-viewmodel-compose", "androidx-navigation-compose", "androidx-datastore-preferences", "androidx-core-splashscreen", "androidx-swiperefreshlayout", "androidx-browser"] compose = ["androidx-ui", "androidx-foundation-layout", "androidx-material", "androidx-material-icons-extended", "androidx-ui-tooling-preview", "review-ktx"] compose-debug = ["androidx-ui-test-manifest", "androidx-ui-tooling"] firebase = ["firebase-analytics", "firebase-messaging", "firebase-crashlytics", "firebase-perf", "firebase-dynamic-links", "firebase-config"] hilt = ["hilt-android", "androidx-hilt-common", "androidx-hilt-navigation-compose"] hilt-ksp = ["hilt-android-compiler", "androidx-hilt-compiler"] kotlin = ["kotlin-stdlib-jdk8", "kotlinx-serialization-json", "kotlinx-coroutines-core"] network = ["retrofit", "converter-kotlinx-serialization", "okhttp", "logging-interceptor"] play-services = ["play-services-auth", "play-services-auth-api-phone", "play-services-oss-licenses"] room = ["androidx-room-runtime", "androidx-room-ktx"] unit-test = ["coroutines-test", "junit"] [plugins] android-application = { id = "com.android.application", version.ref = "agp" } dagger-hilt-android-plugin = { id = "dagger.hilt.android.plugin", version.ref = "dagger" } firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsGradle" } firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "perfPlugin" } google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } oss-licenses-plugin = { id = "com.google.android.gms.oss-licenses-plugin", version.ref = "ossLicensesPlugin" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// build.gradle.kts(Project層) ... buildscript { repositories { google() mavenCentral() } } plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.dagger.hilt.android.plugin) apply false alias(libs.plugins.firebase.crashlytics) apply false alias(libs.plugins.firebase.perf) apply false alias(libs.plugins.google.services) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.oss.licenses.plugin) apply false alias(libs.plugins.spotless) } allprojects { val buildDirPath: String = layout.buildDirectory.get().asFile.absolutePath plugins.matching { anyPlugin -> anyPlugin is AppPlugin || anyPlugin is LibraryPlugin } .whenPluginAdded { apply(plugin = libs.plugins.spotless.get().pluginId) configure<SpotlessExtension> { kotlin { target("**/*.kt", "**/*.kts") targetExclude("$buildDirPath/**/*.kt", "bin/**/*.kt", "**/generated/**/*.kt") ktlint(libs.versions.ktlint.get()) } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
// build.gradle.kts(Module層) ... plugins { alias(libs.plugins.android.application) alias(libs.plugins.dagger.hilt.android.plugin) alias(libs.plugins.firebase.crashlytics) alias(libs.plugins.firebase.perf) alias(libs.plugins.google.services) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ksp) alias(libs.plugins.oss.licenses.plugin) } ... dependencies { // Kotlin implementation(libs.bundles.kotlin) // Compose implementation(platform(libs.androidx.compose.bom)) implementation(libs.bundles.compose) debugImplementation(libs.bundles.compose.debug) androidTestImplementation(libs.ui.test.junit4) // AndroidX implementation(libs.bundles.androidx) // Firebase implementation(platform(libs.firebase.bom)) implementation(libs.bundles.firebase) // Networking implementation(platform(libs.retrofit.bom)) implementation(libs.bundles.network) // Coil implementation(libs.coil.compose) // Hilt implementation(libs.bundles.hilt) ksp(libs.bundles.hilt.ksp) // Leakcanary debugImplementation(libs.leakcanary.android) // Timber implementation(libs.timber) // Room implementation(libs.bundles.room) ksp(libs.androidx.room.compiler) // SMS User Consent API, OSS Licenses implementation(libs.bundles.play.services) // Apache Validator implementation(libs.commons.validator) // Test testImplementation(libs.bundles.unit.test) androidTestImplementation(libs.androidx.junit) // ktlint @Suppress("UnstableApiUsage") ktlintConfig(libs.ktlint.cli) { attributes { attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL)) } } } |
今後は Version Catalog の [versions]
セクションにバージョン情報を集約しているため、ライブラリのアップデートと管理がより簡単になりました。
ぜひ Version Catalog の便利さを体験してみましょう!