How do you set a convention default that depends on another property or on the build layout, and what pitfalls come from eager vs lazy convention values?
answer
- convention(value) eager vs convention(provider) lazy
- derive via map/flatMap/zip
- never .get() while wiring
- eager capture breaks config cache
- layout.buildDirectory.dir(...) is a provider
basics
~20 sUse the provider overload: convention(provider { ... }) or convention(otherProperty.map { ... }). It stays lazy, so the default is computed at read time and can depend on values configured later. Passing an already-computed value captures it eagerly.
solid answer
~40 s`convention` has two overloads: `convention(T value)` (eager — the value is fixed when you call it) and `convention(Provider<T>)` (lazy — recomputed on read). To make a default depend on something else, supply a provider: `outputDir.convention(layout.buildDirectory.dir("reports"))`, or derive from another extension property with `map`/`flatMap`: `archiveName.convention(version.map { "app-$it.jar" })`. The pitfall: if you compute a default eagerly — e.g. `convention(layout.buildDirectory.get().dir("reports"))` or reference `someProp.get()` while wiring — you snapshot whatever those inputs are *at configuration time*, defeating laziness and breaking the configuration cache, and you may read a value before the build author has configured it. The rule of thumb: never call `.get()` while building a convention; chain providers with `map`/`flatMap`/`zip` so resolution happens at read time. Combine with `finalizeValueOnRead()` if you also need a stable, observed value.
code
kotlin · 7 lines// lazy default derived from other properties
ext.archiveName.convention(
ext.baseName.zip(ext.version) { n, v -> "$n-$v.jar" }
)
// lazy default from build layout
ext.outputDir.convention(layout.buildDirectory.dir("dist"))
// AVOID: ext.outputDir.convention(layout.buildDirectory.get().dir("dist"))go deeper
Know that convention can take a provider for a lazy default; deep config-cache reasoning not required.
Show deriving a default from another property with map and name the eager-capture pitfall.
Explain the configuration-cache and reactivity consequences of eager .get() and the 'never get() at config time' rule, with map/flatMap/zip composition.
Establish team conventions/lint guidance so plugin authors consistently use lazy providers and keep the build configuration-cache compatible at scale.
## Eager vs lazy convention overloads `Property.convention(...)` accepts either a plain value or a `Provider`: - `convention("html")` — fine for constants. - `convention(provider)` — the default is a *recipe* evaluated lazily on each read (until finalized). For anything derived from other configurable state, you want the provider form so the default reflects values that may be configured *after* the plugin applies. ## Deriving a default from another property Providers compose with `map`, `flatMap`, and `zip`: ```kotlin abstract class PackagingExtension { abstract val baseName: Property<String> abstract val version: Property<String> abstract val archiveName: Property<String> abstract val outputDir: DirectoryProperty } val ext = extensions.create<PackagingExtension>("packaging") ext.baseName.convention("app") ext.version.convention(provider { project.version.toString() }) // derive a default from two other properties, lazily ext.archiveName.convention( ext.baseName.zip(ext.version) { name, v -> "$name-$v.jar" } ) ext.outputDir.convention(layout.buildDirectory.dir("dist")) ``` Because `archiveName`'s convention is a `zip` of the other two properties, overriding `version` later automatically changes the defaulted `archiveName` — exactly the wiring you want. ## The eager-capture pitfall Contrast these: ```kotlin // BAD: snapshots build dir and version NOW, at apply time ext.outputDir.convention(layout.buildDirectory.get().dir("dist")) ext.archiveName.convention("app-" + ext.version.get() + ".jar") ``` Problems: 1. **Premature read** — `ext.version.get()` may run before the author sets it, capturing the wrong default (or throwing). 2. **Configuration-cache hostility** — eagerly resolving `layout.buildDirectory` or other state at configuration time can break or bloat the cache; lazy providers are the supported pattern. 3. **Lost reactivity** — once you snapshot a value, later changes to its inputs no longer flow through. ## Interaction with finalization and disallowChanges A lazy convention can still be locked: `finalizeValueOnRead()` snapshots the resolved default on first read; `disallowChanges()` prevents an override while keeping the lazy default. These are orthogonal to whether the convention is eager or lazy. ## Rule of thumb 'Never `.get()` at configuration time.' Build defaults from providers (`layout.*`, `objects`, other properties via `map`/`flatMap`/`zip`), and let Gradle resolve them at execution time.
- Why is convention(otherProp.get()) usually a bug?It reads otherProp at configuration time, possibly before it's been set, and snapshots the value — defeating laziness, breaking reactivity, and hurting the configuration cache. Use convention(otherProp.map { ... }) instead.
- How would you make one extension property's default track another property?Use a provider transformation: targetProp.convention(sourceProp.map { transform(it) }) (or zip/flatMap for multiple sources), so the default recomputes whenever the source resolves.
- Does using a lazy convention conflict with finalizeValueOnRead()?No. The convention stays lazy until the first read, at which point finalizeValueOnRead() snapshots and freezes the resolved default. They compose cleanly.
An eager convention is a printed value; a lazy convention is a formula in a spreadsheet cell — change an input and the formula updates, but a printed number never does.
saying these in an interview costs you the question
- Calling .get() on dependencies while building a convention default.
- Claiming eager and provider-based conventions behave the same for config cache.
- Hardcoding paths instead of deriving from layout.buildDirectory provider.