On this page
An imperative loop that filters, sorts, groups and maps a list can easily take 25 lines and three mutable variables. The same logic as a Kotlin collection pipeline is often five lines, with no mutation and clear intent. This guide covers the operators you'll use constantly in Android code, and when to reach for sequences or immutable collections.
Read-only vs mutable collections
val tags: List<String> = listOf("kotlin", "compose") // read-only interface
val cart: MutableList<String> = mutableListOf("p1") // mutable
val updated = tags + "flow" // creates a NEW listList is a read-only view, not a deep guarantee of immutability. The underlying object might be mutable elsewhere. For UI state, create new lists rather than mutating, and consider kotlinx.collections.immutable (persistentListOf, ImmutableList) when you want truly immutable types that Compose treats as stable.
A real pipeline: API data to UI sections
data class Txn(val id: String, val amountPaise: Long, val status: String, val at: LocalDateTime)
data class TxnSection(val date: LocalDate, val totalPaise: Long, val items: List<Txn>)
fun toSections(txns: List<Txn>): List<TxnSection> =
txns
.filter { it.status != "FAILED" }
.sortedByDescending { it.at }
.groupBy { it.at.toLocalDate() } // Map<LocalDate, List<Txn>>, insertion-ordered
.map { (date, items) -> TxnSection(date, items.sumOf { it.amountPaise }, items) }LazyColumn {
sections.forEach { section ->
stickyHeader(key = section.date) { DateHeader(section.date, section.totalPaise) }
items(section.items, key = { it.id }) { txn -> TxnRow(txn) }
}
}The operators you'll use most
| Goal | Operator | Example |
|---|---|---|
| Transform each item | map, mapNotNull | dtos.mapNotNull { it.toDomainOrNull() } |
| Keep matching items | filter, filterIsInstance | items.filterIsInstance<Video>() |
| Find one | firstOrNull, find | users.firstOrNull { it.id == id } |
| Check | any, all, none | cart.any { it.qty == 0 } |
| Aggregate | sumOf, count, maxByOrNull, fold | cart.sumOf { it.qty * it.price } |
| Index by key | associateBy, associate | products.associateBy { it.id } |
| Group | groupBy, partition | val (done, todo) = tasks.partition { it.done } |
| Deduplicate | distinct, distinctBy | messages.distinctBy { it.id } |
| Sort | sortedBy, sortedWith(compareBy…) | sortedWith(compareBy({ it.priority }, { it.title })) |
| Split into pages | chunked, windowed | ids.chunked(50) for batched API calls |
| Combine two lists | zip | labels.zip(values) |
fold and reduce
data class Stats(val count: Int = 0, val totalPaise: Long = 0, val refunds: Int = 0)
val stats = txns.fold(Stats()) { acc, t ->
acc.copy(
count = acc.count + 1,
totalPaise = acc.totalPaise + t.amountPaise,
refunds = acc.refunds + if (t.status == "REFUNDED") 1 else 0,
)
}Prefer specific operators (sumOf, count) when they fit. Use fold when you compute several values in one pass.
Efficient lookups: avoid O(n²)
// ❌ For each order, scan all products: slow for large lists
val rows = orders.map { o -> o to products.first { it.id == o.productId } }
// ✅ Build a map once, then O(1) lookups
val productsById = products.associateBy { it.id }
val rows2 = orders.mapNotNull { o -> productsById[o.productId]?.let { p -> o to p } }Sequences: lazy evaluation
List operations are eager: each step creates a full intermediate list. asSequence() makes them lazy, processing one element at a time through the whole chain, and stopping early when possible.
val firstTenMatches = hugeList.asSequence()
.filter { it.matches(query) }
.map { it.toUi() }
.take(10) // stops as soon as 10 are found
.toList()Use sequences for large collections with several steps, or when you short-circuit with take or first. For small lists (typical UI lists of tens or hundreds of items), plain lists are usually just as fast and simpler.
Android-specific tips
- Transform data outside composables, in the ViewModel or with
remember(key), so pipelines don't rerun on every recomposition. - Move heavy processing off the main thread (
withContext(Dispatchers.Default)) for thousands of items. - Prefer SQL for filtering and aggregating large datasets in Room, rather than loading everything into memory and filtering in Kotlin.
Common mistakes
- Using
first { }when nothing may match, which throwsNoSuchElementException. UsefirstOrNull. - Mutating a list while iterating over it.
- Chaining
filterthencountwhencount { }does it in one pass. - Exposing
MutableListin UI state.
Frequently asked questions
Are functional operators slower than for loops?
Collection operators are inline functions, so lambdas don't allocate objects. Each step does create a new list, which rarely matters at UI-list sizes. Measure before optimising, and use sequences or a single loop for hot paths.
What's the difference between map and forEach?
map returns a new transformed collection. forEach returns nothing and is for side effects.
Does groupBy preserve order?
Yes. It returns a LinkedHashMap in which keys appear in first-encounter order, which is why sorting before grouping works.
Conclusion
Kotlin's collection API lets you express data transformations as small, readable, mutation-free pipelines. Learn the dozen core operators, build maps for lookups, use sequences for large or short-circuiting chains, and keep heavy transformations out of composables.
Next step: write your own reusable operators with Extension and Higher-Order Functions.
Official references
📱 Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it ▸
NotiVaultNotification history: recover dismissed notificationsGet it ▸
Ready to build real Android applications?
Kotlin fluency makes everything else in Android easier, from coroutines and Flow to Compose and clean architecture. The fastest way to build that fluency is to write real Kotlin every day and get feedback on it.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.