That Frozen App Feeling. How to Fix It?
If you have ever built an app with a user interface, you have been in this situation: you kick off a network call, and suddenly, the whole app freezes. The buttons don’t respond, the animations stop and users start getting frustrated. This is the classic problem of long-running tasks on the main thread. It’s exactly what Kotlin Coroutines solve.
They give us a powerful way to write asynchronous code that looks and feels like simple, straightforward, synchronous code. No more “callback hell”!
To really get it, imagine a chef in a kitchen. Chef can only do one thing at a time, just like our app’s main UI thread.
Here’s the recipe:
- Chop vegetables (CPU work).
- Microwave them for 2 minutes (I/O task).
- Prepare a salad while waiting (CPU work).
- Serve the meal.
The Blocking Way (The Inefficient Kitchen)
Without coroutines, chef operates like this:
- Chops the vegetables.
- Puts them in the microwave, hits start, and then… stands there, staring at the timer for two full minutes. Nothing else gets done.
- Once the microwave finally beeps, the chef snaps back to life and starts the salad.
The result? An incredibly inefficient kitchen. The chef is completely blocked. In the app world, this is a frozen UI, and a frustrated user (and one-star review waiting to happen 🙂).
|
|
Output:
|
|
That 2-second pause is deadly for user experience. It should be fixed.
The Suspending Way (The Coroutine Kitchen)
So, how do coroutines pull this off? The secret is the suspend
keyword.
A suspend
function is special. It tells the Kotlin compiler, “Hey, this function might take a while. Feel free to pause it here and let the thread go do something else.
I’ll let you know when I’m ready to resume.”
The new, efficient chef does this:
- Chops the vegetables.
- Puts them in the microwave, hits start and immediately walks away to do other work. This is a suspension point. The chef is free!
- Prepares the salad.
- When the microwave beeps, the chef is notified and comes back to get the food.
But you can’t just call a suspend
function whenever you want.
You have to launch it within a coroutine. This is where Coroutine Builders come in. They are our entry point into this new, non-blocking way.
Task 1: Fire and Forget with launch
Let’s start with the simplest case: we need to run the microwave in the background. We don’t need a result back from it immediately. We just want to “fire and forget” the task.
For this, we use the launch
builder. Think of it as telling chef: “Go do this thing. I don’t need anything back from you right away, just get it done.”
A quick but important warning: To run these in a
main
function, we use a special builder calledrunBlocking
. It’s designed to bridge the blocking world with the suspending world of coroutines. It will block the main thread until every coroutine inside it finishes. This is great for demos and tests, but NEVER use it in production Android code.
|
|
Output:
|
|
The chef starts the salad right away. The microwave task runs concurrently in the background. We have achieved true non-blocking concurrency.
Task 2: Getting a Result Back with async
and await
Okay, launch
is great for kicking off background work. But let’s be real, most of the time we’re not just ‘firing and forgetting’, we’re fetching data. We need a result.
Imagine that the chef needs a special sauce. He tells his assistant to make it. He can keep working, but at some point, he will need to stop and wait for that sauce before he can finish the dish.
This is the job for async
. It’s another builder, but instead of just a Job
, it gives us back something called a Deferred<T>
.
Don’t let the fancy name scare you. It’s just a promise that will contain our value… eventually.
To get our value, we call .await()
. And here’s the key: .await()
is a suspend
function.
If the sauce isn’t ready, the chef will pause there (without blocking the thread!) until it is.
|
|
Output:
|
|
This is the beautiful part. The code still reads top-to-bottom, like a story.
No callbacks, no complicated reactive chains. We just async
the work and await
the result when we need it.
Wrap-up
That’s it for the fundamentals! We’ve covered the absolute core of coroutines:
- Why: Blocking threads freezes apps;
suspend
functions are the answer. - Fire-and-Forget: Use
launch
when you just need to start a background task. - Getting a Result: Use
async
to start a task that returns a value, and.await()
to get that value when you’re ready for it.
What’s Next in Part 2?
Our kitchen is running, but it’s a bit… magical. Where are background tasks actually running? What happens if the customer cancels their order halfway through? Do chefs just keep cooking forever, wasting resources?
We’ll dive into the safety net that makes coroutines so robust. We’ll talk about CoroutineScope, Job lifecycles, and Dispatchers to see how we can manage our coroutines and tell them exactly which part of the kitchen to work in. See you there!