Understanding when values in your build are computed is crucial for writing fast and reliable build logic. Gradle differentiates between eager and lazy evaluation:

Eager Evaluation

Values are computed immediately during the configuration phase, before tasks ever run.

Eager evaluation comes with drawbacks:

  • Slows down builds — even when tasks aren’t executed.

  • Breaks compatibility — with and configuration caching.

Essentially, eager evaluation performs unnecessary work and reduces flexibility.

Lazy Evaluation

Lazy values are computed on-demand, typically during the execution phase, using Gradle’s Provider API (Provider<T> and Property<T>).

This approach has key benefits:

  1. Deferred resolution — values are only computed when needed.

  2. Better performance — avoids unnecessary computation during configuration.

Lazy evaluation enables build features like incremental builds, build caching, and configuration cache compatibility.

Lazy APIs

Lazy APIs were explicitly designed to avoid configuring tasks that aren’t going to be run, especially in large projects or Android builds where configuration overhead can be significant.

For example, you might use a String in your build script, but this value is eagerly evaluated during the configuration phase. Instead, you should use Gradle’s Property<String> type, which defers evaluation until it’s needed during task execution.

Another example which is a common pitfall is using tasks.withType(Test) {}:

tasks.withType(Test) {
    // configure all tasks eagerly
}

This eagerly configures all Test tasks, even in builds where those tasks may never run. Instead, use configureEach:

tasks.withType(Test).configureEach {
    // configure only those tasks actually needed
}

Or:

tasks.named("test") {
    // configure only if task will be executed
}

This use of configureEach and tasks.named(…​) ensures configurations only apply to tasks invoked by the build.