skip to content

How do you wire one task to depend on or consume the output of another in the Kotlin DSL while preserving configuration avoidance?

level: seniorimportance: should knowfreq 40%

answer

  1. keep TaskProvider, don't .get()
  2. dependsOn accepts a provider
  3. flatMap output → input wires + infers dependency
  4. map for values, flatMap for providers
  5. data wiring beats manual dependsOn

basics

~10 s

Keep references as TaskProviders from register/named and pass the provider (or its flatMap output) to dependsOn or an input property. Avoid calling .get(), which realizes the task.

solid answer

~40 s

Hold tasks as `TaskProvider`s (from `register`/`named`) and wire them *through the provider*, never through a realized instance. `dependsOn(someProvider)` accepts a provider and won't realize it until the depending task runs. For data wiring, use the provider chain: `inputFile.set(otherTaskProvider.flatMap { it.outputFile })` keeps everything lazy and also establishes an implicit task dependency automatically. The anti-pattern is calling `.get()` (or using an eager accessor / `getByName`) to grab the instance, which realizes the producing task during configuration. Lazy wiring means a task and its producers are only created if the consumer is actually scheduled, and Gradle infers the dependency edge from the `Provider` linkage so you often don't even need an explicit `dependsOn`.

code

kotlin · 8 lines
kotlin
val generate = tasks.register<GenerateTask>("generate") {
    outputFile.set(layout.buildDirectory.file("gen/out.txt"))
}

tasks.register<PackageTask>("package") {
    // implicit dependency on 'generate' via the provider chain — no dependsOn needed
    source.set(generate.flatMap { it.outputFile })
}

go deeper

for a junior

Know you can dependsOn another task and should pass the provider, not the realized task.

for a middle

Explain provider-based input/output wiring and why .get() is bad during configuration.

for a senior

Design tasks with lazy Property inputs/outputs so dependencies are inferred via flatMap and stay configuration-avoidant.

for a principal

Establish provider-wiring as the standard for custom tasks/plugins; review for stray .get()/eager accessors that inflate configuration time and break laziness.

## The goal: lazy producer/consumer wiring Tasks frequently depend on each other — a packaging task needs a code-gen task's output; a publish task needs a signing task to finish. You want these edges declared **without realizing** any task that the current invocation won't run. ## Two kinds of wiring ### 1. Ordering / lifecycle dependency — `dependsOn` `dependsOn` accepts many things, including a `TaskProvider`: ```kotlin val generate = tasks.register<GenerateTask>("generate") val compile = tasks.named<JavaCompile>("compileJava") compile.configure { dependsOn(generate) } // provider, stays lazy ``` Passing the *provider* (not `generate.get()`) means `generate` is realized only if `compileJava` is actually realized and run. ### 2. Data wiring with providers — preferred Better than a manual `dependsOn` is wiring the **data**, which makes Gradle infer the dependency automatically. Modern tasks expose lazy `Property`/`Provider` inputs and outputs: ```kotlin abstract class GenerateTask : DefaultTask() { @get:OutputFile abstract val outputFile: RegularFileProperty } abstract class PackageTask : DefaultTask() { @get:InputFile abstract val source: RegularFileProperty } val generate = tasks.register<GenerateTask>("generate") { outputFile.set(layout.buildDirectory.file("gen/out.txt")) } tasks.register<PackageTask>("package") { // flatMap chains providers AND records the task dependency implicitly source.set(generate.flatMap { it.outputFile }) } ``` Because `source` is fed from `generate`'s output provider, Gradle knows `package` depends on `generate` — no explicit `dependsOn` needed — and nothing is realized until `package` runs. ## Why `.get()` is the trap `generate.get()` forces the task to be created and configured *now*. Any of these defeat avoidance: `.get()`, `getByName`, eager accessors (`tasks.generate`), `withType<T> { }`, `tasks.all { }`. Keep the value as a `Provider` as long as possible; only the framework should realize it. ## map vs flatMap - Use `provider.map { }` when the transform returns a plain value. - Use `provider.flatMap { }` when the task's property is itself a `Provider`/`Property` (the common case for output files), so you don't end up with a `Provider<Provider<T>>`. ## Summary rules - Always keep `TaskProvider` references; pass them to `dependsOn`. - Prefer data wiring (`set(producer.flatMap { it.output })`) over manual `dependsOn`. - Never call `.get()` during configuration to wire tasks.

  • Why does set(producer.flatMap { it.output }) avoid a manual dependsOn?
    Feeding a task input from another task's output Provider lets Gradle infer the task dependency automatically, so the edge and the data wiring are declared together — lazily.
  • What is wrong with passing generate.get() to dependsOn?
    .get() realizes the producing task during configuration, defeating configuration avoidance even when the consumer never runs.
  • When do you use map vs flatMap on a provider?
    map when the transform returns a plain value; flatMap when it returns another Provider/Property (e.g. an output file property), to avoid nested providers.

Wiring with providers is handing someone a tracking number, not the package: they fetch it only when they actually need it.

saying these in an interview costs you the question

  • Calling .get() during configuration to wire tasks.
  • Manually adding dependsOn when provider-based data wiring already implies the dependency.

context