Free Handbook · Every example compiled & verified

Coroutines

Suspend functions, launch and async, structured concurrency, dispatchers, cancellation, exceptions, Flow and Channels, and viewModelScope on Android.

0 / 136 lessons🔥 0 day streak
ShareXLinkedIn

Module 10 · what you'll be able to do

  • Explain why a coroutine is cheaper than a thread, and what "suspend" actually does to a function
  • Start concurrent work with runBlocking, launch and async/await inside a CoroutineScope
  • Pick the right dispatcher (Main, IO, Default) and move blocking work with withContext
  • Cancel work, add timeouts, and handle exceptions without leaking coroutines or swallowing cancellation
  • Use Flow for streams of values, Channels for hand-offs, and viewModelScope in an Android ViewModel
01

Why coroutines: threads are expensive

A program that calls a web API, a database and a file system spends most of its time waiting. The classic JVM answer is a thread per task: while one thread waits for the network, another runs. Threads work, but each one is an operating-system object with its own stack (around 1 MB reserved by default), and switching between them is done by the OS. A server with 10,000 open requests cannot comfortably hold 10,000 threads, and an Android app must never block its single UI thread at all.

Kotlin's standard library can start threads directly with thread { }. The program below is correct, but notice how much ceremony even four tasks need: create, start, remember, join. Printing only after every join() is what keeps the output deterministic.

kotlinMain.kt
import kotlin.concurrent.thread

fun main() {
    val results = IntArray(4)
    val workers = (0 until 4).map { i ->
        thread(name = "worker-$i") {
            results[i] = (1..1_000).sumOf { it * (i + 1) }
        }
    }
    workers.forEach { it.join() }      // wait for every thread before reading results
    println(results.toList())
    println("total = ${results.sum()}")
}
Outputcompiled & run with real Kotlin
[500500, 1001000, 1501500, 2002000]
total = 5005000
Your turn

Remove the join() line and run it a few times. Why can the printed list now contain zeros?

A coroutine is a computation that can pause in the middle (suspend) without holding a thread, and resume later — possibly on a different thread. While it is paused it is just a small object on the heap holding its local variables. Thousands of coroutines share a handful of threads. The documentation's classic demonstration starts 50,000 of them:

kotlinMain.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    repeat(50_000) {           // 50,000 concurrent coroutines
        launch {
            delay(5_000L)       // suspends: the thread is free while this waits
            print(".")
        }
    }
}

Prints 50,000 dots after about five seconds, using one thread. The same program with thread { Thread.sleep(5000) } would create 50,000 OS threads and is likely to run out of memory.

Thread

  • Scheduled by the operating system
  • Blocks while waiting (Thread.sleep, blocking I/O)
  • Around 1 MB of stack each; thousands is a lot
  • Cancel by interrupting and hoping the code checks

Coroutine

  • Scheduled by Kotlin onto a pool of threads
  • Suspends while waiting (delay, suspending I/O); the thread runs other work
  • A few hundred bytes each; hundreds of thousands is fine
  • Cancellation and timeouts are built in and propagate to children
Two layers: the language and the library
The suspend keyword and the low-level machinery (Continuation, sequence { }) are in the language and standard library. Everything you use day to day — launch, async, runBlocking, delay, Dispatchers, Flow — lives in the kotlinx.coroutines library, added with one Gradle line: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2"). The runnable examples in this module use only the standard library; the library code is shown as snippets, with its output described exactly.
02

suspend functions

Marking a function suspend means "this function may pause partway through". Inside, it reads like ordinary sequential code: call, get a result, use it. A suspend function can only be called from another suspend function or from a coroutine, because only those have somewhere to save their state while paused. The entry point can be suspend fun main() — that is plain Kotlin, no library needed.

kotlinMain.kt
suspend fun loadUser(id: Int): String {
    println("loading user $id")
    return "Asha"
}

suspend fun loadOrders(user: String): List<String> {
    println("loading orders for $user")
    return listOf("#101", "#102")
}

