What are ListProperty, SetProperty and MapProperty in Gradle, and how do you create them on a task or extension?
answer
- ListProperty / SetProperty / MapProperty
- objects.listProperty(...) or abstract getter
- implement Provider of the collection
- lazy, deferred, accumulating
- default = empty, not undefined
basics
~10 sThey 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 sListProperty<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 linesabstract 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
Name the three types, that they are lazy and collection-valued, and that you get them from objects/ObjectFactory or an abstract getter.
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).
Discuss managed vs ObjectFactory construction, @Input snapshotting of collections, and finalization semantics.
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).