Androidアプリ開発探求記（その８）

# 概要
[前回](https://qiita.com/beyondseeker/items/b998a418aaf37bd91837)は、buildSrc を導入しつつ、build.gradle を build.gradle.kts に書き換えてみました。

今回は、*buildSrc* 側で依存関係を一元管理する方法を模索してみようと思います。

# 前回の懸念点
前回は、以下のような懸念点を上げました。

## ◆ extension による可読性の低下
extension を使うと可読性が下がるように思えます。

```kotlin
plugins {
    id("com.android.application")
    // TODO: この extension って逆にわかりづらい気がする。
    kotlin("android")
    id("kotlin-android-extensions")
}
```

```kotlin
plugins {
    id("com.android.application")
    // TODO: こっちのほうが分かりやすいように思える。
    id("org.jetbrains.kotlin.android")
    id("kotlin-android-extensions")
}
```

## ◆ extension のメリット消失
extension のメリットとしては、コードの短縮や typo の発生確率の減少などがありますが、deps を導入して IDE のコード補完が利用可能になると、extension のメリットがほぼなくなるのではないでしょうか？

```kotlin:buildSrc/src/main/java/Dependencies.kt（前回コメントしておいたdeps導入検討コメント）
object versions {
    const val kotlin = "1.3.72"
}

// TODO: 要検討
//object deps {
//}
```

# ネット上を物色してみる
Dependencies.kt でググって、検索されたものを無条件で先頭から物色し、目についた点をメモしてみます。[^1]

## ◆ [kotlinpoet](https://github.com/square/kotlinpoet/blob/fab520162bccbd70686e26bd4544e66f69d08269/buildSrc/src/main/java/Dependencies.kt)
- object をネスト

```kotlin
object versions {
  object kotlin {
    const val plugin = "1.4-M2"
    const val libs = "1.3.72"
  }
  const val spotless = "3.27.0"
  const val ktlint = "0.36.0"
}
```

## ◆ [passiondroid](https://gist.github.com/passiondroid/1277fbf713d857818f1f9d9494faf66e)
- minSdk 等も一元管理している。

```kotlin
object AndroidSdk {
    const val min = 15
    const val compile = 28
    const val target = compile
}
```

## ◆ [chrisbanes/tivi](https://github.com/chrisbanes/tivi/blob/e9d71b84792d74c35bae38641c1620bc44a6f878/buildSrc/src/main/java/app/tivi/buildsrc/dependencies.kt)
- Libs のネスト内部に version を含めている。

```kotlin
    object Kotlin {
        private const val version = "1.4.0-rc"
        const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$version"
        const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$version"
        const val extensions = "org.jetbrains.kotlin:kotlin-android-extensions:$version"
    }
```

## ◆ [adrielcafe/kaptain](https://github.com/adrielcafe/kaptain/blob/401d6f02e39820ab5a2453c6247f9a1c610df290/buildSrc/src/main/kotlin/Dependencies.kt) 
- version を internal にしている

```kotlin
internal object Version {
    const val GRADLE_ANDROID = "3.6.1"
    const val GRADLE_KTLINT = "9.2.1"
}
```

## ◆ [nrohmen](https://gist.github.com/nrohmen/dee8fdca4ac3fbc31e4a5b18ce7a16b7)
- BuildPlugins とか TestLibs とか分けている。

## ◆ BuiltinPluginIdExtensions.kt
プラグインに含まれている。org.gradle.kotlin.dsl.BuiltinPluginIdExtensions.kt

```kotlin
// 下記のように定義されていて、
inline val org.gradle.plugin.use.PluginDependenciesSpec.`java-gradle-plugin`: org.gradle.plugin.use.PluginDependencySpec
    get() = id("org.gradle.java-gradle-plugin")

// このように id を用いずに、
id("org.gradle.java-gradle-plugin")

// 下記のように書ける。しかし、IDEの補完が効かないのが致命的に使いづらい。
`java-gradle-plugin`
```

# 一人ブレスト
- 今回は、依存関係以外(minSdkVersion等)は扱わず、依存関係だけ扱う。
- classpath, plugin の id, dependencyNotation に分けて考える。
- object として、classpath は Classpath, plugin の id は Plugin, dependencyNotation は Deps にまとめてみよう。
- 今回はIDEの補完が最も重要になるので、現状で補完の効かないバッククウォーテーション方式でハイフンを入れたりするのは却下。
- object のネストはツリー構造でさも簡潔に管理できそうに見えるが、同種のまとまりがツリー構造に収まる保証はどこにもない。つまり、保守が破綻する可能性がある。
- object のネストは、かならずドットでネストするのがよいとは限らないし、セミコロンでネストするのがよいとも限らない。ライブラリの追加や統廃合でネスト箇所が変わる可能性もある。また、ドットとセミコロンで必ずネストさせるといったルールでもあればよいが、少なくともそのようなケースをネット上で見方ことは無く、個人の裁量で適宜名称の簡略化をしている。そのため、保守にリスクが付きまとう。
- object のネストが深いと、ネストのたびに補完が必要になってしまうので無駄な手間が増える。
- 命名に関して、物色してきたソースでは、ほぼ全て、適度に省略した名称を自作していた。これは作った本人にとってはわかりやすいかもしれない。しかし、初見の保守者にとって知る由もなし。例えば、下記のように定義している場合、*Libs* はアプリの設計書に書かれているべき内容だし保守者は知っておくべきものなので問題無いが、その後に *Kotlin* や *stdlib* という文字列が続くかどうかなど、エスパーでもない限りわかるはずもない。しかも、*stdlib* に *jdk8* が付与されていることなどエスパーでも見落とすかもしれない。

```kotlin
object Libs {
    object Kotlin {
        private const val version = "1.4.0-rc"
        const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$version"
        const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$version"
        const val extensions = "org.jetbrains.kotlin:kotlin-android-extensions:$version"
    }
}
```

- 結局、初見の（もしくはプロジェクトで独自に作成された命名が脳みそからスワップアウトした）保守者がどのように行動するかというと、*org.jetbrains.kotlin:kotlin-stdlib* という文字列を IDE 上で検索をかけ、Dependencies.kt に飛び、そこから、*Libs.Kotlin.stdlib* という参照を発見するわけだ。作業コストとしては、*org.jetbrains.kotlin:kotlin-stdlib-jdk8* を直接書いたほうが圧倒的に早い。つまり、無駄な工数を強いているということになる。しかし、version が一元管理されているという点は大きなメリット。ただ、バージョンがツリー構造に合致する保証などどこにもない。つまり、*Libs.Hoge* 配下と *Libs.Fuga* 配下で version がクロスオーバーする可能性があるということ。なので、ツリー構造による保守は破綻する可能性がある。しかし、クロスオーバーしない場合に限りツリー構造上に配置するという折衷案はそれなりに機能しそうな気がする。特に、バージョンを単体で外に配置した場合、名前空間や命名にかなりの難があるので。でも、今回はバージョン情報を含めるような階層構造はなさそう。
- 前回からの懸念であった extension に関しても、plugin 側が extension をこっそり追加したところで、保守者は知らないので、まずは dependencyNotation の文字列で検索をかけたりググったりして extension を知る的な本末転倒なことになるので、使わないほうがいいと思われる。
- しかしながら、dependencyNotation の文字列をそのまま使えばいいかというと、typo のリスクがあるし、補完も効かないのでダメ。
- だったら、補完により typo のリスクを無くし、初見でも使え、version の一元管理もできるという方法があればいいんじゃね？
- 変数名を、dependencyNotation の文字列から単純かつ機械的なルールで命名するようにすれば初見でも変数名が分かるし、補完で typo リスクがなくなるし、変数に version も含まれていればバージョンの一元管理のメリットも得られるんじゃね？

# 実装
ということで、現行のコードをベースとして、それっぽく実装してみることにします。
[diff](https://github.com/beyondseeker/chrono0015/compare/d076850c8ad307ce6bdcd5da3710a41117423d80..9ed6e89979d9e3833532211883f64562ae3d7985)

## ◆ build.gradle.kt

### ☆ 変更前
```kotlin
buildscript {
    dependencies {
        classpath("com.android.tools.build:gradle:4.0.1")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}")
    }
}
```

### ☆ 変更後
```kotlin
buildscript {
    dependencies {
        classpath(Classpaths.com_android_tools_build__gradle)
        classpath(Classpaths.org_jetbrains_kotlin__kotlin_gradle_plugin)
    }
}
```

## ◆ app/build.gradle.kt
### ☆ 変更前
```kotlin
plugins {
    id("com.android.application")
    // TODO: この extension って逆にわかりづらい気がする。
    kotlin("android")
    id("kotlin-android-extensions")
}

dependencies {
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.0.10")

    implementation("androidx.appcompat:appcompat:1.1.0")
    implementation("androidx.constraintlayout:constraintlayout:1.1.3")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.kotlin}")

    testImplementation("junit:junit:4.12")

    androidTestImplementation("androidx.test.espresso:espresso-core:3.2.0")
    androidTestImplementation("androidx.test.ext:junit:1.1.1")
}
```

### ☆ 変更後
```kotlin
plugins {
    id(Plugins.com_android_application)
    id(Plugins.org_jetbrains_kotlin_android)
    id(Plugins.kotlin_android_extensions)
}

dependencies {
    coreLibraryDesugaring(Deps.com_android_tools__desugar_jdk_libs)

    implementation(Deps.androidx_appcompat__appcompat)
    implementation(Deps.androidx_constraintlayout__constraintlayout)
    implementation(Deps.org_jetbrains_kotlin__kotlin_stdlib_jdk8)

    testImplementation(Deps.junit__junit)

    androidTestImplementation(Deps.androidx_test_espresso__espresso_core)
    androidTestImplementation(Deps.androidx_test_ext__junit)
```

## ◆ Dependencies.kt
[soruce](https://github.com/beyondseeker/chrono0015/blob/cb41872338a51a2a13ed286f6ea734cf7092f0e2/buildSrc/src/main/java/Dependencies.kt)

```kotlin
/**
 * 複数個所で利用されるバージョン情報
 */
private object Versions {
    const val kotlin = "1.3.72"
}

/**
 * DependencyHandler.add(CLASSPATH_CONFIGURATION, dependencyNotation) の dependencyNotation として
 * 扱われる文字列です。
 *
 * 変数の命名手順：
 *   - <groupId>:<artifactId>:<version> のうち、:<version> を削除する。
 *   - colon を 2連続の underscore に変換し、それ以外の編巣名に利用不可能な文字を underscore に変換する。
 *
 * 利用例：
 *   classpath(Classpaths.com_android_tools_build__gradle)
 */
object Classpaths {
    const val com_android_tools_build__gradle = "com.android.tools.build:gradle:4.0.1"
    const val org_jetbrains_kotlin__kotlin_gradle_plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}"
}

/**
 * PluginDependenciesSpecScope.id(id) の id として扱われる文字列です。
 *
 * 変数の命名手順：
 *   - colon を 2連続の underscore に変換し、それ以外の変数名に利用不可能な文字を underscore に変換する。
 *
 * 利用例：
 *   id(Plugins.com_android_application)
 */
object Plugins {
    const val com_android_application = "com.android.application"
    const val kotlin_android_extensions = "kotlin-android-extensions"
    const val org_jetbrains_kotlin_android = "org.jetbrains.kotlin.android"
}

/**
 * dependencyNotation として扱われる文字列です。
 *
 * 変数の命名手順：
 *   - <groupId>:<artifactId>:<version> のうち、:<version> を削除する。
 *   - colon を 2連続の underscore に変換し、それ以外の変数名に利用不可能な文字を underscore に変換する。
 *
 * 利用例：
 *   coreLibraryDesugaring(Deps.com_android_tools__desugar_jdk_libs)
 *   implementation(Deps.androidx_appcompat__appcompat)
 *   testImplementation(Deps.junit__junit)
 *   androidTestImplementation(Deps.androidx_test_espresso__espresso_core)
 */
object Deps {
    const val androidx_appcompat__appcompat = "androidx.appcompat:appcompat:1.1.0"
    const val androidx_constraintlayout__constraintlayout = "androidx.constraintlayout:constraintlayout:1.1.3"
    const val androidx_test_espresso__espresso_core = "androidx.test.espresso:espresso-core:3.2.0"
    const val androidx_test_ext__junit = "androidx.test.ext:junit:1.1.1"
    const val com_android_tools__desugar_jdk_libs = "com.android.tools:desugar_jdk_libs:1.0.10"
    const val junit__junit = "junit:junit:4.12"
    const val org_jetbrains_kotlin__kotlin_stdlib_jdk8 = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlin}"
}

```

# まとめ
今回は、*buildSrc* の *Dependencies.kt* にて、依存関係の一元管理をしてみました。

次回は、ライブラリのバージョン管理に関して模索してみようと思います。

[^1]: 思った以上に多くの老舗のプロジェクトは kts 化されておらず、buildSrc も使われてないですね、、、。老舗だからこそむやみやたらに kts 化とかしないんだろうな、、、。