Android, Gradle: Lazy Configuration に対応する

Gradle の設定 (build.gradle.kts) は、Configuration Cache により設定の解釈を高速化する仕組みがあります。
このとき、設定値を遅延評価する Lazy Configuration に対応した記述をしていると Configuration Cache が効く場面が多くなり、ビルド時間の短縮が見込めます。

この記事では Gradle の Lazy Configuration について解説します。

Android アプリ開発などで build.gradle.kts を設定したり、Gradle Plugin を開発したりする場合に Lazy Configuration の知識があることで高速な Gradle 実行環境を構築できるようになります。

# Configuration Phase と Configuration Cache

Gradle プロジェクトを実行するとき、以下の3つのフェーズがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17931/5e99a794-b0c6-81cf-7797-e6ec9eeee0c0.png)

https://docs.gradle.org/8.2.1/userguide/build_lifecycle.html#sec:build_phases

Configuration Phase は `build.gradle.kts` を解釈し Gradle プロジェクト環境を構築するフェーズです。

Execution Phase は Gradle Task を実行するフェーズです。

Configuration Cache は Configuration Phase の評価結果を記憶しておき、次の Gradle プロジェクト読み込み時に再利用する仕組みです。`build.gradle.kts` の内容が変わっていなければ、Configuration Phase はほぼスキップされ、Gradle プロジェクトの読み込み時間が短縮されます。

現時点の Gradle　8.2.1 ではまだデフォルトで Configuration Cache は無効ですが、Gradle の主なプラグインが Configuration Cache に対応し始めるなど、Configuration Cache が推進される流れがあります。将来的には Configuration Cache がデフォルトで有効になると思われます。

https://docs.gradle.org/8.2.1/userguide/configuration_cache.html

# Lazy Configuration

Configuration Cache をなるべく効かせるためには、Lazy Configuration に対応した記述が必要になります。
たとえば以下のような外部の値が変化すると Configuration Cache を破棄して再度 Configuration Phase を実行する必要がでてきます。

* build.gradle.kts
* Gradle プロパティ (gradle.properties およびコマンドラインオプションの `-P...`)
* 環境変数
* システムプロパティ
* ...

これらの再評価が必要になる値をなるべく遅延評価できるようにする仕組みが Lazy Configuration です。
値の評価を遅らせることで Configuration Phase の再評価が必要となった場合でもなるべく再評価範囲を小さくし、Configuration Phase を高速化させることができます。

https://docs.gradle.org/8.2.1/userguide/lazy_configuration.html

## Provider と Property

Lazy Configuration の仕組みには以下の二つがあります。

