How do Provider<T> and Property<T> support configuration avoidance during the configuration phase, and why is calling .get() too early a problem?
answer
- Provider = lazy read-only; Property = writable provider
- set providers, not resolved values
- .get() in config forces evaluation
- map/flatMap keep chain lazy
- provider wiring infers task deps
basics
~10 sProvider/Property hold values lazily — the value is computed only when queried. Wiring tasks with providers (via map/flatMap) instead of calling .get() during configuration keeps work deferred and can avoid realizing producer tasks.
solid answer
~50 sThe Configuration Avoidance API for tasks is only half the story; the **lazy properties** (`Provider<T>`, `Property<T>`) carry the laziness into a task's *inputs and outputs*. A `Property` is a writable provider; you `set(...)` it during configuration and its value is queried later. Crucially you should `set` providers, not resolved values: `outputDir.set(layout.buildDirectory.dir("out"))` keeps the path lazy. Calling `.get()` during the configuration phase forces evaluation immediately — and if the provider chains to another task's output, that *realizes the producer task*, cascading away the avoidance benefit. Instead, transform with `provider.map { }` / `flatMap { }`, which build a lazy chain queried only at execution (or graph) time. This also enables correct task wiring: Gradle infers task dependencies from provider connections, so passing `producer.flatMap { it.outputFile }` as an input both defers work *and* records the dependency automatically.
code
kotlin · 12 linesabstract class Pack : DefaultTask() {
@get:InputFile abstract val source: RegularFileProperty
@get:OutputFile abstract val archive: RegularFileProperty
}
val gen = tasks.register<Generate>("gen")
tasks.register<Pack>("pack") {
// lazy provider wiring -> dependency on 'gen' inferred, gen realized only if pack runs
source.set(gen.flatMap { it.outputFile })
archive.set(layout.buildDirectory.file("dist/app.zip"))
}go deeper
Know Provider/Property hold values lazily and you set them during configuration.
Explain set-the-provider, use map/flatMap, and why .get() during configuration is harmful.
Connect provider wiring to automatic dependency inference and cascading realization avoidance.
Mandate lazy-property patterns in shared plugins so configuration-phase cost and Configuration-Cache compatibility are protected org-wide.
## Lazy configuration model Gradle's lazy configuration is built on two types: - **`Provider<T>`** — a read-only, lazily-evaluated value. Its value is computed when `get()` is called, not when the provider is created. - **`Property<T>`** — a `Provider<T>` you can also write to via `set(...)`. Task input/output properties are typically `Property`/`RegularFileProperty`/`DirectoryProperty`/`ListProperty`/`MapProperty`. These exist so that during the **configuration phase** you can *declare* where values come from without *computing* them. Computation is deferred until the value is genuinely needed — usually during task-graph wiring or execution. ## Why this matters for the configuration phase Configuration runs every build. If you compute expensive values (read files, resolve configurations, query other tasks) eagerly while configuring, you pay it every time, even when the owning task never runs. Lazy properties let that cost move to execution and only for tasks that actually run. ## set providers, not values ```kotlin abstract class Generate : DefaultTask() { @get:OutputFile abstract val outputFile: RegularFileProperty @get:Input abstract val version: Property<String> } tasks.register<Generate>("generate") { // lazy: build dir resolved later, not now outputFile.set(layout.buildDirectory.file("gen/out.txt")) // lazy: project.version read when queried version.set(providers.provider { project.version.toString() }) } ``` ## The .get() trap Calling `.get()` during configuration **forces** evaluation: - It resolves the value immediately, paying any cost now. - If the provider is `someTask.flatMap { it.output }`, calling `.get()` realizes `someTask` — cascading realization and undoing task configuration avoidance. Use `map`/`flatMap`/`zip` to transform lazily instead: ```kotlin val upper = stringProperty.map { it.uppercase() } // still lazy tasks.named<JavaExec>("run") { args(upper.get()) // BAD if in config; OK only inside doFirst/doLast (execution) } ``` Reading a provider inside `doFirst`/`doLast` (execution phase) is fine — by then realization has already happened for tasks that run. ## Automatic dependency inference When you connect an output provider of task A to an input property of task B, Gradle records the A→B dependency automatically — no explicit `dependsOn` needed. This is both a correctness win (no forgotten dependencies) and a laziness win (the wiring is provider-based, not realized). ## registerIfAbsent and shared state Related lazy APIs include build-service registration (`gradle.sharedServices.registerIfAbsent`) which also defers instantiation until first use — the same defer-until-needed principle applied beyond tasks.
- When is it safe to call provider.get()?During the execution phase — inside a task action (doFirst/doLast) or the task's @TaskAction. By then the value is needed and the producing tasks have run. Calling get() during configuration forces eager evaluation and possible task realization.
- How does wiring with providers help correctness, not just performance?Connecting an output Provider of one task to an input Property of another makes Gradle infer the task dependency automatically, eliminating a class of missing-dependsOn bugs.
saying these in an interview costs you the question
- Calling .get() during configuration to populate another property instead of passing the provider.
- Setting properties to already-resolved values (e.g. a String path) instead of lazy providers.
- Believing lazy properties are only about ergonomics, not configuration-phase cost.