Using providers.gradleProperty, how do you supply a default and handle a missing property without breaking the configuration cache?
answer
- absent != empty string
- .orElse for lazy default
- .getOrNull / .getOrElse for eager
- never blind .get() on optional prop
- wire so cache tracks the input
basics
~10 sUse .orElse("default") or .getOrElse("default") on the provider, or .getOrNull() to detect absence. Avoid calling .get() at configuration time when the property might be missing.
solid answer
~40 s`providers.gradleProperty("x")` returns an *absent* provider when `x` is undefined. To stay lazy and config-cache-safe you keep working with the provider: - `.orElse("default")` returns a new lazy provider that yields the default when absent — best for wiring into task inputs. - `.map { it.toBoolean() }` transforms lazily; combine with `.orElse(false)`. - `.isPresent` reports presence without forcing a value (when wired). If you must read eagerly (rare), `getOrElse("default")` and `getOrNull()` resolve immediately. The key discipline is **not** calling `.get()` on a possibly-absent provider at configuration time — that throws *and* re-introduces an eager read. Prefer pushing the default into the provider chain and letting Gradle resolve it at execution, so the value is tracked as a cache input and missing-property handling is uniform.
code
kotlin · 8 lines// Lazy default, config-cache-safe
val mode = providers.gradleProperty("mode").orElse("release")
// Lazy transform + default
val verbose = providers.gradleProperty("verbose").map { it.toBoolean() }.orElse(false)
// Guarded eager (only when you must decide at config time)
if (verbose.get()) logger.lifecycle("verbose on")go deeper
Know .orElse("default") supplies a fallback and .getOrNull() detects a missing property.
Explain the lazy vs eager handlers and why blind .get() on optional props is wrong.
Discuss how wiring with .orElse keeps the value as a tracked cache input and chain across sources.
Set conventions for optional-property handling and defaults so behavior is consistent and cache-correct across modules.
## Absence is a first-class state A `Provider<String>` from `providers.gradleProperty("x")` is either *present* (the property exists) or *absent* (it doesn't). This is different from an empty string — absence means "no value at all." How you handle absence determines both correctness and config-cache safety. ## Lazy handlers (preferred) Keep operating on the provider; resolution happens later: ```kotlin // supply a default lazily val mode = providers.gradleProperty("mode").orElse("release") // transform lazily, then default val verbose = providers.gradleProperty("verbose") .map { it.toBoolean() } .orElse(false) tasks.register("info") { val m = mode // capture provider, not value val v = verbose doLast { println("mode=${m.get()} verbose=${v.get()}") } // .get() at execution time is fine } ``` `orElse` accepts a literal *or another provider*, so you can chain across sources. `.map`/`.flatMap` transform without forcing the value. None of these capture a `Project`, so the configuration cache is happy. ## Eager handlers (use sparingly) Sometimes you genuinely need a value during configuration (e.g., to decide whether to register a task). Then: - `getOrElse("default")` — resolves now, returns default if absent. - `getOrNull()` — resolves now, returns null if absent. - `get()` — resolves now, **throws** `MissingValueException` if absent. These are still config-cache-safe in the sense that they don't touch `Project`; the risk is only that you've moved work to configuration time and lose laziness. A guarded pattern: ```kotlin if (providers.gradleProperty("enableX").map { it.toBoolean() }.getOrElse(false)) { tasks.register("x") { /* ... */ } } ``` ## The anti-pattern Calling `.get()` on a maybe-absent provider at configuration time: ```kotlin val x = providers.gradleProperty("x").get() // throws when absent; eager ``` This both crashes for users who didn't set `x` and forces an eager read. Replace with `.orElse(...)` (wire lazily) or `.getOrElse(...)` (guarded eager). ## Why config-cache cares When the provider (with its `orElse` default) is wired into a task input, Gradle records the property as an input. If the user later defines `x`, the recorded input changes and the cache invalidates correctly. An eager `.get()` value stored in a local variable would *not* be re-evaluated on a cache hit unless wired, leading to stale results — another reason to prefer lazy handling.
- What does .get() do when the property is absent?It throws a MissingValueException. Use .getOrNull(), .getOrElse(default), or wire .orElse(default) to avoid forcing an absent value.
- Why prefer .orElse over getOrElse when wiring into a task input?.orElse keeps the whole chain lazy and lets Gradle track the property as a cache input that re-evaluates on changes; getOrElse resolves eagerly at configuration time and is not re-evaluated.
saying these in an interview costs you the question
- Calling .get() unconditionally on an optional property and assuming a default — it throws.
- Confusing absence with an empty string value.