How does the `by extra` delegate work in the Kotlin DSL for both declaring and reading extra properties, and what are its pitfalls?
answer
- by extra(value) = declare; by extra = read
- lookup key = property name
- read variant casts Any? -> declared type
- var by extra is writable -> updates map
- has() before get() to avoid exception
basics
~20 sval x by extra("v") declares an extra property named x with value v. val x: String by extra (no value) reads an existing one. The delegate stores under the property name and casts on read.
solid answer
~50 sIn Kotlin, `extra` is a property delegate provider on `ExtensionAware`. `val springVersion by extra("6.1")` calls the variant that takes an initial value: it registers `springVersion` in the project's `ExtraPropertiesExtension` and binds the local `val` to it. `val springVersion: String by extra` (no argument) is the read variant: it does NOT create anything — it looks up the property by the delegated name and casts the stored `Any?` to the declared type. Because the property name drives the lookup, the local variable name must exactly match the stored key. Pitfalls: (1) reading a missing key throws `UnknownPropertyException` at execution time; (2) the cast can throw `ClassCastException` if the stored type differs; (3) `var x by extra(...)` is writable and reassignment updates the underlying map; (4) capturing the value eagerly at configuration time can break the configuration cache — prefer providers when feeding tasks.
code
kotlin · 16 lines// root build.gradle.kts
val springVersion by extra("6.1.0")
val ci by extra(System.getenv("CI") != null)
// subproject build.gradle.kts — read what root set
val springVersion: String by extra // name must match
val ci: Boolean by extra
dependencies {
implementation("org.springframework:spring-core:$springVersion")
}
// safe map-style access when the name can't match
if (project.extra.has("springVersion")) {
val v = project.extra["springVersion"] as String
}go deeper
Recognise the two forms — by extra(value) to declare, by extra to read — even if delegate internals are fuzzy.
Explain that the property name is the lookup key, that read casts Any?, and the common runtime failures (unknown property, ClassCastException).
Discuss safe access (has/map form), configuration-cache implications of eager reads, and when to prefer providers.
Frame ext usage policy across a multi-module build and steer teams toward typed alternatives (catalogs, convention plugins) over delegate-heavy ext sharing.
## The delegate, precisely Kotlin property delegation lets `val foo by something` route reads (and writes for `var`) through `something`'s `getValue`/`setValue` operators. Gradle's Kotlin DSL adds an `extra` member on `ExtensionAware` that returns such a delegate provider, with two flavours. ### Declaring with an initial value ```kotlin val springVersion by extra("6.1.0") var buildNumber by extra(0) ``` This registers the key (using the **property name**, here `springVersion`/`buildNumber`) into `project.extra` and seeds it with the supplied value. The local `val`/`var` is now a view onto that map entry. For a `var`, `buildNumber = 5` writes straight back into the extension. ### Reading an existing property ```kotlin val springVersion: String by extra ``` No argument is passed, so nothing is created. On first access the delegate calls `project.extra.get("springVersion")` and casts the result to `String`. This is the idiom for reading, in a subproject's `build.gradle.kts`, a value that the root project set. ## The name is load-bearing Because the lookup key is the property's own name, this fails: ```kotlin val version: String by extra // looks up "version", NOT "springVersion" ``` You would need `val springVersion: String by extra`, or the map form `project.extra["springVersion"] as String`. ## Pitfalls | Pitfall | Symptom | |---|---| | Key never set | `ExtraPropertiesExtension.UnknownPropertyException` at execution time | | Wrong declared type | `ClassCastException` on read | | Name mismatch | reads the wrong/absent key silently or throws | | Eager capture | configuration-cache problems / stale values | ## Map-style alternative When you can't make the local name match, drop the delegate: ```kotlin val v = project.extra["springVersion"] as String project.extra["flag"] = true if (project.extra.has("flag")) { /* ... */ } ``` `has(name)` is the safe existence check that avoids the unknown-property exception. ## Relationship to providers Extra properties resolve eagerly to plain values. If a task needs the value, wiring it through a `Provider` (e.g. `providers.provider { project.extra["x"] }`) keeps laziness and is friendlier to the configuration cache than reading the raw value during task configuration.
- Why must the local Kotlin variable name match the property key when using the read-only `by extra`?The delegate has no explicit key argument, so it uses the delegated property's own name as the lookup key in the ExtraPropertiesExtension. A different local name looks up a different (likely missing) key.
- How do you check whether an extra property exists without triggering an exception?Use project.extra.has("name") (or properties.containsKey) before reading; a plain get on a missing key throws UnknownPropertyException.
- Can you reassign an extra property through the delegate?Yes if declared as var by extra(...); reassigning the var writes back into the underlying ExtraPropertiesExtension map.
saying these in an interview costs you the question
- Saying `val x by extra` (no value) creates the property — it only reads.
- Thinking the lookup key is independent of the variable name.
- Claiming the cast in the read delegate is checked at compile time.