What do finalizeValue(), finalizeValueOnRead(), and disallowChanges() do on a property, and when would you use each when designing an extension?
answer
- disallowChanges = no more set
- finalizeValue = resolve+freeze now
- finalizeValueOnRead = freeze on first get
- stops read-then-mutate bugs
- tasks auto-finalize inputs
basics
~10 sThey lock a property. disallowChanges() forbids further set() calls. finalizeValue() resolves the current value now and freezes it. finalizeValueOnRead() defers that freeze until the first get(). All prevent later mutation of configured values.
solid answer
~50 sThese three methods make a `Property`/`Provider` immutable in slightly different ways. **`disallowChanges()`** keeps the property's current source (value or convention) but rejects any later `set()`/`convention()` — reads still resolve lazily. **`finalizeValue()`** eagerly *resolves* the provider chain to a concrete value right now and freezes it; later mutations throw and the upstream providers it depended on are no longer consulted. **`finalizeValueOnRead()`** is the lazy variant: it doesn't resolve immediately, but the *first* `get()` snapshots the value and freezes it, so the value can't change between two reads. A plugin typically calls `finalizeValueOnRead()` on properties it exposes to tasks after the configuration phase, so build authors get a clear error if they try to mutate a value the task already observed — turning subtle ordering bugs into loud failures. `disallowChanges()` is common right after wiring a derived value you don't want overridden.
code
kotlin · 6 linesval format = objects.property<String>()
format.convention("html")
format.finalizeValueOnRead()
println(format.get()) // resolves & freezes here
format.set("pdf") // throws: value already finalizedgo deeper
Know that these lock a property against further changes; naming disallowChanges() as 'no more set' is enough.
Distinguish all three precisely: block-mutation vs resolve-now vs freeze-on-first-read, and give a use case for each.
Explain why finalizeValueOnRead preserves laziness/config-cache friendliness and how Gradle auto-finalizes task inputs to surface ordering bugs.
Discuss using finalization as an API-design contract to make plugin guarantees explicit and to fail fast on misuse across many consumers.
## The problem these solve Lazy properties are mutable during configuration. That flexibility creates a hazard: a value might be read by one task, then mutated, so a *second* reader sees a different value — an order-dependent bug. The finalization API closes that window. ## The three methods ### `disallowChanges()` Leaves the current value/convention in place but forbids future `set(...)` and `convention(...)`. Reads remain lazy (providers still evaluated on `get()`). Use it after you've wired a property and want to guarantee nobody overrides it. ### `finalizeValue()` *Eagerly* walks the provider chain, computes the final value **now**, caches it, and detaches from upstream providers. Any later mutation throws. Because it resolves immediately, calling it too early (before sources are configured) can capture a wrong/incomplete value — so it's normally done late, e.g. at the end of configuration. ### `finalizeValueOnRead()` The deferred form: nothing is resolved yet, but the **first `get()`** finalizes the value and freezes it. This preserves laziness (good for configuration cache and for values depending on things configured later) while still guaranteeing a stable value once observed. This is the most commonly recommended one for plugin-exposed properties. ## How Gradle uses them internally Gradle automatically finalizes task input properties as the task graph is prepared, which is why mutating a task's `Property` input from a `doFirst` after another input read it can throw. Understanding finalization explains those 'cannot change property after it has been finalized' errors. ## Choosing in an extension design - Want a derived/computed value that consumers must not override → `disallowChanges()` after wiring. - Want author convenience during config but a stable value once tasks run → `finalizeValueOnRead()`. - Need the concrete value *right now* for branching logic → `finalizeValue()` (rare; usually a smell). ```kotlin abstract class ReportExtension { abstract val format: Property<String> } val ext = extensions.create<ReportExtension>("report") ext.format.convention("html") afterEvaluate { // lock so tasks see a stable value and overrides after this fail loudly ext.format.finalizeValueOnRead() } ``` ## Relationship to convention Finalization interacts with conventions: if only a convention exists, finalization snapshots the convention's resolved value. After finalization, neither `set` nor `convention` can change it.
- Why prefer finalizeValueOnRead() over finalizeValue() in a plugin?finalizeValueOnRead() keeps the value lazy until first read, so it still picks up things configured later and plays well with the configuration cache; finalizeValue() resolves immediately and can capture an incomplete value if called too early.
- Does disallowChanges() resolve the value immediately?No. It only forbids future mutation; reads still resolve the provider lazily on get(). Only finalizeValue()/finalizeValueOnRead() snapshot a concrete value.
saying these in an interview costs you the question
- Saying disallowChanges() computes/caches the value — it only blocks mutation.
- Claiming finalizeValue() and finalizeValueOnRead() are identical — one is eager, one freezes on first read.
- Recommending finalizeValue() early in configuration without warning it can capture an incomplete value.