suspend fun main() {
    val user = loadUser(7)
    val orders = loadOrders(user)
    println("$user has ${orders.size} orders: $orders")
}
Outputcompiled & run with real Kotlin
loading user 7
loading orders for Asha
Asha has 2 orders: [#101, #102]

No callbacks and no .then() chains: suspend code is written top to bottom, and each call finishes before the next line runs.

Error you will hit

Calling a suspend function from a regular function

kotlin
suspend fun fetchUser(id: Int): String = "user-$id"

fun main() {
    println(fetchUser(1))
}
Main.kt:4:13: error: suspend function 'suspend fun fetchUser(id: Int): String' can only be called from a coroutine or another suspend function.
    println(fetchUser(1))
            ^^^^^^^^^
Why the compiler said that

A regular function has no way to pause. If fetchUser suspended, main would have nowhere to keep its place, so the compiler refuses the call. This is the error you see most often when you start with coroutines.

The fix

Make the caller suspend too (here, suspend fun main()), or start a coroutine for it: runBlocking { } in main and tests, viewModelScope.launch { } on Android, a framework-provided scope on a server. Do not sprinkle runBlocking inside library code to silence this error.

kotlin
suspend fun fetchUser(id: Int): String = "user-$id"

suspend fun main() {
    println(fetchUser(1))
}

What the compiler does with suspend

The compiler rewrites every suspend function to take one extra hidden parameter, a Continuation: an object that holds "the rest of the function" plus its local variables. To suspend, a function stores that continuation somewhere and returns. To resume, someone calls continuation.resume(value), and the function continues from the line after the suspension point. The standard library exposes this directly through suspendCoroutine and startCoroutine, so you can watch it happen with no threads at all:

kotlinMain.kt
import kotlin.coroutines.*

var saved: Continuation<String>? = null

suspend fun waitForReply(): String = suspendCoroutine { cont ->
    println("  suspending, continuation saved")
    saved = cont
}

fun main() {
    val body: suspend () -> Unit = {
        println("coroutine: start")
        val reply = waitForReply()
        println("coroutine: resumed with $reply")
    }
    body.startCoroutine(Continuation(EmptyCoroutineContext) { result ->
        println("coroutine: finished ok=${result.isSuccess}")
    })
    println("main: the coroutine is paused, main keeps going")
    saved?.resume("pong")
    println("main: done")
}
Outputcompiled & run with real Kotlin
coroutine: start
  suspending, continuation saved
main: the coroutine is paused, main keeps going
coroutine: resumed with pong
coroutine: finished ok=true
main: done

startCoroutine ran the body until it suspended, then returned control to main. Calling resume("pong") made waitForReply() return "pong" and the body ran to the end. kotlinx.coroutines is built on exactly this mechanism; delay saves the continuation in a timer instead of a variable.

Your turn

Call saved?.resume("again") a second time at the end of main. Read the exception: a continuation may be resumed only once.

03

See suspension happen: sequence and iterator builders

The easiest place to watch a coroutine suspend is the standard library's sequence { } builder (you met sequences as lazy collections in Collections). The block is a coroutine. Each yield(value) hands one value to the consumer and suspends the block right there; the block resumes only when the consumer asks for the next value. Two pieces of code take turns on one thread, with no callbacks.

kotlinMain.kt
fun main() {
    val tickets = sequence {
        println("  [builder] preparing ticket 1")
        yield("T-1")
        println("  [builder] preparing ticket 2")
        yield("T-2")
        println("  [builder] preparing ticket 3")
        yield("T-3")
        println("  [builder] done")
    }

    for (t in tickets.take(2)) {
        println("consumer got $t")
    }
}
Outputcompiled & run with real Kotlin
  [builder] preparing ticket 1
consumer got T-1
  [builder] preparing ticket 2
consumer got T-2

The lines interleave, and ticket 3 is never prepared: after take(2) is satisfied, nobody resumes the builder, so it stays suspended at yield("T-2") and is simply garbage-collected.

Your turn

Change take(2) to take(5). Which extra lines appear, and how many tickets does the consumer get?

VisualizeTwo coroutines taking turnsStep 1 / 10
fun main() {
val tickets = sequence {
println(" [builder] preparing ticket 1")
yield("T-1")
println(" [builder] preparing ticket 2")
yield("T-2")
println(" [builder] preparing ticket 3")
yield("T-3")
println(" [builder] done")
}
for (t in tickets.take(2)) {
println("consumer got $t")
}
}
Line 2

sequence { } only stores the block. Nothing inside it runs yet.

Variables now
buildernot started
All 10 steps as a table
StepLineWhat happenedVariables now
12sequence { } only stores the block. Nothing inside it runs yet.builder = not started
212The loop asks for the first element, so the builder starts running.builder = running taken = 0
33The builder prints its first line.
44yield("T-1") hands the value to the loop and suspends the builder at this line.builder = suspended at line 4 t = "T-1" taken = 1
513The loop body runs with the value.
612The loop asks for the next element, which resumes the builder just after line 4.builder = running
75Execution continues where it paused, with all its state intact.
86yield("T-2") suspends again.builder = suspended at line 6 t = "T-2" taken = 2
913The loop prints the second ticket.
1012take(2) already has two values, so it answers "no more" without resuming the builder. Line 7 never runs.builder = suspended forever (collected)

Because a suspended builder costs nothing, it can describe an infinite stream: the loop only runs as far as the consumer pulls. yieldAll yields every element of a collection or another sequence. iterator { } is the same builder when you need an Iterator rather than a Sequence.

kotlinMain.kt
fun fibonacci(): Sequence<Long> = sequence {
    var a = 0L
    var b = 1L
    while (true) {          // infinite, but only runs as far as someone pulls
        yield(a)
        val next = a + b
        a = b
        b = next
    }
}

fun countdown(from: Int): Iterator<Int> = iterator {
    yieldAll(from downTo 1)
    yield(0)
}

fun main() {
    println(fibonacci().take(10).toList())
    println(fibonacci().first { it > 1000 })
    println(countdown(3).asSequence().toList())
}
Outputcompiled & run with real Kotlin
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
1597
[3, 2, 1, 0]
Your turn

Write fun evens() = sequence { … } that yields 0, 2, 4, … forever, and print the first five with take(5).toList().

Error you will hit

Calling your own suspend function inside sequence { }

kotlin
suspend fun pause() {}

val numbers = sequence {
    yield(1)
    pause()
    yield(2)
}

fun main() {
    println(numbers.toList())
}
Main.kt:5:5: error: restricted suspending functions can invoke member or extension suspending functions only on their restricted coroutine scope.
    pause()
    ^^^^^
Why the compiler said that

The sequence block is a restricted coroutine: the only way it may suspend is through yield and yieldAll, because the consumer is a plain loop that knows how to resume it only after a yield. An arbitrary suspend function (such as a network call or delay) could pause the builder with nobody to resume it.

The fix

Keep sequence builders to pure computation plus yield. If producing each value needs real suspending work (I/O, delay), you want a Flow from kotlinx.coroutines, which is covered below.

kotlin
fun pause() {}   // a regular function is fine

val numbers = sequence {
    yield(1)
    pause()
    yield(2)
}

fun main() {
    println(numbers.toList())
}
Error you will hit

Asking an iterator builder for more than it has

kotlin
fun main() {
    val letters = iterator {
        yield("a")
    }
    println(letters.next())
    println(letters.next())
}
a
Exception in thread "main" java.util.NoSuchElementException
	at kotlin.sequences.SequenceBuilderIterator.nextNotReady(SequenceBuilder.kt:188)
	at kotlin.sequences.SequenceBuilderIterator.next(SequenceBuilder.kt:171)
	at MainKt.main(Main.kt:6)
	at MainKt.main(Main.kt)
Why the compiler said that

The second next() resumed the builder, which ran to the end of its block without yielding again. There was no value to return, so the iterator threw.

The fix

Check hasNext() before next(), or let a for loop or asSequence().toList() drive the iterator for you.

kotlin
fun main() {
    val letters = iterator {
        yield("a")
    }
    while (letters.hasNext()) println(letters.next())
}
04

runBlocking, launch, async and structured concurrency

kotlinx.coroutines gives you coroutine builders — functions that start a coroutine. runBlocking { } blocks the current thread until everything inside finishes; it bridges normal code to coroutines and belongs in main and tests, not in production request handlers. launch { } starts a fire-and-forget coroutine and returns a Job. async { } starts one that computes a value and returns a Deferred<T>; await() suspends until the value is ready.

kotlinMain.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(100L)
        println("World")
    }
    println("Hello")
}

