skip to content

What are ListProperty, SetProperty and MapProperty in Gradle, and how do you create them on a task or extension?

level: juniorimportance: must knowfreq 60%

answer

  1. ListProperty / SetProperty / MapProperty
  2. objects.listProperty(...) or abstract getter
  3. implement Provider of the collection
  4. lazy, deferred, accumulating
  5. default = empty, not undefined

basics

~10 s

They are lazy, collection-valued properties: ListProperty<T>, SetProperty<T>, MapProperty<K,V>. You create them via ObjectFactory (objects.listProperty(String::class.java)) and usually expose them as abstract getters on a task or extension.

solid answer

~30 s

ListProperty<T>, SetProperty<T> and MapProperty<K,V> are the collection-valued members of Gradle's lazy configuration API. Like Property<T> they hold a value computed lazily (at execution time, not configuration time), but the value is a List, Set or Map. You obtain them from ObjectFactory — objects.listProperty(String::class.java), objects.setProperty(...), objects.mapProperty(String::class.java, String::class.java) — or, more idiomatically, declare an abstract getter (val items: ListProperty<String>) and let Gradle's managed-property mechanism instantiate it for you. They give you deferred wiring (you can point them at a Provider before its value exists), incremental accumulation via add/addAll/put, and proper up-to-date checking when used as @Input on a task.

code

kotlin · 12 lines
kotlin
abstract class ReportTask : DefaultTask() {
    @get:Input abstract val authors: ListProperty<String>
    @get:Input abstract val labels: SetProperty<String>
    @get:Input abstract val props: MapProperty<String, String>
}

tasks.register<ReportTask>("report") {
    authors.add("alice")
    authors.addAll("bob", "carol")
    labels.add("release")
    props.put("version", "1.0")
}

go deeper

for a junior

Name the three types, that they are lazy and collection-valued, and that you get them from objects/ObjectFactory or an abstract getter.

for a middle

Explain the Provider relationship, the empty default, and why a plain List can't replace them (no deferred wiring, no accumulation, no proper input tracking).

for a senior

Discuss managed vs ObjectFactory construction, @Input snapshotting of collections, and finalization semantics.

for a principal

Frame them as part of the lazy-config contract that makes configuration-cache and parallel configuration safe across a plugin ecosystem.

## The problem they solve Gradle builds are configured eagerly but should compute values lazily — a task input might depend on another task's output that doesn't exist until execution. For single values, `Property<T>` defers the value behind a `Provider<T>`. For *collections*, Gradle provides three specialised properties: - **`ListProperty<T>`** — an ordered, duplicate-allowing `List<T>`. - **`SetProperty<T>`** — an unordered, de-duplicated `Set<T>`. - **`MapProperty<K, V>`** — a `Map<K, V>`. All three implement `Provider` of their collection type, so `get()`/`getOrElse(...)`/`map(...)` work, and the value is only materialised when queried. ## Creating them Two idiomatic ways: 1. **Via `ObjectFactory`** (the `objects` service) — explicit construction, useful inside an `init` block or a non-managed class. 2. **Managed properties** — declare an `abstract` getter returning the property type and Gradle generates the implementation. This is the preferred style for tasks and extensions. ```kotlin abstract class GenerateTask : DefaultTask() { @get:Input abstract val tags: ListProperty<String> @get:Input abstract val flags: SetProperty<String> @get:Input abstract val env: MapProperty<String, String> } ``` For a non-abstract class you inject `ObjectFactory`: ```kotlin val tags = objects.listProperty(String::class.java) val env = objects.mapProperty(String::class.java, String::class.java) ``` ## Why not a plain `List`? A plain `var tags: List<String>` is evaluated when assigned and cannot be wired to a `Provider`, cannot accumulate lazily, and does not participate in Gradle's input snapshotting the way a lazy property does. Collection properties give deferred resolution, lazy accumulation (`add`/`addAll`/`put`), and clean up-to-date checks. ## Default state A freshly created collection property is **not** undefined — it defaults to an *empty* collection (Gradle calls this the implicit empty value), so `get()` returns `[]`/`{}` rather than throwing. You can still override the starting point with `convention(...)` or reset with `empty()`.

  • What does ListProperty<T> return from get() if you never set a value?
    An empty list. Collection properties default to an empty collection, not to a missing/undefined value, so get() does not throw NoSuchElementException.
  • Why prefer an abstract getter over objects.listProperty(...)?
    Managed (abstract) properties let Gradle instantiate and finalize them, reduce boilerplate, and are the documented idiom for tasks/extensions. You only fall back to ObjectFactory when the class can't be managed.

saying these in an interview costs you the question

  • Saying they wrap an eager List that is evaluated at configuration time.
  • Claiming get() throws if unset — collection properties default to empty.
  • Confusing them with ConfigurableFileCollection (a different, file-specific type).

context