Coroutines Basics
suspend functions, launch, and async — Kotlin's lightweight approach to writing asynchronous code that reads like sequential code.
3 min de lecture
Any app that talks to a network, reads a file, or queries a database needs to run that work without freezing everything else while it waits — an Android app that blocks its main thread for a network call simply freezes the UI. Kotlin's answer is coroutines: lightweight, suspendable units of work that let asynchronous code read almost exactly like ordinary sequential code.
The problem coroutines solve
Traditional approaches to "don't block while waiting" tend to fall into one of two camps: callbacks, which nest deeply and get hard to follow once you chain more than two or three together, or raw threads, which are expensive to create in bulk and require careful synchronization. Coroutines are cheap enough to launch thousands of at once, and they let you write the logic top-to-bottom without callback nesting.
suspend functions
A function marked suspend can pause its execution without blocking the underlying thread, then resume later when the value it's waiting for is ready:
suspend fun fetchUser(id: Int): String {
delay(1000) // simulates a network call — pauses without blocking the thread
return "User #$id"
}delay() is coroutines' equivalent of Thread.sleep(), but it doesn't tie up a thread while it waits — the thread is freed to do other work, and the coroutine picks back up on (potentially any) available thread once the delay ends. A suspend function can only be called from another suspend function, or from inside a coroutine — that restriction is what lets the compiler guarantee suspension is handled correctly everywhere.
Starting a coroutine with launch
launch starts a new coroutine that runs and doesn't return a result directly — useful for "fire it off" work:
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
val user = fetchUser(1)
println(user)
}
println("Fetching...")
}
// Output:
// Fetching...
// User #1Notice the order: "Fetching..." prints before "User #1", because launch starts the coroutine and immediately continues past it — the coroutine's body runs concurrently rather than blocking the line after launch. (runBlocking here just bridges regular blocking code, like main(), into the coroutine world — you wouldn't typically use it inside a real Android app.)
Getting a result back with async
When you need a return value from concurrent work, async returns a Deferred<T>, and .await() suspends until that value is ready:
fun main() = runBlocking {
val deferredUser = async { fetchUser(1) }
val deferredPosts = async { fetchUser(2) } // pretend this fetches posts
// Both run concurrently — the total wait is ~1 second, not ~2
println(deferredUser.await())
println(deferredPosts.await())
}Because both async blocks start immediately and run concurrently, the total time is roughly however long the slower one takes, not the sum of both — a sequential pair of suspend calls without async would take twice as long.
Why this matters for Android specifically
Android's official guidance is to use coroutines for anything asynchronous — network calls, database access, file I/O — precisely because blocking the main thread, even briefly, causes visible UI jank or an "Application Not Responding" crash. Coroutines let that asynchronous code read top-to-bottom, in the same style as the rest of your logic, rather than scattered across callback lambdas.
This lesson only scratches the surface — real coroutine code also deals with structured concurrency, dispatchers, and cancellation — but suspend, launch, and async are the vocabulary everything else builds on. The final lesson in this course introduces Jetpack Compose, the UI framework where you'll see coroutines, lambdas, and everything else from this course come together in practice.