skip to content

How do you reference and wire a task declared with tasks.register without realizing it? Show how to use the returned TaskProvider.

level: middleimportance: should knowfreq 45%

answer

  1. TaskProvider = typed lazy promise
  2. configure {} adds config lazily
  3. dependsOn(provider) defers resolution
  4. map / flatMap to read outputs
  5. .get() only at execution time

basics

~10 s

register returns a TaskProvider. Pass the provider itself to dependsOn or inputs — Gradle resolves it lazily. Use provider.configure { } to add config, and flatMap to read its outputs without calling get().

solid answer

~40 s

The `TaskProvider<T>` from `register` is a lazy handle you wire *as a provider*, never by resolving it early. Gradle's APIs accept providers directly: `dependsOn(myProvider)`, `from(zipProvider)`, `inputs.files(compileProvider)` — all defer resolution until the task graph is built. To add more configuration after registration, use `myProvider.configure { ... }` (still lazy) rather than `myProvider.get().something`. To consume a task's output, map through the provider: `zipTask.flatMap { it.archiveFile }` yields a `Provider<RegularFile>` that only realizes `zipTask` if its output is actually needed. The cardinal rule: keep everything in `Provider`/`TaskProvider` space; the moment you call `.get()` (or read a concrete property) you realize the task and break the laziness chain. In Kotlin DSL, `val z by registering(Zip::class) { }` gives you the same provider via delegation.

code

kotlin · 9 lines
kotlin
val compile = tasks.register("compileThing", JavaCompile::class)

// add config later, still lazy
compile.configure { options.encoding = "UTF-8" }

val jar = tasks.register("jarThing", Jar::class) {
    dependsOn(compile)                       // provider accepted directly
    from(compile.map { it.destinationDirectory }) // lazy output wiring
}

go deeper

for a junior

Know that register returns a TaskProvider and you can pass it to dependsOn instead of a task name.

for a middle

Use configure{}, dependsOn(provider), and map/flatMap to wire tasks without calling get() during configuration.

for a senior

Design plugin APIs that expose Provider-typed outputs so consumers wire lazily; explain map vs flatMap and execution-time get().

for a principal

Set conventions for provider-based wiring across the org's shared plugins to guarantee configuration avoidance and configuration-cache compatibility.

## TaskProvider is a lazy reference `val compile = tasks.register("compile", JavaCompile::class)` returns a `TaskProvider<JavaCompile>`. Think of it as a typed promise. You can do three things lazily with it: 1. **Configure later:** `compile.configure { options.encoding = "UTF-8" }` — the action runs when (if) `compile` is realized. 2. **Depend on it:** `tasks.register("bundle") { dependsOn(compile) }` — Gradle accepts the provider and resolves the dependency when building the graph. 3. **Read its outputs lazily:** map through it. ## Lazy output wiring with map / flatMap The key trick for cross-task wiring is **provider chaining**: ```kotlin val zipIt = tasks.register("zipIt", Zip::class) { from("build/libs") archiveFileName.set("app.zip") } // archiveFile is itself a Provider<RegularFile>; flatMap composes them val zipFile: Provider<RegularFile> = zipIt.flatMap { it.archiveFile } tasks.register("publishZip", Copy::class) { from(zipFile) // lazy: realizes zipIt only if publishZip runs into("dist") } ``` - Use **`map`** when the lambda returns a plain value. - Use **`flatMap`** when the lambda returns another `Provider`/`Property` (e.g. `archiveFile`). Because you never call `.get()`, `zipIt` stays unrealized until `publishZip` is on the execution path. ## When you must resolve Calling `.get()` is legitimate at *execution time* (inside a `doLast`/task action) where everything is already realized. The anti-pattern is calling `.get()` during *configuration*. ## Kotlin DSL delegation ```kotlin val zipIt by registering(Zip::class) { archiveFileName.set("app.zip") } ``` `by registering` is sugar that both registers the task and binds the local `val` to the `TaskProvider`. ## Summary Wire providers to providers. Configure via `.configure {}`, depend via `dependsOn(provider)`, read outputs via `map`/`flatMap`. Reserve `.get()` for execution time. This preserves the configuration-avoidance benefit that `register` exists to provide.

  • When is calling provider.get() acceptable?
    At execution time — inside a task action (doLast/doFirst) or after the graph is built — where the task is already realized. Avoid it during the configuration phase.
  • What is the difference between map and flatMap on a provider?
    map transforms the value with a lambda returning a plain value; flatMap is for when the lambda returns another Provider/Property (it flattens the nested provider), e.g. mapping to archiveFile.

saying these in an interview costs you the question

  • Calling provider.get() during configuration just to read a property.
  • Storing the resolved Task instead of the TaskProvider and re-using it.
  • Using map where flatMap is needed (yielding a Provider<Provider<...>>).

context