Prints Hello then World. launch returns immediately, so "Hello" prints first; the launched coroutine suspends in delay and resumes 100 ms later. runBlocking does not return until its child finishes.

kotlinMain.kt
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

suspend fun loadPrice(): Int { delay(300L); return 40 }
suspend fun loadTax(): Int { delay(300L); return 2 }

fun main() = runBlocking {
    val ms = measureTimeMillis {
        val price = async { loadPrice() }   // starts now
        val tax = async { loadTax() }       // also starts now
        println("total = ${price.await() + tax.await()}")
    }
    println("took about $ms ms")
}

Prints total = 42, then a time of roughly 300 ms — not 600 — because both calls wait at the same time. Calling loadPrice() and loadTax() directly, without async, would run them one after the other.

Structured concurrency: every coroutine has a parent

Every coroutine is started inside a CoroutineScope, and the scope is its parent. That gives three guarantees: a parent waits for all its children before it completes; cancelling a parent cancels all its children; and a child that fails cancels its parent and siblings, so an error is never silently lost. Inside a suspend function, coroutineScope { } creates a child scope and returns only when everything launched in it is done.

kotlinMain.kt
import kotlinx.coroutines.*

suspend fun loadDashboard() = coroutineScope {
    launch { delay(200L); println("orders loaded") }
    launch { delay(100L); println("profile loaded") }
    println("requests started")
}   // does not return until both launches finish