* `Provider<*>` ([`Interface Provider<T>` ドキュメント](https://docs.gradle.org/8.2.1/javadoc/org/gradle/api/provider/Provider.html))
    * 値の供給元を表す
    * 読み取り専用
* `Property<*>` ([`Interface Property<T>` ドキュメント](https://docs.gradle.org/8.2.1/javadoc/org/gradle/api/provider/Property.html))
    * 値を保持するクラス
    * 読み書き可能

たとえば Property は以下のように使われます。

```kotlin
// build.gradle.kts
abstract class Greeting : DefaultTask() { 
    @get:Input
    abstract val greeting: Property<String> 

    @Internal
    val message: Provider<String> = greeting.map { it + " from Gradle" } 

    @TaskAction
    fun printMessage() {
        logger.quiet(message.get())
    }
}

tasks.register<Greeting>("greeting") {
    greeting = "Hi" // greeting.set("Hi") の Kotlin 向けシンタックスシュガー
}
```

※`greeting = "Hi"` は `greeting.set("Hi")` の [Kotlin lazy property assignment](https://docs.gradle.org/8.2.1/userguide/kotlin_dsl.html#kotdsl:assignment) 表記です

上記の Gradle スクリプトは `printMessage()` のみ Execution Phase ですが、それ以外はすべて Configuration Phase で実行されます。
`greeting` の値は `message.get()` によりはじめて評価されて `Hi from Gradle` 文字列が生成されます。

ここでたとえば greeting を Gradle Property から取得し、さらに Lazy Configuration されるようにすると以下のようになります。

```kotlin
// build.gradle.kts
abstract class Greeting : DefaultTask() { 
    @get:Input
    abstract val greeting: Property<String> 

    @Internal
    val message: Provider<String> = greeting.map { it + " from Gradle" } 

    @TaskAction
    fun printMessage() {
        logger.quiet(message.get())
    }
}

tasks.register<Greeting>("greeting") {
    val property: Provider<String> = providers.gradleProperty("greeting")
    greeting = property // greeting.set(property) の Kotlin 向けシンタックスシュガー
}
```

Configuration Phase にて Gradle Property `greeting` を使うようにしていますが、実際に値が読み取られるのは Execution Phase の `message.get()` です。
Configuration Phase ではその値 (`greeting`) は使われていないことから Configuration Phase の再評価は不要となります。

## Lazy Configuration 対応の Provider

外部環境変数等は以下の Provider 経由でアクセスしましょう。

| API | 説明 |
| --- | --- |
| [`project.providers.gradleProperty(propertyName: String): Provider<String>`](https://docs.gradle.org/8.2.1/javadoc/org/gradle/api/provider/ProviderFactory.html#gradleProperty-java.lang.String-) | Gradle Property アクセス |
| [`project.providers.systemProperty(propertyName: String): Provider<String>`](https://docs.gradle.org/8.2.1/javadoc/org/gradle/api/provider/ProviderFactory.html#systemProperty-java.lang.String-) | System Property アクセス |
| [`project.providers.environmentVariable(variableName: String): Provider<String>`](https://docs.gradle.org/8.2.1/javadoc/org/gradle/api/provider/ProviderFactory.html#environmentVariable-java.lang.String-) | 環境変数アクセス |

プロジェクトディレクトリ構成は以下の Provider (DirectoryProperty / Directory クラス) 経由でアクセスしましょう。

| API | 説明 |
| --- | --- |
| [`project.layout.buildDirectory: DirectoryProperty`](https://docs.gradle.org/8.2.1/javadoc/org/gradle/api/file/ProjectLayout.html#getBuildDirectory--) | `project.buildDir` の Property 版 |
| [`project.layout.projectDirectory: Directory`](https://docs.gradle.org/8.2.1/javadoc/org/gradle/api/file/ProjectLayout.html#getProjectDirectory--) | `project.projectDir` の Property 版 |

`project.rootDir` の Property 版は `project.rootProject.projectDirectory` です。

## Lazy Configuration 対応の Property と File 表現

以下の Property 系クラスは Lazy Configuration に対応しています。

| 型 | 説明 |
| --- | --- |
| `Provider<RegularFile>` | 遅延評価ファイル Provider |
| `Provider<Directory>` | 遅延評価ディレクトリ Provider |
| `RegularFileProperty` | 遅延評価ファイル Property |
| `DirectoryProperty` | 遅延評価ディレクトリ Property |
| `ListProperty<T>` | `List<T>` の遅延評価 Property |
| `SetProperty<T>` | `Set<T>` の遅延評価 Property |

https://docs.gradle.org/8.2.1/userguide/lazy_configuration.html#lazy_configuration_reference

## Provider と Property の値の取り出し

この項目は書きかけですが、具体的に実用例となるコードを書いてあります。

```kotlin
// build.gradle.kts

val fileProvider: Provider<RegularFile> = project.layout.projectDirectory.file("sample.txt")
// 遅延評価の実行 (値の取り出し)
val file: RegularFile = file.get()

// 任意の Property の生成
val myProperty = project.objects.property<String>()
// myProperty の中身を取り出さずに新しい Property を生成する
val myProperty2 = myProperty.map {
  it + " / value2"
}
// 値が空の場合の値の取り出し "else"
val elseValue = myProperty.orElse("else")
// Property のデフォルト値の設定
myProperty.convention("default value")
// デフォルト値の取り出し "default value"
val defaultValue = myProperty.get()

// 値を取り出さずに Gradle プロパティを遅延評価で処理する
// option は Provider でありまだ値は取り出されず評価されていない
val otherProperty: Property<Boolean> = ...
val option: Provider<Boolean> =
    // Gradle プロパティ myFlagOption が存在すれば
    providers.gradleProperty("myFlagOption")
    // true として扱う
    .map { true }
    // myFlagOption が存在しなければ otherProperty から遅延評価させる
    .orElse(otherProperty)
```