skip to content

How do add, addAll, put and putAll accumulate values lazily on collection properties, and why does that matter?

level: middleimportance: must knowfreq 55%

answer

  1. add/addAll vs put/putAll
  2. accepts plain value OR Provider
  3. recorded recipe, resolved at get()
  4. any missing provider => whole property missing
  5. append-only; finalize blocks further mutation

basics

~10 s

add/addAll (list/set) and put/putAll (map) append to the property without resolving it. They accept plain values or Providers, and the actual elements are only computed when the property's value is queried.

solid answer

~40 s

Collection properties accumulate incrementally. `add(value)` / `addAll(a, b, c)` / `addAll(provider)` append to a ListProperty/SetProperty; `put(k, v)` / `putAll(map)` / `putAll(provider)` add entries to a MapProperty. Crucially each call records a deferred *recipe*, not a resolved value — you can pass a `Provider<T>` (or `Provider<Iterable<T>>` for addAll) whose value doesn't exist yet, and Gradle resolves the whole accumulation chain only when someone calls `get()`. This lets multiple plugins or configuration blocks each contribute elements without ordering constraints, and keeps task-input wiring honest: if a contributed provider points at another task's output, Gradle infers the task dependency automatically. If any contributed provider has no value at resolution time, the entire property has no value (use `addAll(provider.orElse(emptyList()))` to tolerate that).

code

kotlin · 12 lines
kotlin
abstract class BundleTask : DefaultTask() {
    @get:Input abstract val modules: ListProperty<String>
    @get:Input abstract val coords: MapProperty<String, String>
}

tasks.register<BundleTask>("bundle") {
    modules.add("auth")
    modules.addAll("billing", "notifications")
    modules.addAll(providers.gradleProperty("extraModules").map { it.split(",") })
    coords.put("group", "com.katajob")
    coords.put("version", providers.gradleProperty("ver").orElse("0.0.0"))
}

go deeper

for a junior

Know add/addAll for list & set, put/putAll for map, and that they append.

for a middle

Explain laziness (recorded recipe vs resolved value), accepting Providers, and the automatic task-dependency benefit.

for a senior

Cover missing-value propagation, orElse guards, finalization blocking mutation, and append-only semantics.

for a principal

Discuss how lazy accumulation lets independent plugins contribute without ordering coupling and keeps the model configuration-cache safe.

## Accumulation API | Property | Add one | Add many | Add from a Provider | |---|---|---|---| | `ListProperty`/`SetProperty` | `add(T)` | `addAll(T...)` / `addAll(Iterable)` | `add(Provider<T>)`, `addAll(Provider<out Iterable<T>>)` | | `MapProperty` | `put(K, V)` | `putAll(Map)` | `put(K, Provider<V>)`, `putAll(Provider<out Map<K,V>>)` | ## Lazy = a recorded recipe, not a value Each `add`/`put` call stores an instruction. Nothing is resolved until the property is queried (`get()`, `getOrElse(...)`, or when Gradle snapshots the task input before execution). This is what makes the API *lazy*: you can wire `add(otherTask.flatMap { it.outputFile.map(...) })` at configuration time even though that value only exists after `otherTask` runs. ```kotlin val tags = objects.listProperty(String::class.java) tags.add("core") // plain value tags.add(versionProvider.map { "v$it" }) // a Provider — resolved later tags.addAll(extraTagsProvider) // Provider<List<String>> // tags.get() is only computed when queried ``` ## Two consequences that show up in interviews ### 1. Automatic task dependencies If a contributed `Provider` carries task-dependency information (e.g. it came from another task's output property), then a task consuming this collection property as an `@Input`/`@InputFiles` automatically depends on the producing task. You don't write `dependsOn`. ### 2. Missing-value propagation If *any* contributed provider has **no value** when the property is resolved, the whole property has no value and `get()` throws. Guard with `orElse`: ```kotlin tags.addAll(maybeMissing.orElse(emptyList())) ``` ## Append-only by default `add`/`addAll`/`put` only *append*; they never remove. To start from a clean slate you call `empty()` (or reset via `set(...)`/`value(...)`), which is why convention defaults and `empty()` interact with accumulation (covered separately). ## Mutation-after-read pitfall Once a property is *finalized* (e.g. after the task graph is calculated, or after `finalizeValue()`), further `add`/`put` calls throw `IllegalStateException`. Accumulate during configuration, not execution.

  • You call addAll(someProvider) and someProvider has no value at execution. What happens?
    The whole ListProperty has no value, so get()/snapshotting throws. Use addAll(someProvider.orElse(emptyList())) to make the contribution optional.
  • Does add(otherTask.output...) create a task dependency?
    Yes — if the provider carries the producing task's dependency information, a task consuming the collection property as an input automatically depends on the producer, with no explicit dependsOn.
  • Can you call add() inside the task action (doLast)?
    No — by execution time the property is finalized; mutating it throws IllegalStateException. Accumulate during configuration.

saying these in an interview costs you the question

  • Saying add/addAll resolve the value immediately.
  • Claiming a missing contributed provider is silently skipped (it poisons the whole property unless guarded with orElse).
  • Thinking add/put can also remove elements — they are append-only.

context