fun main() = runBlocking {
    loadDashboard()
    println("dashboard ready")
}

Prints requests started, profile loaded, orders loaded, dashboard ready. "dashboard ready" is guaranteed to be last because coroutineScope waits for its children.

Avoid GlobalScope
GlobalScope.launch { } starts a coroutine with no parent. Nothing waits for it, nothing cancels it when the screen or request that started it goes away, and its failures are not propagated. The library marks it @DelicateCoroutinesApi for that reason. Use a scope that has a lifetime: coroutineScope, viewModelScope, or one your framework gives you.
05

Dispatchers, cancellation and timeouts

A dispatcher decides which thread or threads a coroutine runs on. You choose it when you launch, or switch it for a block with withContext(dispatcher) { }, which suspends the caller and returns the block's result. The rule most teams follow: the function that does blocking or heavy work switches dispatcher itself, so callers can call it from anywhere ("main-safe").

DispatcherThreadsUse for
Dispatchers.MainThe UI thread (Android, Swing, JavaFX; needs the platform artifact)Updating the UI, calling suspend functions that are main-safe
Dispatchers.IOA large pool (64 threads or the number of cores, whichever is larger)Blocking calls: JDBC, files, legacy HTTP clients
Dispatchers.DefaultOne thread per CPU core (at least 2)CPU work: parsing, sorting, image processing
Dispatchers.UnconfinedWhatever thread resumed itRarely; tests and special cases
kotlin
import kotlinx.coroutines.*
import java.io.File

// main-safe: callers do not need to know this blocks
suspend fun readConfig(path: String): String = withContext(Dispatchers.IO) {
    File(path).readText()
}

suspend fun checksum(data: ByteArray): Long = withContext(Dispatchers.Default) {
    data.fold(0L) { acc, b -> acc * 31 + b }
}

No output on its own: these are library functions. Thread names printed from inside coroutines (DefaultDispatcher-worker-1 and so on) vary from run to run, so never rely on them in tests.

Cancellation is cooperative

job.cancel() asks a coroutine to stop. It actually stops at the next suspension point: every suspending function in kotlinx.coroutines (delay, await, withContext, channel operations) checks for cancellation and throws CancellationException. A tight CPU loop with no suspension point never checks, so it keeps running — call ensureActive() or check isActive inside long loops.

kotlinMain.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        repeat(1_000) { i ->
            println("job: working $i")
            delay(500L)
        }
    }
    delay(1_300L)
    println("main: cancelling")
    job.cancelAndJoin()        // cancel, then wait until it has really stopped
    println("main: done")
}

Prints job: working 0, job: working 1, job: working 2, main: cancelling, main: done. The job was sitting in delay when the cancel arrived, so it stopped there.

A timeout is cancellation on a timer. withTimeout(ms) { } throws TimeoutCancellationException when time runs out; withTimeoutOrNull(ms) { } returns null instead, which is usually what you want for "try this, fall back otherwise".

kotlinMain.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    val result = withTimeoutOrNull(1_300L) {
        repeat(1_000) { i ->
            println("fetching page $i")
            delay(500L)
        }
        "all pages"
    }
    println("result = $result")
}

Prints fetching page 0, 1 and 2, then result = null: the block was cancelled at 1,300 ms, before it could return "all pages".

