バージョンカタログの導入と運用方法の紹介


この記事は [Android Advent Calendar 2023](https://qiita.com/advent-calendar/2023/android) 7日目の記事です。

こんにちは。[株式会社コネヒト](https://connehito.com/)に所属しているAndroidエンジニアの関根です！
 [Android Advent Calendar 2023](https://qiita.com/advent-calendar/2023/android)の存在に気づき、ちょうど7日目だけ空いていたので大急ぎでこの記事を書いています。
 今回は所属するコネヒトで開発しているAndroid版ママリアプリで実施した、バージョンカタログの導入について紹介します。少しでもみなさんの参考になれば嬉しく思います。

## モチベーション
コネヒトではママリアプリのマルチモジュール化を目指しています。
その前段階として、外部ライブラリの依存関係を集中管理する状態を作り、段階的にマルチモジュール化を進めることを狙っています。


## そもそもバージョンカタログとは？
Gradle 7.0からfeature previewとして導入され、Grade 7.6にて正式導入された機能で、外部ライブラリの依存関係を一括管理する方法です。
旧来の方法では、外部ライブラリの共通バージョニングに対応するためにGradle上に変数を定義したり、マルチモジュール環境ではそれぞれのモジュールで依存関係を定義する必要があるなど、少し冗長な方法を取る必要がありましたが、バージョンカタログはこの課題を解決してくれます。
これだけでも便利な機能だと思えますが、もう少し踏み込んでメリットを整理してみます。

## バージョンカタログのメリット

https://docs.gradle.org/current/userguide/platforms.html

Gradleのサイトにあるメリットを直訳すると以下の通りです。

- Gradle はカタログごとにタイプセーフなアクセサーを生成するため、IDE でオートコンプリートを使用して依存関係を簡単に追加できます。
- 各カタログは、ビルドのすべてのプロジェクトに表示されます。これは、依存関係のバージョンを宣言し、そのバージョンへの変更がすべてのサブプロジェクトに適用されることを確認するための中心的な場所です。
- カタログでは、一般的に一緒に使用される「依存関係のグループ」である依存関係バンドルを宣言できます。
- カタログでは、依存関係のグループと名前を実際のバージョンから分離し、代わりにバージョン参照を使用できるため、複数の依存関係間でバージョン宣言を共有できます。

>For each catalog, Gradle generates type-safe accessors so that you can easily add dependencies with autocompletion in the IDE.
>
>Each catalog is visible to all projects of a build. It is a central place to declare a version of a dependency and to make sure that a change to that version applies to every subproject.
>
>Catalogs can declare dependency bundles, which are "groups of dependencies" that are commonly used together.
>
>Catalogs can separate the group and name of a dependency from its actual version and use version references instead, making it possible to share a version declaration between multiple dependencies.

それぞれ簡単に整理してみます。


## IDEでのオートコンプリート

特定のスクリプトファイルの拡張プロパティで、バージョン数値を管理し参照する方法もありますが、オートコンプリートには対応しておらず、別ファイルを確認する必要があります。それを、以下の画像のように、IDEがフォローしてくれるようになる機能です。ちなみに宣言に移動などもサポートしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55418/8fa7ad18-9d19-531f-e24c-e65d0e4ca04b.png)

なお、筆者は最初オートコンプリートが機能しませんでしたが、IDEのアップデートをしたところ無事に機能しました。

## 依存関係のバージョン宣言とサブプロジェクトへの適用

依存関係の宣言を集約する機能です。例えば、以下のように、`settings.gradle`に依存関係の定義を書いておくと他の全てのモジュールから参照できるようになります。

```groovy
// [settings.gradle]
dependencyResolutionManagement {
    versionCatalogs {
        libs {
            library('room-runtime', 'androidx.room:room-runtime:x.x.x')
            library('room-ktx', 'androidx.room:room-ktx:x.x.x')
            library('room-rxjava3', 'androidx.room:room-rxjava3:x.x.x')
        }
    }
}

// [app/build.gradle]
dependencies {
    implementation libs.room.runtime
    implementation libs.room.ktx
    implementation libs.room.rxjava3
}

// [hoge_module/build.gradle]
dependencies {
    implementation libs.room.runtime
    implementation libs.room.ktx
    implementation libs.room.rxjava3
}
```

`versionCatalogs`の定義により、エイリアスとしてアクセサーが提供され、参照側ではバージョンの指定は不要となります。エイリアスは `-`  or `_` or `.` を区切り文字として、マッピングが行われ、例えば、`room-runtime`という定義であれば、`room.runtime`として展開されます。

なお、`settings.gradle`に記述する以外に、tomlファイルに定義することが可能で、
`gradle/libs.versions.toml`のパスにファイルを配置すると、デフォルトで参照してくれます。

```yaml
# ./gradle/libs.versions.toml

[versions]

[libraries]
room-runtime = { group = "androidx.room", name = "room-runtime", version = "x.x.x" }
room-ktx = { group = "androidx.room", name = "room-ktx", version = "x.x.x" }
room-rxjava3 = { group = "androidx.room", name = "room-rxjava3", version = "x.x.x" }

[bundles]
```

## 複数の依存関係間でバージョン宣言の共有
複数のモジュールで、共通のバージョニングルールが適用されているライブラリを、一つの宣言で管理できるようになる機能です。

```yaml
[versions]
# room用のバージョンを宣言
room = "x.x.x"

[libraries]
# それぞれ、version.refにversion.roomを指定
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-rxjava3 = { group = "androidx.room", name = "room-rxjava3", version.ref = "room" }

[bundles]
```

Android開発では、共通バージョニングのライブラリも多いので、宣言的にルールをつけられると管理の手間を下げられると思います。なお、Renovateもバージョンカタログに対応しており、tomlファイルに対してのPRも生成してくれます。

https://github.com/renovatebot/renovate/blob/main/docs/usage/java.md#gradle-file-support


## 依存関係バンドルの宣言
最後は、いくつかのライブラリをまとめて利用するケースに対応する機能です。
例えば、`room-runtime`, `room-ktx`, `room-rxjava3`を一緒に利用している場合は以下のようになります

```yaml
# ./gradle/libs.versions.toml

[versions]
room = "x.x.x"

[libraries]
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-rxjava3 = { group = "androidx.room", name = "room-rxjava3", version.ref = "room" }

[bundles]
# room-runtime, room-ktx, room-rxjava3をバンドル宣言
room = [
    "room-runtime",
    "room-ktx",
    "room-rxjava3",
]

// [app/build.gradle]
dependencies {
    # room-runtime, room-ktx, room-rxjava3が全て参照される
    implementation libs.bundle.room
}
```

バージョンカタログを導入することで得られるメリットについて書かせていただきましたが
この後は、ママリアプリでの運用事例について紹介したいと思います。

## コネヒトでのバージョンカタログの定義を紹介

ママリアプリのバージョンカタログの定義を抜粋すると以下の通りです。

```yaml
[versions]

sdk-compile = "34"
sdk-min = "28"
sdk-target = "34"

kotlin = "x.x.xx"
kotlin-coroutines = "x.x.x"

androidx-compose-bom = "xxxx.xx.xx"

media3 = "x.x.x"
firebase = "x.x.x"

[libraries]

# Kotlin
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }

# AndroidX Compose
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" }
androidx-compose-material = { module = "androidx.compose.material:material" }
androidx-compose-material-icons = { module = "androidx.compose.material:material-icons-extended" }

# Media3
media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
media3-exoplayer-hls = { module = "androidx.media3:media3-exoplayer-hls", version.ref = "media3" }
media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3" }


# Test Libs
androidx-junit = { module = "androidx.test.ext:junit", version = "1.1.3" }

[plugins]
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

[bundles]
kotlin = [
    "kotlin-stdlib",
    "kotlin-reflect",
]

media3 = [
    "media3-exoplayer",
    "media3-exoplayer-hls",
    "media3-ui",
]

app = [
    "androidx-compose-material",
    "androidx-compose-material-icons",
    "androidx-compose-runtime",
    "androidx-compose-ui",
    "androidx-compose-ui-tooling",
    "androidx-compose-ui-tooling-preview",
]
app-test = [
    "androidx-junit",
]

```

利用側は以下のように指定を行います。

```yaml
android {
    ## SDKの指定はInteger値として取る
    compileSdk libs.versions.sdk.compile.get().toInteger()

    defaultConfig {
        minSdkVersion libs.versions.sdk.min.get().toInteger()
        targetSdkVersion libs.versions.sdk.target.get().toInteger()
    }
}

dependencies {
    ## bomはplatform指定をする
    implementation platform(libs.androidx.compose.bom)
    implementation platform(libs.firebase.bom)
    
    ## アプリモジュールでまとめて指定する
    implementation(libs.bundles.app)
    
    # Media3は個別で指定しないと、動画表示時にクラッシュする
    implementation libs.bundles.media3
}
```

この構成の意図は外部ライブラリの変更を、`libs.versions.toml`だけで完結できる状態にするというものです。変更対象のファイルを減らすことで、軽微でも作業をしやすくなる効果はあるかと考えています。media3のbundleを個別定義していますが、`app`バンドルにまとめて定義すると、media3の利用時に必要なtargetCompatibilityの指定が反映されず、動画プレイヤー呼び出し時にクラッシュバグが発生したことが理由です。今のところ個別定義の解決方法しか見つかっておりません・・・。

## Renovateでの依存ライブラリ更新の運用

コネヒトでは、 Renovateを利用して、依存ライブラリに新しいバージョンがある場合、自動でPullRequestを送る仕組みを導入しており、その運用も`libs.versions.toml`に対して作業を行なっています。
具体的な運用としては、1週間に一度、Androidエンジニアが集まるミーティングの中で、時間を割いて一つ一つのリリースノートを確認し、アプリに影響がない場合にはその場でマージしています。

やることは以下の通りです

1. Renovateの作成したPullRequestを開く
2. リリースノートのリンクをクリックして差分を確認する
3. アプリへの影響がなさそうであれば、その場でマージする
   1. 影響があれば別Issueを切ったり、コメントを残しておく

それぞれ紹介していきます。

### 1. Renovateの作成したPullRequestを開く

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55418/95ee2787-fceb-0275-3a47-d67dd896a354.png)


Renovateが作ったPullRequestです。
リリースノートへのリンクが記載されており、差分の確認が捗ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55418/a1ad32d6-b561-3bb2-7b51-24c5337185a5.png)

