A Familiar But Slightly Awkward Pattern
If you’ve written Kotlin for a while, you’ve almost certainly written code like this:
fun getActiveUsers(users: List): List { val result = mutableListOf () for (user in users) { if (user.isActive) result.add(user) if (user.isPremium) result.add(user.copy(label = "Premium")) } return result.toList() // convert back to read-only }
The pattern works, but it’s ceremonial: create a mutable collection, build it up imperatively, then convert it back to a read-only version. The mutable list is just scaffolding — what you actually want is the final read-only result.
Kotlin 1.6 introduced a cleaner alternative: buildList, buildMap, and buildSet. These are standard library functions that let you construct read-only collections using an imperative builder block, without ever exposing a mutable reference outside the builder.
buildList: Construct a List Without the Boilerplate
buildList takes a lambda where this is a MutableList. When the lambda returns, the list is sealed into a read-only List and returned. The mutable state never leaks:
val numbers = buildList {
add(1)
add(2)
addAll(listOf(3, 4, 5))
if (System.currentTimeMillis() % 2 == 0L) add(6) // conditional logic is fine
}
// numbers is List, fully read-only
println(numbers) // [1, 2, 3, 4, 5] or [1, 2, 3, 4, 5, 6]
You can use any MutableList operation inside the block: add, addAll, remove, set, sortWith, and so on. The key insight is that the complexity of building the list stays inside the builder — callers always receive a clean, immutable result.
Rewriting the earlier example:
fun getActiveUsers(users: List): List = buildList { for (user in users) { if (user.isActive) add(user) if (user.isPremium) add(user.copy(label = "Premium")) } }
Cleaner, no intermediate variable, and the intent is immediately obvious: we’re building a list.
buildMap: Construct a Map the Same Way
buildMap works identically, but this inside the lambda is a MutableMap. This is especially useful when your map construction involves non-trivial logic:
data class Config(val key: String, val value: String, val isSecret: Boolean) fun buildPublicConfig(configs: List): Map = buildMap { for (config in configs) { if (!config.isSecret) { put(config.key, config.value) } } // Always include the app version put("app_version", BuildConfig.VERSION_NAME) }
You can also use putAll, getOrPut, remove, and any other MutableMap API. A particularly handy pattern is using getOrPut to build a grouping map:
fun groupByFirstLetter(words: List): Map > = buildMap { for (word in words) { val key = word.first().lowercaseChar() getOrPut(key) { mutableListOf() }.also { (it as MutableList).add(word) } } }
buildSet: When You Need Deduplication
buildSet gives you a MutableSet inside the builder and returns a read-only Set. It shines when you want to merge collections while automatically removing duplicates:
fun mergePermissions(
userPermissions: List,
rolePermissions: List,
extraPermissions: List
): Set = buildSet {
addAll(userPermissions)
addAll(rolePermissions)
addAll(extraPermissions)
// You can also remove permissions conditionally
remove("ADMIN_DELETE") // always strip this one for safety
}
Specifying an Initial Capacity
All three builders accept an optional capacity parameter. If you have a rough idea of how many elements you’ll add, providing it avoids internal array resizing:
val items = buildList(capacity = 256) {
repeat(200) { add("item_$it") }
}
This is purely a performance hint — the collection will still grow if you exceed the initial capacity.
How Do These Compare to Other Approaches?
You might wonder how buildList compares to listOf + spread operator, sequence().toList(), or chained map/filter calls.
listOf is perfect for static, known-at-compile-time lists. As soon as you have conditional logic or loops, you’re back to a mutable intermediate.
Functional chains (filter, map, flatMap) are great for transformations over an existing source. But they don’t compose well when you need to build a collection from multiple sources or with logic that doesn’t fit neatly into a transform pipeline.
buildList fills the gap: imperative construction logic, zero mutable leakage, and a single-expression style that works well with Kotlin’s expression-body functions.
Practical Android Example: Building a RecyclerView Item List
Here’s a realistic scenario — building a mixed list of items for a RecyclerView adapter that includes headers, content rows, and a footer:
sealed class ListItem {
data class Header(val title: String) : ListItem()
data class Row(val data: UserData) : ListItem()
object Footer : ListItem()
}
fun buildAdapterItems(
sections: List,
showFooter: Boolean
): List = buildList {
for (section in sections) {
add(ListItem.Header(section.title))
for (user in section.users) {
add(ListItem.Row(user.toData()))
}
}
if (showFooter) add(ListItem.Footer)
}
Compare this to the same code written with mutableListOf + toList() — the builder version has less noise and makes the intent clearer.
Summary
buildList, buildMap, and buildSet are small but high-quality additions to the Kotlin standard library. They close the gap between the convenience of mutable building and the safety of read-only collections — without the toList() ceremony at the end. Next time you find yourself writing val result = mutableListOf<T>() … return result.toList(), reach for a builder instead.
This post was written by a human with the help of Claude, an AI assistant by Anthropic.
