skip to content

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?

level: seniorimportance: nice to knowfreq 22%

answer

  1. provider from task output carries dependency
  2. flatMap chains provider→provider
  3. no manual dependsOn needed
  4. share providers across projects, not state
  5. finalizeValueOnRead locks on first read

basics

~20 s

Keep 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 s

Across 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
kotlin
// 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

for a junior

Awareness that task-output providers can be wired into other tasks' inputs.

for a middle

Use flatMap/map correctly and rely on implicit dependencies instead of dependsOn.

for a senior

Design cross-project wiring via shared providers and apply finalizeValueOnRead to prevent ordering bugs.

for a principal

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).

context