Pocket Studio Academy
HomePart 11.19

Coroutines, gently

Full course12 min read·4 questions

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.

Think of it like this

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:

JobRoughly
Reading a hundred notes from a database10–100 ms
Loading a photo from storage50–500 ms
Waiting one tick of a timer1000 ms
Asking a server for somethinganything at all

So the rule is simple to state and easy to break: never make the main thread wait.

Blocking: the wrong way

Playground.ktkotlin
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 :

text
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:

Playground.ktkotlin
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

Playground.ktkotlin
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

Playground.ktkotlin
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:

Playground.ktkotlin
1fun main() = runBlocking {
2  // suspend functions are allowed in here
3}
Careful

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.

Where these names come from

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.

Try it in Pocket Studio
  1. Open Pocket Studio, tap Projects, and open your Kotlin practice project.
  2. Tap Editor and open Playground.kt.
  3. Select everything in the file and delete it.
  4. Type the program from the walkthrough above, including the three import lines at the top.
  5. 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:

text
1Order taken
2Next customer, please
3Serving tea
4Closing time
  1. Read that order again. Line 15 of your file printed before line 13 finished. That is launch doing its job.
  2. Now break the café. Delete the word launch and the braces around line 13, leaving just println("Serving " + boilKettle()) on its own.
  3. 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.
  4. Put the launch block 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 a suspend function.
  • 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.

Error Doctor5 common errors
e: Suspend function 'delay' should be called only from a coroutine or another suspend function
MeansYou called delay from ordinary code. Pausing needs a coroutine to remember where you got to, and there is not one here.
FixEither mark the function you are in with 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.
e: Suspend function 'boilKettle' should be called only from a coroutine or another suspend function
MeansSame rule, one step up the chain: the moment a function of yours calls a suspend function, it must itself be suspend or be inside a coroutine.
FixAdd 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.
e: Unresolved reference: launch
MeansThe name is not visible in this file. Either the import is missing, or you called launch where there is no coroutine to launch from.
FixAdd import kotlinx.coroutines.launch at the top, and check that the call really is inside a runBlocking block or another coroutine.
e: Unresolved reference: kotlinx
MeansThe coroutines library itself is not on this project's list of dependencies, so there is nothing for the import to point at.
FixOpen 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.
ANR in com.example.app Reason: Input dispatching timed out
MeansNot a compiler error — this one appears in Logcat while the app is running, and the user sees a dialog offering to close it. Android waited several seconds for your app to respond to a tap and gave up.
FixFind the slow work happening on the main thread — a database read, a big loop, a Thread.sleep — and move it into a coroutine so the main thread is free to answer taps.
Recap
  • 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.