Never call Thread.sleep inside a coroutine
Thread.sleep, a blocking JDBC call or File.readText() on Dispatchers.Main freezes the thread for every coroutine sharing it — on Android that is a frozen screen and eventually an "App Not Responding" dialog. Use delay to wait, and wrap blocking calls in withContext(Dispatchers.IO).
06

Exception handling in coroutines

Exceptions follow the parent-child tree. When a child fails, it cancels its parent, the parent cancels the other children, and the exception is rethrown where the scope was entered. That means an ordinary try/catch around coroutineScope { } (or around await()) catches a failure from any child inside it.

kotlinMain.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    val total = try {
        coroutineScope {
            val price = async { 40 }
            val tax = async<Int> { throw IllegalStateException("tax service down") }
            price.await() + tax.await()
        }
    } catch (e: IllegalStateException) {
        println("caught: ${e.message}")
        -1
    }
    println("total = $total")
}

Prints caught: tax service down, then total = -1. The failure of one async cancelled the whole coroutineScope, and the exception surfaced at its boundary.

Sometimes one child failing should not cancel the others — loading three independent widgets on a screen, for example. supervisorScope { } (or a SupervisorJob) makes failures one-way: a failed child does not cancel its siblings or parent. An uncaught exception in a launch then goes to a CoroutineExceptionHandler, the last-resort handler for fire-and-forget coroutines.

kotlinMain.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, e -> println("handled: ${e.message}") }
    supervisorScope {
        launch(handler) { throw RuntimeException("widget A failed") }
        launch { delay(100L); println("widget B loaded") }
    }
    println("screen done")
}

Prints handled: widget A failed, widget B loaded, screen done. With coroutineScope instead of supervisorScope, widget B would have been cancelled and the exception would have crashed main.

Do not swallow CancellationException
Cancellation is delivered as a CancellationException. A broad catch (e: Exception) (or runCatching { }) around suspending code catches it too, and the coroutine carries on as if nothing happened — a cancelled screen keeps loading, a timed-out request keeps running. Catch the specific exceptions you expect (IOException, HttpException), or rethrow: catch (e: CancellationException) { throw e } before the general catch.
07

Flow and Channels

A suspend function returns one value. A Flow<T> emits many values over time — search results as the user types, a price that updates, rows streamed from a database. It is the suspending cousin of a sequence: the flow { } builder calls emit(value) instead of yield, and, unlike a sequence, it may call any suspend function in between. A flow is cold: nothing runs until a terminal operator such as collect is called, and each collector gets its own run.

kotlinMain.kt
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun readings(): Flow<Int> = flow {
    for (i in 1..4) {
        delay(100L)          // allowed here, unlike inside sequence { }
        emit(i)
    }
}

fun main() = runBlocking {
    readings()
        .map { it * it }
        .filter { it % 2 == 0 }
        .collect { println("got $it") }
    println("done")
}

Prints got 4, got 16, done. The operators (map, filter, take, debounce, combine…) read like collection functions and run lazily as values arrive.

Two hot flows hold state for UI and events. StateFlow<T> always has a current value and replays it to new collectors — the standard way to expose screen state from a ViewModel. SharedFlow<T> broadcasts events to every current collector. Use flowOn(Dispatchers.IO) to run the upstream part of a flow on another dispatcher.

A Channel is a queue between coroutines: one side calls send, the other receive, and each suspends when it has to wait. Each value is delivered to exactly one receiver, which makes channels a fit for worker pools and producer-consumer pipelines. In application code you will use Flow far more often; channels are the lower-level tool Flow is partly built on.

kotlinMain.kt
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

fun main() = runBlocking {
    val jobs = Channel<Int>()
    launch {
        for (id in 1..3) jobs.send(id * 10)   // suspends until someone receives
        jobs.close()                          // no more values: ends the for loop below
    }
    for (id in jobs) println("processing $id")
    println("queue drained")
}

Prints processing 10, processing 20, processing 30, queue drained. Forgetting close() leaves the receiving loop suspended forever, and runBlocking never returns.

ToolValuesHot or coldCan suspend between values?Library
sequence { }Many, pulledColdNo, only yieldstdlib
suspend functionOneRuns when calledYeslanguage
FlowMany, pushed to collectColdYeskotlinx.coroutines
StateFlow / SharedFlowMany, broadcastHotYeskotlinx.coroutines
ChannelMany, each to one receiverHotYeskotlinx.coroutines
08

Coroutines on Android: viewModelScope

