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:

  1. Chop vegetables (CPU work).
  2. Microwave them for 2 minutes (I/O task).
  3. Prepare a salad while waiting (CPU work).
  4. Serve the meal.

The Blocking Way (The Inefficient Kitchen)

Without coroutines, chef operates like this:

  1. Chops the vegetables.
  2. Puts them in the microwave, hits start, and then… stands there, staring at the timer for two full minutes. Nothing else gets done.
  3. 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 🙂).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fun main() {
    // This all runs on the main thread
    println("Chef starts cooking.")
    println("1. Chopping vegetables.")

    // Starting the microwave (main thread is BLOCKED!)
    println("2. Putting food in microwave.")
    Thread.sleep(2000)
    println("Microwave finished.")

    println("3. Preparing salad.")
    println("4. Serving the meal.")
    println("Chef finished cooking.")
}

Output:

1
2
3
4
5
6
7
8
Chef starts cooking.
1. Chopping vegetables.
2. Putting food in microwave.
(...2 second pause where nothing happens...)
Microwave finished.
3. Preparing salad.
4. Serving the meal.
Chef finished cooking.

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:

  1. Chops the vegetables.
  2. Puts them in the microwave, hits start and immediately walks away to do other work. This is a suspension point. The chef is free!
  3. Prepares the salad.
  4. 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 called runBlocking. 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import kotlinx.coroutines.*

// runBlocking creates a scope and blocks the main thread
fun main() = runBlocking {
    println("Chef starts cooking.")
    println("1. Chopping vegetables.")

    // launch starts a new task in the background
    launch {
        // Calling our suspend function.
        microwaveFood()
    }

    // This runs IMMEDIATELY after launch (without waiting!)
    println("3. Preparing salad.")
    println("4. Waiting for everything to finish to serve the meal.")
    // runBlocking waits here for the launch block to complete
}

suspend fun microwaveFood() {
    println("2. Putting food in microwave.")
    // Suspension point. It pauses coroutine, not the thread
    delay(2000)
    println("Microwave finished.")
}

Output:

1
2
3
4
5
6
7
Chef starts cooking.
1. Chopping vegetables.
3. Preparing salad.
4. Waiting for everything to finish to serve the meal.
2. Putting food in microwave.
(...2 second pause where the app is NOT frozen...)
Microwave finished.

Launch

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Chef: Starting to prepare a meal and a sauce.")

    // Start making the sauce in the background with async
    val deferredSauce: Deferred<String> = async {
        prepareSauce()
    }

    // While the sauce is preparing, chef gets to work
    println("Chef: Preparing the main course...")
    delay(1000)
    println("Chef: Main course is ready.")

    // We need the sauce now
    // The coroutine suspends here if the sauce isn't ready yet
    val sauce = deferredSauce.await()
    println("Chef: Got the sauce! It's '$sauce'.")
    println("Chef: Combining everything and serving the meal.")
}

suspend fun prepareSauce(): String {
    println("Assistant: Starting to prepare the sauce...")
    delay(2000)
    println("Assistant: Sauce is ready!")
    return "Tomato Sauce"
}

Output:

1
2
3
4
5
6
7
8
9
Chef: Starting to prepare a meal and a sauce.
Chef: Preparing the main course...
Assistant: Starting to prepare the sauce...
(1 second passes)
Chef: Main course is ready.
(another 1 second passes)
Assistant: Sauce is ready!
Chef: Got the sauce! It's 'Tomato Sauce'.
Chef: Combining everything and serving the meal.

AsyncAwait

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!