Kahibaro
Discord Login Register

20.2 Kotlin Coroutines

Why Coroutines Matter for Android

Kotlin coroutines give you a simple way to run work in the background without blocking the main thread. On Android the main thread is responsible for drawing the UI and handling user input. If you block it with network calls or heavy work the app feels frozen and can even show an ANR (Application Not Responding) dialog.

Coroutines help you write asynchronous code that still looks like normal sequential code. Instead of creating and managing threads directly you describe what should run in the background and what should run on the main thread. The coroutine system handles the scheduling for you.

In this chapter you focus on what coroutines are, how to start them, how to switch between background and main thread, and how to use them safely in Android.

Coroutines do not create magic performance. They help you avoid blocking the main thread and make async code easier to read, but heavy work is still heavy work and must run off the main thread.

Basic Coroutine Concepts

A coroutine is a lightweight task that can be suspended and resumed without blocking the underlying thread. You can think of it as a function that can pause at certain points and continue later.

Two ideas are important:

A suspend function is a function that can suspend its execution without blocking the thread. It can only be called from another suspend function or from inside a coroutine.

A CoroutineScope defines the lifecycle of coroutines that run inside it. When the scope is cancelled, all coroutines created in that scope are also cancelled.

In Android you usually work inside scopes that are aware of lifecycle components. For example lifecycleScope for an Activity or Fragment and viewModelScope inside a ViewModel. These are provided by AndroidX and tie the lifetime of coroutines to the UI component.

Starting Coroutines

You start coroutines with builder functions like launch and async. Both run code asynchronously inside a scope, but they have different use cases.

launch is used when you want to start a coroutine that does some work and you do not need a direct result. It returns a Job that you can use to cancel or observe the coroutine.

async is used when you want to calculate a value in the background and get a result later. It returns a Deferred<T> and you use await() to get the result.

Inside a coroutine you can call suspend functions such as delay to simulate long operations without blocking the thread.

import kotlinx.coroutines.*
fun exampleScope() {
    val scope = CoroutineScope(Dispatchers.Default)
    scope.launch {
        // Do some background work, no direct result
        delay(1000)
        println("Finished work in launch")
    }
    val deferred = scope.async {
        // Calculate and return a value
        delay(500)
        42
    }
    scope.launch {
        val result = deferred.await()
        println("Result is $result")
    }
}

On Android you usually avoid creating your own CoroutineScope tied to the global application because it can easily outlive UI components. Instead you prefer the lifecycle aware scopes that are provided for you.

Dispatchers and Threads

Dispatchers decide which thread or thread pool a coroutine runs on. The key dispatchers you use on Android are:

Dispatchers.Main runs code on the Android main thread. You use this when updating UI elements.

Dispatchers.IO is optimized for I/O work such as reading and writing files, network requests, or database operations.

Dispatchers.Default is optimized for CPU intensive work like sorting large lists or complex calculations.

You can choose the dispatcher when you start a coroutine by passing it to launch or async.

scope.launch(Dispatchers.IO) {
    // Network request or database query
}
scope.launch(Dispatchers.Main) {
    // Update UI elements
}

You can also switch dispatchers inside a coroutine using the withContext function. This is useful when you start on the main thread, do some work on IO or Default, then return to the main thread with the result.

suspend fun loadDataAndUpdateUI() {
    val data = withContext(Dispatchers.IO) {
        // Load from network or database
        "Loaded data"
    }
    withContext(Dispatchers.Main) {
        // Use the data on the main thread
        println(data)
    }
}

Always update views only from Dispatchers.Main. Do heavy network or disk work only on Dispatchers.IO or Dispatchers.Default.

Suspending Functions

A suspend function looks like a regular function but can call other suspending operations such as delay, withContext, or custom suspend functions. When a coroutine reaches a suspension point, the function can pause and the thread is free to do other work. Later the coroutine resumes from the same line without blocking.

For example, delay suspends only the coroutine, not the thread.

suspend fun fetchUserName(): String {
    delay(1000) // Simulate network delay
    return "Alice"
}
fun useSuspendFunction(scope: CoroutineScope) {
    scope.launch {
        val name = fetchUserName()
        println("User name is $name")
    }
}

You cannot call a suspend function directly from normal code. You must call it inside another suspend function or inside a coroutine builder like launch or async.

Coroutines and Android Lifecycle

On Android coroutines must respect the lifecycle of activities, fragments, and view models. You do not want a coroutine to continue running after the user leaves the screen.

Lifecycle aware scopes solve this:

lifecycleScope is available inside activities and fragments. Any coroutine started in this scope is cancelled when the lifecycle is destroyed.

viewModelScope is available inside a ViewModel. Coroutines in this scope are cancelled when the ViewModel is cleared.

You use these scopes instead of manually creating and cancelling coroutine scopes inside your UI components.

class MyActivity : AppCompatActivity() {
    fun loadData() {
        lifecycleScope.launch {
            val data = withContext(Dispatchers.IO) {
                // Load from network or database
                "Data from server"
            }
            // This runs only while the Activity is alive
            // If the Activity is destroyed, this coroutine is cancelled
            println(data)
        }
    }
}

