skip to content

What does Property.convention(...) do on a Gradle lazy property, and how does it differ from calling set(...)?

level: juniorimportance: must knowfreq 62%

answer

  1. convention = default, set = override
  2. explicit always wins
  3. get() throws if neither set
  4. convention can be a Provider (lazy)
  5. no-op if set already happened

basics

~10 s

convention(...) supplies a default value used only if nobody calls set(...). set(...) is an explicit assignment that overrides any convention. So convention = fallback default, set = the actual chosen value.

solid answer

~40 s

On a `Property<T>` (or `Provider`-backed type), `convention(value)` registers a *default* that is returned by `get()` only when no explicit value has been assigned. `set(value)` (or Kotlin `=`) assigns the real value and always wins over the convention. The key rule: **convention does not override an explicit set, and a later convention call does not override an earlier set**. This lets a plugin author declare sensible defaults in the plugin while still letting the build script override them. Conventions can themselves be lazy — `convention(provider { ... })` defers computation. Internally the property tracks whether it is 'explicitly set'; if not, reads fall through to the convention. This is the modern replacement for eagerly initialising fields, and it keeps configuration lazy so values resolve at execution time.

code

kotlin · 8 lines
kotlin
abstract class GreetingExtension {
    abstract val message: Property<String>
}

val ext = extensions.create<GreetingExtension>("greeting")
ext.message.convention("Hello")   // default
// build author may do: greeting { message = "Hi" }
println(ext.message.get())        // "Hi" if set, else "Hello"

go deeper

for a junior

State plainly: convention is the default, set is the override, and set wins. Mention get() can throw when nothing is provided.

for a middle

Add that conventions can be lazy via convention(provider{...}) and explain the resolution order (explicit → convention → undefined).

for a senior

Discuss why plugins prefer convention over set for composability with disallowChanges/finalizeValueOnRead and detecting whether the author configured a value.

for a principal

Frame it as part of the lazy-configuration contract that enables configuration cache and clean plugin/consumer separation of defaults vs overrides.

## What a lazy property is Gradle's configuration model uses *lazy properties*: `Property<T>`, `ListProperty<T>`, `MapProperty<T>`, `RegularFileProperty`, `DirectoryProperty`, and the read-only `Provider<T>`. Instead of holding a value directly, they hold a *recipe* that is evaluated on `get()`. This defers work until it is actually needed and lets values be wired together before any of them are known. ## `convention` vs `set` A property has two distinct slots: - the **convention** — a default supplied via `convention(value)` or `convention(provider)`; - the **explicit value** — assigned via `set(value)` (Groovy/Java) or `=` (Kotlin assignment), or `value(...)`. The resolution rule on `get()`: 1. If an explicit value was assigned, return it. 2. Otherwise, if a convention was registered, return the convention's value. 3. Otherwise the property is *undefined* — `get()` throws, while `getOrNull()` returns null and `getOrElse(d)` returns `d`. Two subtle but exam-critical rules: - **A convention never overrides an explicit value.** Once something has called `set(...)`, calling `convention(...)` afterwards has no visible effect. - **Calling `convention(...)` after `set(...)` is a no-op for reads**, and calling `set(...)` after `convention(...)` overrides the convention. Order of the two *kinds* of call doesn't change the precedence — explicit always wins. ## Why use `convention` instead of just `set` in a plugin If a plugin did `ext.outputDir.set(layout.buildDirectory.dir("reports"))`, that would be an *explicit* value, and a build author's own `set(...)` would still win — but so would order-of-evaluation surprises, and you lose the ability to detect 'was this ever configured'. Using `convention(...)` declares intent: 'this is my default unless overridden'. It keeps the property *unset* from the build author's perspective, which composes correctly with `disallowChanges()`, `finalizeValueOnRead()`, and downstream wiring. ## Laziness of the convention itself `convention(provider { expensiveDefault() })` means the default is only computed if the convention is actually read (i.e., nobody set the property). Combined with `layout.buildDirectory`, this is the idiomatic way to default file/dir properties. ```kotlin abstract class GreetingExtension { abstract val message: Property<String> abstract val outputDir: DirectoryProperty } project.extensions.create<GreetingExtension>("greeting").apply { message.convention("Hello from Gradle") outputDir.convention(layout.buildDirectory.dir("greetings")) } ``` Here the build can override `greeting { message = "Hi" }`, and the convention silently steps aside.

  • What happens if you call convention("a") after the build already called set("b")?
    Nothing visible — get() still returns "b". An explicit value always wins over a convention regardless of call order.
  • What does get() do if neither set nor convention was called?
    It throws an exception (the property has no value). Use getOrNull() or getOrElse(default) to read safely.

convention is like a factory default setting; set(...) is the user changing it in the menu. Once the user changes it, restoring the factory default text on the box doesn't un-change their choice.

saying these in an interview costs you the question

  • Saying convention(...) overrides set(...) — it's the opposite.
  • Claiming a later convention() replaces an earlier set() — explicit always wins.
  • Confusing convention with a guaranteed value: get() can still throw if nothing was provided and there's no convention.

context