Coroutines, gently
Some work takes time, and doing it in the wrong place freezes the screen. This lesson explains why apps go numb, and introduces the three words — suspend, launch and delay — that let work pause without anything freezing.
Why apps freeze
You have seen this. You tap Save, and the app stops. Not a spinner — actually stops. The button stays pressed, scrolling does nothing, and after a few seconds Android offers to close the app for you.
Nothing has crashed. The app is busy. And "busy" turns out to have a very specific meaning, which is the whole of this lesson.
A small café with one barista behind the counter.
A customer orders tea. The barista fills the kettle, switches it on, and then stands there watching it. Two minutes pass. The queue does not move. Nobody else can even be greeted, because the only person who can serve them is staring at a kettle.
Now run the same café properly. The barista switches the kettle on, turns straight back to the queue, takes the next three orders, and returns when it whistles.
Same one barista. Same two-minute boil. Completely different café.
That is the entire idea behind coroutines: waiting is not the same as working, and you should not tie up your only worker to do it.
One worker, one counter
Android draws your screen on a single called the . A thread is one line of work, carried out strictly in order, one step after another.
The main thread has a lot to do. Roughly sixty times a second it works out what every pixel should look like and draws it. In between, it handles your taps, your scrolls, and your keyboard.
That is about sixteen milliseconds per frame. If your code takes longer than that on the main thread, a frame is missed. If it takes a second, sixty frames are missed and the screen is visibly frozen. If it takes five seconds, Android decides the app is broken and shows the dialog: Isn't responding — close app?
Meanwhile, plenty of ordinary jobs take far longer than sixteen milliseconds:
| Job | Roughly |
|---|---|
| Reading a hundred notes from a database | 10–100 ms |
| Loading a photo from storage | 50–500 ms |
| Waiting one tick of a timer | 1000 ms |
| Asking a server for something | anything at all |
So the rule is simple to state and easy to break: never make the main thread wait.
Blocking: the wrong way
1fun main() {
2 println("Order taken")
3 Thread.sleep(2000)
4 println("Serving tea")
5}Thread.sleep does exactly what the bad barista does. It holds on to the thread and does nothing with it for two seconds. That is called .
In this tiny practice program you just wait, and no harm is done. In an app, that same line freezes the screen — and you can see it happen in :
1I/Choreographer: Skipped 120 frames! The application
2may be doing too much work on its main thread.A hundred and twenty skipped frames is two seconds of a dead screen. Users notice at about three.
Suspending: the right way
Here is the same wait, done properly:
delay(2000)It looks almost identical, and it does something completely different. says "wake me in two seconds" and then hands the thread back. During those two seconds the thread is free to draw frames, handle taps, and run other work. When the time is up, your code carries on from exactly where it stopped.
Pausing without holding the thread is called suspending, and a piece of work that can do it is a .
The three words you need
1. suspend — this function is allowed to pause
1suspend fun boilKettle(): String {
2 delay(2000)
3 return "tea"
4}is a promise to the reader and a fact for the compiler: this function may stop partway through and continue later.
It comes with one rule: a suspend function can only be called from another suspend function, or from inside a coroutine. That is not bureaucracy. Pausing means somebody has to remember where you got to and come back for you, and only a coroutine does that job.
2. launch — start a coroutine and carry on
1launch {
2 println("Serving " + boilKettle())
3}starts the work and returns immediately. The line after it runs at once, without waiting for the block to finish. That is the barista turning back to the queue.
Work that starts now and finishes later is called , and it is worth getting used to the idea that the order your lines are written is no longer the order things finish.
3. runBlocking — the bridge, for practice programs only
Ordinary code cannot call launch or delay out of nowhere, because neither exists outside a coroutine. builds one and waits for everything inside to finish:
1fun main() = runBlocking {
2 // suspend functions are allowed in here
3}runBlocking does exactly what its name says: it blocks the thread it is on until the work inside is done. That is perfect for a practice program or a test, where blocking is the point.
It is wrong inside an app, where blocking the main thread is the exact problem you are trying to avoid. Real apps never need it: Part 4 gives you viewModelScope, and Part 3 gives you LaunchedEffect, both of which start coroutines that are already tied to the right lifetime. Use runBlocking on this page, then leave it behind.
launch, delay and runBlocking live in a library called kotlinx.coroutines, so the file needs import lines for them. Any Android project created by Pocket Studio already has that library, because Compose and the lifecycle tools depend on it.
- Open Pocket Studio, tap Projects, and open your Kotlin practice project.
- Tap Editor and open
Playground.kt. - Select everything in the file and delete it.
- Type the program from the walkthrough above, including the three
importlines at the top. - Tap Run, then watch the Output panel for about three seconds.
The lines arrive like this — the first two instantly, the third after two seconds, the last after two and a half:
1Order taken
2Next customer, please
3Serving tea
4Closing time- Read that order again. Line 15 of your file printed before line 13 finished. That is
launchdoing its job. - Now break the café. Delete the word
launchand the braces around line 13, leaving justprintln("Serving " + boilKettle())on its own. - Run again. The output is now in written order, and "Next customer" does not appear until after the two-second wait. Same functions, no
launch, a queue that stopped moving. - Put the
launchblock back.
What comes later
This has been deliberately gentle, and there is more. Real apps also need to say which thread a piece of work should run on, what happens when the user leaves the screen halfway through, and how a stream of values arriving over time is handled.
Those all have good answers, and you will meet them exactly when you need them rather than now:
- Part 3 starts coroutines from a screen with
LaunchedEffect. - Part 4 gives every ViewModel a
viewModelScope, and every database function you write will be asuspendfunction. - Focus Flow, in Part 5, is a timer built from a loop containing
delay(1000)— the exact shape you just typed, ticking once a second while the screen stays perfectly smooth.
For now, three sentences are enough to carry forward: slow work must not sit on the main thread; suspend marks a function that can pause; launch starts work that runs alongside everything else.
delay from ordinary code. Pausing needs a coroutine to remember where you got to, and there is not one here.suspend, or wrap the call in a coroutine. In a practice program that means fun main() = runBlocking { ... }; in an app it means viewModelScope.launch { ... }, which Part 4 covers.suspend in front of fun on the calling function, and keep going up until you reach a launch or runBlocking. The chain always ends at a coroutine.launch where there is no coroutine to launch from.import kotlinx.coroutines.launch at the top, and check that the call really is inside a runBlocking block or another coroutine.app/build.gradle.kts and add implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0") inside the dependencies block, then tap Sync. Most Pocket Studio projects already have it through Compose.Thread.sleep — and move it into a coroutine so the main thread is free to answer taps.- Android draws your screen on one , with about sixteen milliseconds per frame. Slow work there means missed frames, and enough of them means an dialog.
- —
Thread.sleep, or any long job — holds the thread and freezes the screen. Suspending steps aside and gives it back. - A is work that can pause and resume without holding a thread.
- marks a function that may pause, and it can only be called from another suspend function or from inside a coroutine.
- starts a coroutine and carries straight on; waits without blocking; is the bridge for practice programs and tests only.
- Next: Part 1 is finished — you can read and write Kotlin. Part 2 opens up Android itself, starting with what actually happens in the two seconds after you tap an app icon.