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

TypeOrderedDuplicatesCommon implementation
ListYesYeslistOf / mutableListOf
SetInsertion order for LinkedHashSetNosetOf / mutableSetOf
MapInsertion order for LinkedHashMapUnique keysmapOf / 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.Main for UI, Dispatchers.IO for blocking I/O, Dispatchers.Default for CPU work.
  • Structured concurrency: a parent scope does not finish until its children do, and a child failure cancels its siblings. Never launch into GlobalScope from application code.
  • Cancellation is cooperative. A long loop must call ensureActive() or yield(), 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.

Classes, data classes and extensions Syntax and null safety

Last refreshed 2026-09-18.