AndroidのテンプレートをVersion catalog対応にする
Version catalogとは
Gradle 7.0から追加されたバージョン管理を一元管理できる仕組みです。
今まではbuildSrc
内でobjectクラスに定義していたのですが、dependabotでは上手く反応してくれなかったりしていました。
Version catalogではToml
ファイルで管理できるところが便利そうです。
少し前から気になっていたのですがGradle 7.4からStableになったことと、Android studio Flamingoからアプリ生成のテンプレートがGradle 8.0になったことをきっかけに、使ってみようと思います。
Let's 実装
導入の練習としてAndroid studioのプロジェクト作成のテンプレートからVersion catalogに移行します。
今回はGroovyのままで記述してきますがKTSでも利用可能です。
ゴールはRenovateを使用してライブラリーを更新してもらうことです。
環境
- Mac OS Venture(13.3)
- Android studio Flamingo(2022.2.1)
1. Android studioでプロジェクトを作成する
今回はFlamingoのテンプレートの[Empty Activity]を使用します。
Android studioのテンプレート
(Android studio FlamingoからMaterial3が推奨されているらしい)
作業内容を後に残せるように作成したプロジェクトはGitHubにpushしておきます。
2. Tomlファイルの作成
今回はversion-catalog-updateを導入して移行しています。
結果的に自分で作成した方がよかったかもと思いましたが、ものは試しでつかって見ます。
- ルート直下の
build.gradle
に下記を追加
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.0.0' apply false
id 'com.android.library' version '8.0.0' apply false
id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
+ id "com.github.ben-manes.versions" version "0.41.0"
+ id "nl.littlerobots.version-catalog-update" version "0.8.0"
}
- syncしてbuild
- コマンド実行
./gradlew versionCatalogUpdate --create
を実行します。
そうするとgradle
ディレクトリにlibs.versions.toml
が生成されます。
生成されたTomlファイル
[versions]
androidx-activity = "1.5.1"
androidx-arch-core = "2.1.0"
androidx-compose-animation = "1.3.0"
androidx-compose-foundation = "1.3.0"
androidx-compose-material = "1.3.0"
androidx-compose-runtime = "1.3.0"
androidx-compose-ui = "1.3.0"
androidx-core = "1.8.0"
androidx-lifecycle = "2.5.1"
androidx-savedstate = "1.2.0"
org-jetbrains-kotlin = "1.7.20"
org-jetbrains-kotlinx = "1.6.4"
[libraries]
androidx-activity = { module = "androidx.activity:activity", version.ref = "androidx-activity" }
androidx-activity-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
androidx-activity-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }
androidx-annotation = "androidx.annotation:annotation:1.5.0"
androidx-annotation-annotation-experimental = "androidx.annotation:annotation-experimental:1.1.0"
androidx-arch-core-core-common = { module = "androidx.arch.core:core-common", version.ref = "androidx-arch-core" }
androidx-arch-core-core-runtime = { module = "androidx.arch.core:core-runtime", version.ref = "androidx-arch-core" }
androidx-autofill = "androidx.autofill:autofill:1.0.0"
androidx-collection = "androidx.collection:collection:1.0.0"
androidx-compose-animation = { module = "androidx.compose.animation:animation", version.ref = "androidx-compose-animation" }
androidx-compose-animation-animation-core = { module = "androidx.compose.animation:animation-core", version.ref = "androidx-compose-animation" }
androidx-compose-compiler = "androidx.compose.compiler:compiler:1.3.2"
androidx-compose-compose-bom = "androidx.compose:compose-bom:2022.10.00"
androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "androidx-compose-foundation" }
androidx-compose-foundation-foundation-layout = { module = "androidx.compose.foundation:foundation-layout", version.ref = "androidx-compose-foundation" }
androidx-compose-material = { module = "androidx.compose.material:material", version.ref = "androidx-compose-material" }
androidx-compose-material-material-icons-core = { module = "androidx.compose.material:material-icons-core", version.ref = "androidx-compose-material" }
androidx-compose-material-material-ripple = { module = "androidx.compose.material:material-ripple", version.ref = "androidx-compose-material" }
androidx-compose-material3 = "androidx.compose.material3:material3:1.0.0"
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "androidx-compose-runtime" }
androidx-compose-runtime-runtime-saveable = { module = "androidx.compose.runtime:runtime-saveable", version.ref = "androidx-compose-runtime" }
androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "androidx-compose-ui" }
androidx-compose-ui-ui-geometry = { module = "androidx.compose.ui:ui-geometry", version.ref = "androidx-compose-ui" }
androidx-compose-ui-ui-graphics = { module = "androidx.compose.ui:ui-graphics", version.ref = "androidx-compose-ui" }
androidx-compose-ui-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-compose-ui" }
androidx-compose-ui-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-compose-ui" }
androidx-compose-ui-ui-text = { module = "androidx.compose.ui:ui-text", version.ref = "androidx-compose-ui" }
androidx-compose-ui-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "androidx-compose-ui" }
androidx-compose-ui-ui-tooling-data = { module = "androidx.compose.ui:ui-tooling-data", version.ref = "androidx-compose-ui" }
androidx-compose-ui-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "androidx-compose-ui" }
androidx-compose-ui-ui-unit = { module = "androidx.compose.ui:ui-unit", version.ref = "androidx-compose-ui" }
androidx-compose-ui-ui-util = { module = "androidx.compose.ui:ui-util", version.ref = "androidx-compose-ui" }
androidx-concurrent-concurrent-futures = "androidx.concurrent:concurrent-futures:1.0.0"
androidx-core = { module = "androidx.core:core", version.ref = "androidx-core" }
androidx-core-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-customview-customview-poolingcontainer = "androidx.customview:customview-poolingcontainer:1.0.0"
androidx-lifecycle-lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "androidx-lifecycle" }
androidx-lifecycle-lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidx-lifecycle" }
androidx-lifecycle-lifecycle-livedata-core = { module = "androidx.lifecycle:lifecycle-livedata-core", version.ref = "androidx-lifecycle" }
androidx-lifecycle-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "androidx-lifecycle" }
androidx-lifecycle-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
androidx-lifecycle-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
androidx-lifecycle-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" }
androidx-lifecycle-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "androidx-lifecycle" }
androidx-profileinstaller = "androidx.profileinstaller:profileinstaller:1.2.0"
androidx-savedstate = { module = "androidx.savedstate:savedstate", version.ref = "androidx-savedstate" }
androidx-savedstate-savedstate-ktx = { module = "androidx.savedstate:savedstate-ktx", version.ref = "androidx-savedstate" }
androidx-startup-startup-runtime = "androidx.startup:startup-runtime:1.1.1"
androidx-test-espresso-espresso-core = "androidx.test.espresso:espresso-core:3.5.1"
androidx-test-ext-junit = "androidx.test.ext:junit:1.1.5"
androidx-tracing = "androidx.tracing:tracing:1.0.0"
androidx-versionedparcelable = "androidx.versionedparcelable:versionedparcelable:1.1.1"
junit = "junit:junit:4.13.2"
org-apache-logging-log4j-log4j-core = "org.apache.logging.log4j:log4j-core:2.17.1"
org-jetbrains-annotations = "org.jetbrains:annotations:13.0"
org-jetbrains-kotlin-kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-kotlin-stdlib-common = { module = "org.jetbrains.kotlin:kotlin-stdlib-common", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-kotlin-stdlib-jdk7 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlinx-kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "org-jetbrains-kotlinx" }
org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "org-jetbrains-kotlinx" }
org-jetbrains-kotlinx-kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "org-jetbrains-kotlinx" }
org-jetbrains-kotlinx-kotlinx-coroutines-core-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", version.ref = "org-jetbrains-kotlinx" }
[plugins]
com-android-application = "com.android.application:8.0.0"
com-android-library = "com.android.library:8.0.0"
com-github-ben-manes-versions = "com.github.ben-manes.versions:0.41.0"
nl-littlerobots-version-catalog-update = "nl.littlerobots.version-catalog-update:0.8.0"
org-jetbrains-kotlin-android = "org.jetbrains.kotlin.android:1.7.20"
libs.versions.toml
について
3. セクション
-
[versions]
:バージョンの定義 -
[libraries]
:ライブラリの定義 -
[plugins]
:プラグインの定義 -
[bundles]
:[libraries]
で定義したライブラリをまとめられる
個人的には[bundles]
でよく使うライブラリ群をまとめられるのが便利かと思います。
Alias
記述には少し気をつける点がありますが、かいつまんで説明すると
- エイリアスは
-
_
.
で区切られたもので構成される -
#
などの記号は使えない - 文字列のみ
Aliases and their mapping to type safe accessors
1. エイリアスは-
_
.
で区切られたもので構成される
バージョンを定義したものはタイプセーフなアクセッサーに変換されるので、区切られたものは.
で連結されます。
もちろんなくても大丈夫です。
-
android-plugin
→android.plugin
-
android_plugin_core
→android.plugin.core
-
kotlin
→kotlin
2. #
などの記号は使えない
公式でも書かれているのですが、基本は英数字で書きましょう。
先頭でなければ数字も使えたはず。
-
this.#is.not!
← NG!
3. 文字列のみ
入れられる値は文字列のみになります。
AndroidではsdkのバージョンなどをInt型で入れなければならないのですが、文字列しか利用できないため少し記述が必要です。
例としてcompileSdk
の場合
[versions]
app-compileSdk = "33"
と記述しておき、使用する際に
android {
compileSdk libs.versions.app.compileSdk.get().toIntger()
}
android {
compileSdk = libs.versions.app.compileSdk.get().toInt()
}
Int型に変換する必要があります。
各使用例
定義したものにアクセスするにはlibs
から参照できます。
[versions]
[versions]
app-versionCode = "1"
app-versionName = "1.0"
android {
versionCode libs.versions.app.targetSdk.get().toIntger()
versionName libs.versions.app.versionName.get()
}
[libraries]
BOMも利用できます
[versions]
androidx-core = "1.8.0"
compose = "2022.10.00"
[libraries]
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
# バージョンは直接でも書ける
# androidx-core-ktx = "androidx.core:core-ktx:1.10.0"
# BOMを利用する場合
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose" }
compose-material3 = { module = "androidx.compose.material3:material3" }
dependencies {
implementation libs.bundles.android.core
implementation platform(libs.compose.bom)
implementation libs.compose.material3
}
[plugins]
[versions]
android-plugin = "8.0.0"
[plugins]
android-application = { id = "com.android.application", version.ref = "android-plugin" }
plugins {
alias(libs.plugins.android.application)
// 適応しない場合
alias(libs.plugins.android.application) apply false
}
[bundles]
[versions]
compose = "2022.10.00"
[libraries]
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose" }
compose-material3 = { module = "androidx.compose.material3:material3" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
[bundles]
compose-core = ["compose-material3", "compose-ui-tooling-preview"]
dependencies {
implementation platform(libs.compose.bom)
implementation libs.bundles.compose.core
}
4. 置換後
[versions]
app-compileSdk = "33"
app-minSdk = "27"
app-targetSdk = "33"
app-versionCode = "1"
app-versionName = "1.0"
android-plugin = "8.0.0"
androidx-activity = "1.5.1"
androidx-core = "1.8.0"
androidx-lifecycle = "2.5.1"
androidx-savedstate = "1.2.0"
compose = "2022.10.00"
compose-compiler = "1.3.2"
espresso-core = "3.5.1"
junit = "4.13.2"
junit-ext = "1.1.5"
kotlin = "1.7.20"
kotlin-coroutine = "1.6.4"
[libraries]
androidx-activity-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso-core" }
androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "junit-ext" }
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose" }
compose-material3 = { module = "androidx.compose.material3:material3" }
compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
junit = { module = "junit:junit", version.ref = "junit" }
kotlin-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "kotlin-coroutine" }
kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }
kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" }
[bundles]
android-core = ["androidx-core-ktx", "androidx-lifecycle-runtime-ktx"]
compose-core = ["compose-material3", "compose-ui-tooling-preview"]
compose-debug = ["compose-ui-tooling", "compose-ui-test-manifest"]
[plugins]
android-application = { id = "com.android.application", version.ref = "android-plugin" }
android-library = { id = "com.android.library", version.ref = "android-plugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace 'com.example.sampleapp'
compileSdk libs.versions.app.compileSdk.get().toInteger()
defaultConfig {
applicationId "com.example.sampleapp"
minSdk libs.versions.app.minSdk.get().toInteger()
targetSdk libs.versions.app.targetSdk.get().toInteger()
versionCode libs.versions.app.targetSdk.get().toInteger()
versionName libs.versions.app.versionName.get()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion libs.versions.compose.compiler.get()
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation libs.bundles.android.core
implementation libs.androidx.activity.activity.compose
implementation platform(libs.compose.bom)
implementation libs.bundles.compose.core
testImplementation libs.junit
androidTestImplementation libs.junit
androidTestImplementation libs.androidx.test.espresso.core
androidTestImplementation platform(libs.compose.bom)
androidTestImplementation libs.compose.ui.test.junit4
debugImplementation libs.bundles.compose.debug
}
自分はこの後Groovy → KTSに移行しています
Renovateの導入
- https://github.com/apps/renovate からInstall
- Repositoryを選択(全てのRepositoryも可能)
- しばらく待っていると該当のRepositoryにPRが建てられます
- 差分はこれだけ
+ {
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "config:base"
+ ]
+ }
- マージすれば定期的にライブラリにアップデートがないかチェックしてくれるようになります。
今回は現状のAndroid studio Flamingoがgradle 8.1に対応していないのでcloseしています
最少であって8.1には対応してるかも
今回はVersion catalog+Renovateの導入練習だったので、設定やらなんやらは今回は省きますが、MonorepoでPRをグルーピングしたり、オートマージ、特定バージョンのスキップなど様々な設定ができそうです。
雑感
良かった部分
- 意外とすんなり移行できた。
- 今回はシングルモジュールだし、シンプルなテンプレートで移行しただけかもしれないが、
Toml
ファイルを用意しただけでバージョン管理ができるようになったのは便利だと感じました。
- 今回はシングルモジュールだし、シンプルなテンプレートで移行しただけかもしれないが、
-
[bundles]
でライブラリをまとめられるのが良い。
嬉しくない部分
-
Toml
ファイル内をリファクタしたらリネームやコードジャンプが効かないのがやや面倒ではあった。 -
Toml
ファイルだけでは新しいアップデートが来ているか分かりずらい。- 今回はRenovateを導入して回避できている。
-
buildSrc
の時も分かりずらいからお互い様感はまだある。
-
libs.versions.xxx
で少し記述が長い気がする。- 以前は
Dependencies.compose
の様な書き方で実装していた分、少し読み取りずらい気がしなくもない。
以前はこんな感じの実装だった↓
- 以前は
object Versions {
const val compose = "2023.03.00"
}
object Dependencies {
object Compose {
const val bom = "androidx.compose:compose-bom:${Versions.compose}"
}
}
今後の目標
この記事書きながらアップデートはしており、この後Dangerを突っ込んだりして自分の今後のテンプレート化を目指しています。
いずれはCIの導入して自動テストとかLint、カバレッジの計測とかも書きたい。
Discussion