When wiring extension values into tasks across projects or when an extension value depends on another task's output, how do you keep the wiring lazy and correct — and what does `finalizeValueOnRead` add?
answer
- provider from task output carries dependency
- flatMap chains provider→provider
- no manual dependsOn needed
- share providers across projects, not state
- finalizeValueOnRead locks on first read
basics
~20 sKeep wiring providers, including ones backed by task outputs — task.input.set(otherTask.flatMap { it.output }) — so Gradle infers task dependencies automatically. finalizeValueOnRead locks a property's value on first read to catch late mutations and avoid inconsistent reads.
solid answer
~40 sAcross projects you still wire providers, but you must avoid eager cross-project access (which also breaks project isolation). Prefer sharing values through providers, e.g. a producer project exposing a `Provider` the consumer wires in. When an extension value is computed from a task's output, wire the task's output provider directly: `consumerTask.input.set(producerTask.flatMap { it.outputFile })`. Because the provider carries a *task dependency*, Gradle automatically runs the producer first — no manual `dependsOn`. For correctness under late configuration, `Property.finalizeValueOnRead()` makes the property resolve and *lock* its value the first time it's read, so a later mutation can't silently change what a task already saw; `finalizeValue()` locks immediately. These guard against order-dependent bugs where two consumers read a property at different lifecycle points and disagree.
code
kotlin · 6 lines// value + implicit dependency, no dependsOn
consumerTask.inputFile.set(producerTask.flatMap { it.outputFile })
// lock a property so late mutation can't change an already-read value
abstract class Ext { abstract val version: Property<String> }
ext.version.finalizeValueOnRead()go deeper
Awareness that task-output providers can be wired into other tasks' inputs.
Use flatMap/map correctly and rely on implicit dependencies instead of dependsOn.
Design cross-project wiring via shared providers and apply finalizeValueOnRead to prevent ordering bugs.
Establish project-isolation-safe wiring conventions and value-finalization policy across a multi-project build.
## Providers carry dependencies A powerful property of Gradle providers is that a provider derived from a task's output **also carries the implicit task dependency**. So wiring: ```kotlin consumerTask.inputFile.set(producerTask.flatMap { it.outputFile }) ``` does two things: it supplies the value lazily *and* tells Gradle that `consumerTask` depends on `producerTask`. You don't write `dependsOn` — the wiring expresses it. Use `flatMap` when the producer exposes a `Provider`/`Property` (you're chaining provider→provider); use `map` when transforming a plain resolved-at-execution value. ## Cross-project wiring and isolation Reaching into another project's mutable state during configuration is fragile and, under stricter isolation, disallowed. The clean pattern is to **share providers**: ```kotlin // producer project val artifact: Provider<RegularFile> = tasks.named<Jar>("jar").flatMap { it.archiveFile } // consumer wires that provider into its own task input ``` This keeps the dependency and the value lazy, and avoids snapshotting another project's configuration too early. ## Finalizing values Lazy properties are mutable until something reads them. That flexibility creates a hazard: consumer A reads the property at one point, consumer B mutates it later, consumer C reads a *different* value. Two methods tame this: - **`finalizeValueOnRead()`** — the property computes and **locks** its value the first time it is read; later attempts to change it are ignored or error. This guarantees every reader sees the same value and surfaces ordering mistakes. - **`finalizeValue()`** — locks the value immediately (resolving its current provider), useful at the end of configuration. Gradle applies `finalizeValueOnRead` automatically to many task input properties so task inputs are stable once observed. ## Putting it together ```kotlin abstract class ConsumerTask : DefaultTask() { @get:InputFile abstract val inputFile: RegularFileProperty } tasks.register("consume", ConsumerTask::class.java) { // value + automatic dependency on the producer it.inputFile.set(producer.flatMap { p -> p.outputFile }) } ``` ## Takeaways - Wire task-output providers to get value **and** dependency for free. - Use `flatMap` to chain provider-to-provider; `map` to transform values. - Share providers across projects instead of reaching into foreign state. - Use `finalizeValueOnRead`/`finalizeValue` to lock values and catch late mutations.
- Why don't you need `dependsOn(producerTask)` when wiring its output provider?A provider derived from a task's output carries the task dependency; wiring it into an input makes Gradle schedule the producer first automatically.
- When do you use `flatMap` vs `map`?`flatMap` when the producer hands you another provider/property (chaining provider→provider); `map` when you transform a plain value the provider will yield.
- What does `finalizeValueOnRead` protect against?Order-dependent inconsistency: it locks the value on first read so a later mutation can't silently give different readers different values.
saying these in an interview costs you the question
- Adding `dependsOn` manually instead of letting the wired output provider express the dependency.
- Reaching into another project's mutable extension during configuration.
- Confusing `map` (value transform) with `flatMap` (provider chaining).