In a ViewModel you often do the background work and expose results to the UI through LiveData or other observable data holders. Coroutines in viewModelScope are a natural fit for this pattern.

Structured Concurrency and Scope Hierarchies

Structured concurrency means that coroutines have a parent child relationship based on their scope. A parent coroutine waits for all its children to complete before it completes, unless it is cancelled. If the parent fails or is cancelled, all child coroutines are also cancelled automatically.

This helps you avoid coroutines that run freely without control and makes it easier to reason about cancellation and errors.

For example, when you call launch or async inside another coroutine, they inherit that coroutine’s scope.

fun loadUserAndPosts(scope: CoroutineScope) {
    scope.launch {
        // Parent coroutine
        val userDeferred = async { fetchUser() }
        val postsDeferred = async { fetchPosts() }
        // Both child coroutines run in parallel
        val user = userDeferred.await()
        val posts = postsDeferred.await()
        // Parent continues after both children complete
        println("User: $user, Posts: $posts")
    }
}

If any of the child coroutines fails with an exception, the parent is cancelled and the other child is cancelled too. This avoids half completed operations.

Cancellation

Cancellation is cooperative in coroutines. This means a coroutine checks from time to time if it has been cancelled and stops its work in a controlled way. Many suspending functions in the standard library, such as delay, are cancellable. They throw a CancellationException if the coroutine is cancelled during the suspension.

You can cancel a coroutine through its Job. When you call launch, you get a Job back that you can store and later cancel.

val job = scope.launch {
    repeat(1000) { i ->
        println("Working $i")
        delay(100)
    }
}
// Later
job.cancel()

If you write long running suspend functions that do loops or heavy calculation without calling suspending functions, you should occasionally check coroutineContext.isActive or call ensureActive() to be responsive to cancellation.

import kotlinx.coroutines.isActive
import kotlinx.coroutines.ensureActive
suspend fun longCalculation() {
    for (i in 0 until 1_000_000) {
        coroutineContext.ensureActive()
        // Do some part of the calculation
    }
}

On Android, lifecycle aware scopes automatically cancel their coroutines when the component is destroyed. For example when an activity is finishing, lifecycleScope cancels all running coroutines. This prevents memory leaks and unnecessary work.

Error Handling with Coroutines

Errors in coroutines appear as exceptions. How these exceptions propagate depends on the builder you use and on the coroutine hierarchy.

In a simple launch coroutine, an uncaught exception is sent to the CoroutineExceptionHandler of that scope, which normally crashes the app if it is not handled.

In an async coroutine, the exception is stored and rethrown when you call await().

You can use try and catch inside a coroutine just as in normal code.

scope.launch {
    try {
        val result = withContext(Dispatchers.IO) {
            riskyNetworkCall()
        }
        println("Result: $result")
    } catch (e: IOException) {
        println("Network error: ${e.message}")
    }
}

If you want a group of child coroutines to behave independently so that failure in one child does not cancel the others, you can use a special scope such as supervisorScope. This is useful when you launch several parallel tasks and want to handle each failure separately. The detailed theory and patterns for this belong in architectural discussions, but you should know that supervisorScope exists for such cases.

Use try and catch inside coroutines to handle expected errors such as network failures. Do not let these errors crash your app on the main thread.

Coroutines with Callbacks and Legacy Code

Many Android APIs still use callbacks instead of suspending functions. To work with them in coroutines you either call these APIs from a background dispatcher or wrap them in suspend functions that provide a nicer interface.

A common pattern uses suspendCancellableCoroutine to convert a callback based API to a suspending one. This is more advanced and fits better with networking or database layers that you build around coroutines, but you should recognize that this pattern is how older APIs are adapted to coroutine style.

In many cases popular Android libraries already provide coroutine friendly wrappers. For example networking libraries offer suspend functions for HTTP requests, and Room can expose suspending DAO functions that run database queries on the correct dispatcher.

Practical Coroutines Pattern on Android

A typical flow for background tasks with coroutines on Android looks like this:

You start a coroutine in viewModelScope or lifecycleScope.

Inside that coroutine, you switch to Dispatchers.IO for heavy or blocking operations such as network calls or database queries using withContext.

You return to Dispatchers.Main to update the UI or LiveData with the result.

You catch any expected exceptions and show error messages, usually through the UI or logging.

All of this takes advantage of structured concurrency, lifecycle aware scopes, and dispatchers to keep your code simple and safe.

class MyViewModel : ViewModel() {
    fun refreshData() {
        viewModelScope.launch {
            try {
                val data = withContext(Dispatchers.IO) {
                    // Network or database call
                    "Fresh data"
                }
                // Update LiveData or state for the UI here
                println("Loaded: $data")
            } catch (e: Exception) {
                // Handle error
                println("Error: ${e.message}")
            }
        }
    }
}

By following this pattern you keep the UI responsive, keep long running work off the main thread, and tie background work to the lifecycle of your Android components using Kotlin coroutines.

Views: 1

Comments

Please login to add a comment.

Don't have an account? Register now!