先述した通り、`libs.versions.toml`のみが更新されます。
`[versions]`のセクションにバージョン定義があればその行が更新され、共通バージョニングのライブラリは一括更新されます。
他の作業とのコンフリクトも少なくなり作業がしやすくなったと感じています。

### 2. リリースノートのリンクをクリックして差分を確認する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55418/99924260-0dbb-c80a-73bb-6bb3892f1131.png)

リリースノートの一覧や元サイトへのリンクがあるので、変更内容の確認と、ママリアプリへの影響を確認します。
影響がある場合や、他のライブラリに依存する場合、例えばKotlinのバージョンアップが必要な場合などは、別Issueを作り別途対応を進めるようにします。

### 3. アプリへの影響がなさそうであれば、その場でマージする
影響がない場合は、その場でマージをします。
現状は1週間にだいたい2、3個のライブラリを更新できている状態です。
反面、直近では別ライブラリの依存で更新できないものが増えてきて、そちらの対応を進める必要が出てきた状況です。


## まとめ
今回はコネヒトにおけるバージョンカタログの定義の紹介と、運用方法について紹介させていただきました。
バージョンカタログを導入したことで、共有バージョニングのライブラリに対してもシンプルな運用方法を実現できているかと思います。
今後マルチモジュール化を進める上での、より良い運用方法にアップデートしていけたらと思います。

お読みいただきありがとうございました！