Coroutines and collections
Suspend functions without blocking a thread, structured concurrency, Flow for streams, and the collection operators you will use daily.
Collections and the operations on them
| Type | Ordered | Duplicates | Common implementation |
|---|---|---|---|
List | Yes | Yes | listOf / mutableListOf |
Set | Insertion order for LinkedHashSet | No | setOf / mutableSetOf |
Map | Insertion order for LinkedHashMap | Unique keys | mapOf / mutableMapOf |
data class Sale(val region: String, val amount: Int)
val sales = listOf(
Sale("EU", 120), Sale("US", 80), Sale("EU", 45), Sale("APAC", 200),
)
val big = sales.filter { it.amount >= 100 } // keeps order
val regions = sales.map { it.region }.distinct()
val byRegion = sales.groupBy { it.region } // Map<String, List<Sale>>
val totals = sales.groupBy { it.region }
.mapValues { (_, rows) -> rows.sumOf { it.amount } }
val ranked = sales.sortedByDescending { it.amount }
val firstEu = sales.firstOrNull { it.region == "EU" }
// sequences avoid building an intermediate list for long chains
sales.asSequence()
.filter { it.amount > 50 }
.map { it.amount }
.take(2)
.toList()The OrNull family (firstOrNull, singleOrNull, maxOrNull) returns null instead of throwing, which fits Kotlin's null-safety model far better than catching an exception from the throwing variant.
Coroutines
A coroutine is a suspendable computation. A suspend function can pause without blocking its thread, so thousands of concurrent operations run on a handful of threads instead of one thread each.
suspend fun loadUser(id: Long): User = withContext(Dispatchers.IO) {
api.fetch(id) // switches thread, then switches back
}
fun main() = runBlocking {
val one = launch { println(loadUser(1).email) } // fire and forget
val two = async { loadUser(2).email } // returns a Deferred
println(two.await())
one.join()
}
// parallel work with automatic cancellation
coroutineScope {
val (a, b) = listOf(async { loadUser(1) }, async { loadUser(2) }).map { it.await() }
}Dispatchers.Mainfor UI,Dispatchers.IOfor blocking I/O,Dispatchers.Defaultfor CPU work.- Structured concurrency: a parent scope does not finish until its children do, and a child failure cancels its siblings. Never launch into
GlobalScopefrom application code. - Cancellation is cooperative. A long loop must call
ensureActive()oryield(), or it will keep running after cancellation was requested.
Flow for streams
fun ticks(): Flow<Int> = flow {
var i = 0
while (true) {
emit(i++)
delay(1000)
}
}
val viewModelScopeJob = CoroutineScope(Dispatchers.Main).launch {
ticks()
.filter { it % 2 == 0 }
.map { it * 10 }
.flowOn(Dispatchers.Default) // upstream runs off the main thread
.collect { value -> render(value) }
}
// a hot, shared stream is the right shape for UI state
val state: StateFlow<String> = _state.asStateFlow()💡
A cold
flow builder reruns its body for every collector, and nothing happens until someone collects. For a value many observers share, convert once with shareIn or expose a StateFlow instead of letting each screen subscribe to a cold source.FAQ
launch or async?
launch when you do not need a result; failures surface through the parent scope. async when you need a value, which you obtain with await. Use async only inside a coroutineScope so a failure cannot escape unobserved.Why does my database call freeze the UI?
It is running on the main dispatcher. Move it to
Dispatchers.IO with withContext, and check that no runBlocking sits in a UI path.Related
Classes, data classes and extensions Syntax and null safety
Last refreshed 2026-09-18.