Coroutines let you write asynchronous code that reads like sequential code — no callbacks. Learn suspend functions, launching concurrent work, structured concurrency, and Flow for streams of async values.
A coroutine is a lightweight thread you can suspend and resume without blocking. Marking a function suspend means it can pause at a suspension point (like a network call) and resume later, so asynchronous code reads top-to-bottom like normal code instead of nested callbacks. Thousands of coroutines can run on a few real threads, which is why they scale so well.
import kotlinx.coroutines.*
// 'suspend' = this function can pause without blocking a thread
suspend fun fetchUser(): String {
delay(1000) // suspends here (non-blocking) for 1s
return "Ada"
}
fun main() = runBlocking { // a coroutine scope for main
println("fetching...")
val user = fetchUser() // reads sequentially, but doesn't block a thread
println("got $user")
}
// no callbacks, no nesting — async code that reads like sync code.Within a coroutine scope, launch starts a fire-and-forget coroutine and async starts one that returns a result via await. Using async for independent work lets tasks run concurrently — two network calls overlap instead of running one after the other — and you await both. This is how you parallelize I/O cleanly without thread management.
import kotlinx.coroutines.*
suspend fun loadProfile() = withContext(Dispatchers.IO) { delay(1000); "profile" }
suspend fun loadPosts() = withContext(Dispatchers.IO) { delay(1000); "posts" }
fun main() = runBlocking {
// sequential would take ~2s. async runs them CONCURRENTLY -> ~1s.
val profile = async { loadProfile() }
val posts = async { loadPosts() }
println("${profile.await()} + ${posts.await()}") // waits for both, ~1s total
launch { println("fire-and-forget side task") } // no result needed
}Coroutines are structured: they run inside a scope, and a scope does not complete until all its children do — and cancelling a scope cancels every coroutine in it. This prevents leaks (an Android screen’s scope cancels its work when the screen closes) and makes error handling predictable. You almost never manage threads by hand; the scope owns the lifecycle.
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("working $i")
delay(200) // suspension point where cancellation is checked
}
} finally {
println("cleaned up") // runs on cancellation
}
}
delay(500)
job.cancel() // cancel the coroutine
job.join()
// On Android, viewModelScope / lifecycleScope cancel automatically when the
// screen goes away -> no leaked background work. That's structured concurrency.
}Where a suspend function returns one value, a Flow emits a stream of values over time — asynchronously and lazily. You build a pipeline with the same operators as collections (map, filter) and collect the results, and the flow only runs when collected. Flow is the backbone of reactive Android: a database query or UI state exposed as a Flow updates consumers automatically as data changes.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
// emits a stream of values over time (e.g. sensor readings, DB updates)
fun tickers(): Flow<Int> = flow {
for (i in 1..3) {
delay(500)
emit(i) // push a value into the stream
}
}
fun main() = runBlocking {
tickers()
.map { it * 10 } // same operators as collections
.filter { it > 10 }
.collect { println(it) } // terminal: runs the flow -> 20, 30
// a Room query or UI state exposed as Flow re-emits when data changes ->
// the reactive foundation of modern Android (see the Architecture course).
}