Android is where most Kotlin developers meet coroutines, and it shows why structured concurrency matters. A ViewModel has viewModelScope, a scope that runs on Dispatchers.Main.immediate and is cancelled automatically when the ViewModel is cleared (the user leaves the screen for good). Work started there cannot outlive the screen or leak. Activities and fragments have lifecycleScope for the same reason.

kotlinOrdersViewModel.kt
sealed interface OrdersUiState {
    data object Loading : OrdersUiState
    data class Loaded(val orders: List<Order>) : OrdersUiState
    data class Error(val message: String) : OrdersUiState
}

class OrdersViewModel(private val repo: OrdersRepository) : ViewModel() {
    private val _state = MutableStateFlow<OrdersUiState>(OrdersUiState.Loading)
    val state: StateFlow<OrdersUiState> = _state.asStateFlow()

    init { refresh() }

    fun refresh() {
        viewModelScope.launch {                   // cancelled in onCleared()
            _state.value = OrdersUiState.Loading
            _state.value = try {
                OrdersUiState.Loaded(repo.fetchOrders())   // main-safe suspend fun
            } catch (e: IOException) {
                OrdersUiState.Error("Check your connection")
            }
        }
    }
}

class OrdersRepository(private val api: OrdersApi) {
    suspend fun fetchOrders(): List<Order> = withContext(Dispatchers.IO) {
        api.getOrders()
    }
}

The shape of almost every modern Android screen: a sealed UI state (Module 08), a StateFlow the UI collects, and viewModelScope.launch for the work. Only IOException is caught, so cancellation still propagates.

In Jetpack Compose the screen reads that state with val state by viewModel.state.collectAsStateWithLifecycle(), which stops collecting while the app is in the background. With classic views, collect inside lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { … } }. Libraries you will use alongside — Retrofit, Room, DataStore, Ktor client — all expose suspend functions or Flows directly.

Coroutine
A computation that can suspend and later resume without blocking a thread.
suspend
A modifier marking a function that may pause; callable only from a coroutine or another suspend function.
Continuation
The compiler-generated object holding the rest of a suspended function and its local variables.
Suspension point
A call where a coroutine may pause, such as yield, delay or await; also where cancellation is checked.
CoroutineScope
The owner of a set of coroutines; it waits for them, cancels them, and receives their failures.
Structured concurrency
The rule that every coroutine has a parent scope, so none can leak or fail silently.
launch / Job
Starts a coroutine that returns no value; the Job lets you cancel or join it.
async / Deferred
Starts a coroutine that computes a value; await() suspends until it is ready.
Dispatcher
Decides which thread(s) a coroutine runs on: Main, IO, Default or Unconfined.
withContext
Runs a block on another dispatcher and returns its result, suspending the caller meanwhile.
Flow
A cold stream of values produced by suspending code and consumed with collect.
StateFlow
A hot flow that always holds a current value; used to expose UI state.
Channel
A suspending queue that hands each value from a sender to exactly one receiver.
viewModelScope
An Android ViewModel's scope, cancelled automatically when the ViewModel is cleared.
Quick check

Inside runBlocking, what does launch { delay(100); println("B") }; println("A") print?

Quick check

A sequence { } has three yield calls, and the caller runs .first() on it. How many of the yields execute?

Frequently asked questions

Are Kotlin coroutines the same as threads?
No. A coroutine runs on a thread, but while it is suspended (waiting for I/O or a delay) it gives the thread back so other coroutines can use it. Thousands of coroutines can share a few threads, which is why they are much cheaper than one thread per task. Java 21 virtual threads solve a similar problem at the JVM level; coroutines add structured concurrency, cancellation and Flow on top, and also work on Android and Kotlin Multiplatform.
Do I need a library to use coroutines in Kotlin?
The suspend keyword, suspend fun main() and the sequence { } builder are part of the language and standard library. The practical API — launch, async, runBlocking, delay, dispatchers and Flow — comes from the kotlinx.coroutines library, which you add as one Gradle dependency. Android projects usually get it through the lifecycle and ViewModel libraries.
Should I use Flow or LiveData on Android?
New code uses StateFlow for UI state. It is plain Kotlin (testable without Android), has the full set of Flow operators, and works with Compose through collectAsStateWithLifecycle(). LiveData still works and you will see it in older codebases, but it is Android-only and has far fewer operators.

Finish the Kotlin handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.