skip to content

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?

level: seniorimportance: should knowfreq 30%

answer

  1. convention(value) eager vs convention(provider) lazy
  2. derive via map/flatMap/zip
  3. never .get() while wiring
  4. eager capture breaks config cache
  5. layout.buildDirectory.dir(...) is a provider

basics

~20 s

Use 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
kotlin
// 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

for a junior

Know that convention can take a provider for a lazy default; deep config-cache reasoning not required.

for a middle

Show deriving a default from another property with map and name the eager-capture pitfall.

for a senior

Explain the configuration-cache and reactivity consequences of eager .get() and the 'never get() at config time' rule, with map/flatMap/zip composition.

for a principal

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.

context