skip to content

Show how to wire providers.gradleProperty into a custom task input the lazy way, and explain what goes wrong if you call .get() in the configuration block.

level: seniorimportance: should knowfreq 35%

answer

  1. Property<T> input + provider wiring
  2. who.set(provider), not provider.get()
  3. .get() only in @TaskAction/doLast
  4. eager get = lost input tracking + crash on absent
  5. config-cache staleness

basics

~10 s

Declare a Property<String> task input and set it to the provider: input.set(providers.gradleProperty("x").orElse("d")). Calling .get() in the configuration block resolves eagerly, loses laziness, and can break config-cache or up-to-date checks.

solid answer

~40 s

Give the task a lazy input field — `abstract val flag: Property<String>` annotated `@Input` — and wire the provider without resolving it: ```kotlin tasks.register<MyTask>("run") { flag.set(providers.gradleProperty("flag").orElse("default")) } ``` Gradle now resolves `flag` at execution time and records the gradle property as a tracked input, so up-to-date checks, the build cache, and the configuration cache all behave correctly. If instead you write `flag.set(providers.gradleProperty("flag").get())`, you resolve the value *at configuration time*: the property read is no longer tracked as a live input, a missing property throws during configuration for everyone, and the configuration cache may store a stale snapshot. The rule: pass *providers* into `Property.set`, and only call `.get()` inside `@TaskAction`/`doLast` execution code where eager resolution is expected.

code

kotlin · 10 lines
kotlin
abstract class GreetTask : DefaultTask() {
    @get:Input abstract val who: Property<String>
    @TaskAction fun run() = println("hello ${who.get()}") // execution time
}

tasks.register<GreetTask>("greet") {
    // GOOD: wire the provider lazily
    who.set(providers.gradleProperty("who").orElse("world"))
    // BAD: who.set(providers.gradleProperty("who").get())
}

go deeper

for a junior

Recognize that task inputs are Property<T> and you set them from providers.

for a middle

Show the correct wiring and name that .get() belongs in @TaskAction, not configuration.

for a senior

Explain the cache/up-to-date/config-cache consequences of eager resolution and the absence-crash failure mode.

for a principal

Establish lint/review rules banning .get() in configuration blocks and codify lazy-wiring patterns across the plugin/build codebase.

## Lazy task inputs in modern Gradle A well-formed Gradle task declares its inputs as lazy containers: ```kotlin abstract class GreetTask : DefaultTask() { @get:Input abstract val who: Property<String> @TaskAction fun run() = println("hello ${who.get()}") // .get() here = execution time, OK } ``` `Property<T>` is a settable, lazy input. You feed it either a literal or, crucially, **a provider**: ```kotlin tasks.register<GreetTask>("greet") { who.set(providers.gradleProperty("who").orElse("world")) } ``` ## Why wiring the provider is correct When you call `who.set(provider)`, Gradle stores the *provider*, not a value. Several machineries depend on this: - **Up-to-date checks / build cache**: the `@Input` value is read at execution time and hashed. If the gradle property changes between builds, the input hash changes and the task re-runs. Wiring the provider makes this automatic. - **Configuration cache**: the serialized task carries "read gradle property who, else 'world'" as its input. On a later build Gradle can detect the property changed and invalidate. No `Project` is captured. - **Laziness / ordering**: the value is read when needed, so even values configured later in the build still take effect. ## What breaks with eager .get() at configuration time ```kotlin tasks.register<GreetTask>("greet") { who.set(providers.gradleProperty("who").get()) // BAD } ``` Problems: 1. **Crashes on absence**: if `who` isn't defined, `.get()` throws `MissingValueException` during configuration — and it throws for *every* invocation that configures this task, even tasks the user never runs (unless configuration avoidance saves you). 2. **Lost input tracking**: you stored a fixed `String`, not a provider. The link to the gradle property source is gone; the property is no longer the tracked thing. 3. **Configuration-cache staleness/eagerness**: the value is captured at store time and not re-derived from the property on cache reuse; combined with eager configuration this fights the cache's design. 4. **Configuration-time work**: you've done resolution during configuration, undermining configuration avoidance and config-cache performance. ## The discipline - Pass **providers** into `Property.set(...)`; build defaults with `.orElse(...)` and transforms with `.map(...)` while still in provider form. - Call `.get()`/`.getOrElse(...)` **only** inside `@TaskAction` or `doLast { }` execution code. - Treat any `.get()` in a `tasks.register { }` / `tasks.named { }` configuration block as a smell. This keeps the build lazy, cacheable, and config-cache-compatible — the whole reason the provider API exists for reading gradle/system/env properties.

  • Where is it acceptable to call .get() on the provider?
    Inside execution-time code — @TaskAction methods or doLast/doFirst closures — where the value is genuinely needed; not in the task configuration block.
  • How does wiring the provider help up-to-date checks?
    Gradle reads the @Input value at execution time and hashes it; if the underlying gradle property changes, the hash changes and the task re-runs automatically.
  • Does eager .get() always crash?
    Only when the property is absent (MissingValueException). Even when it succeeds it still loses input tracking and works against the configuration cache, so it's an anti-pattern regardless.

Setting the input to a provider is like giving a recipe step 'measure the flour at bake time'; calling .get() at config time is measuring the flour now and writing the number on the recipe — if the bag is empty you fail immediately, and if someone refills it later your number is wrong.

saying these in an interview costs you the question

  • Calling provider.get() inside tasks.register { } / tasks.named { } configuration blocks.
  • Storing a resolved String as a task input instead of wiring the provider, then wondering why the task isn't re-running on property change.
  • Claiming .get() at config time is fine because 'it works on my machine' where the property is always set.

context