Kotlin Coroutines

Kotlin Coroutines are a way to handle asynchronous programming in a simpler, more readable way. They let you write code that runs in the background (like fetching data from the internet) without blocking the main thread, making your app smooth and responsive. Think of coroutines as lightweight threads that are easier to manage.

Let’s break down the basics with simple explanations and examples.


1. What Are Coroutines?

  • Coroutines allow you to write asynchronous code that looks like synchronous code (sequential and easy to read).

  • They help with tasks like:

    • Fetching data from a server.

    • Reading/writing to a database.

    • Performing heavy computations without freezing the app.

  • Unlike threads, coroutines are cheaper and managed by Kotlin, so you don’t have to worry about low-level thread management.


2. Key Concepts

Here are the main building blocks of coroutines:

  • CoroutineScope: Defines the scope (or lifetime) of a coroutine. Every coroutine runs inside a scope.

  • launch: Starts a coroutine that runs in the background and doesn’t return a result.

  • async: Starts a coroutine that can return a result (useful when you need data back).

  • suspend: A keyword used for functions that can pause and resume later without blocking the thread.

  • Dispatchers: Tell coroutines which thread to run on:

    • Dispatchers.Main: For UI-related tasks (e.g., updating the screen).

    • Dispatchers.IO: For network or file operations.

    • Dispatchers.Default: For CPU-intensive tasks (e.g., calculations).


3. Setting Up Coroutines

To use coroutines, add this dependency to your build.gradle (Android or Kotlin project):

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0"

For Android, also add:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0"

4. Basic Examples

Let’s walk through simple examples to understand how coroutines work.

Example 1: Using launch to Run a Background Task

This example shows how to run a task in the background and update the UI.

import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext

fun main() = runBlocking { // Creates a scope for coroutines
    println("Main starts")

    // Launch a coroutine in the background
    launch(Dispatchers.Default) {
        delay(1000) // Simulates a long task (1 second wait)
        println("Background task done")
    }

    println("Main continues")
}

Output:

Main starts
Main continues
Background task done

Explanation:

  • runBlocking creates a scope for coroutines in a console app (use it for testing).

  • launch starts a coroutine in the Dispatchers.Default context (good for CPU tasks).

  • delay is a suspend function that pauses the coroutine for 1 second without blocking the thread.

  • The main program continues while the coroutine runs in the background.


Example 2: Using async to Get a Result

This example shows how to use async to compute something and get the result.

import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Starting calculation")

    // Start an async coroutine
    val deferredResult = async(Dispatchers.Default) {
        delay(1000) // Simulate work
        42 // Return a result
    }

    // Wait for the result
    val result = deferredResult.await()
    println("Result is $result")
}

Output:

Starting calculation
Result is 42

Explanation:

  • async starts a coroutine that returns a Deferred object (like a promise for a result).

  • await() waits for the result without blocking the thread.

  • The coroutine computes 42 after a 1-second delay and returns it.


Example 3: Using suspend Functions

Suspend functions are the heart of coroutines. They can pause and resume without blocking.

import kotlinx.coroutines.*

// A suspend function
suspend fun fetchData(): String {
    delay(1000) // Simulate network call
    return "Data from server"
}

fun main() = runBlocking {
    println("Fetching data...")
    val data = fetchData() // Call suspend function
    println("Got: $data")
}

Output:

Fetching data...
Got: Data from server

Explanation:

  • suspend fun fetchData() is a function that can pause (e.g., during delay).

  • It can only be called from a coroutine or another suspend function.

  • The main program waits for fetchData to complete before printing the result.


Example 4: Switching Dispatchers (Android Example)

This is a common Android use case: fetch data in the background and update the UI.

import kotlinx.coroutines.*
import android.widget.TextView

fun updateUI(textView: TextView) {
    // Create a scope for coroutines
    val scope = CoroutineScope(Dispatchers.Main)

    scope.launch {
        // Start on Main thread (UI)
        textView.text = "Fetching data..."

        // Switch to IO for network call
        val data = withContext(Dispatchers.IO) {
            delay(1000) // Simulate network
            "Hello from server!"
        }

        // Back to Main to update UI
        textView.text = data
    }
}

Explanation:

  • CoroutineScope(Dispatchers.Main) ensures the coroutine starts on the main thread.

  • withContext(Dispatchers.IO) switches to the IO thread for the network task.

  • After getting the data, it switches back to the main thread to update the TextView.


5. Canceling Coroutines

You can cancel a coroutine if you no longer need it (e.g., when a user closes a screen).

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch(Dispatchers.Default) {
        repeat(5) { i ->
            println("Task $i")
            delay(500)
        }
    }

    delay(1200) // Let it run for a bit
    job.cancel() // Cancel the coroutine
    println("Coroutine canceled")
}

Output:

Task 0
Task 1
Task 2
Coroutine canceled

Explanation:

  • launch returns a Job object that represents the coroutine.

  • job.cancel() stops the coroutine before it completes.

  • This is useful for cleaning up tasks (e.g., stopping network calls when a user navigates away).


6. Best Practices

  • Always use a scope: Never launch coroutines without a CoroutineScope. Use runBlocking for tests, viewModelScope for ViewModels (Android), or custom scopes.

  • Handle exceptions: Use try-catch or a CoroutineExceptionHandler to handle errors.

  • Cancel when done: Cancel coroutines when they’re no longer needed (e.g., when an Android Activity is destroyed).

  • Choose the right dispatcher: Use Dispatchers.Main for UI, Dispatchers.IO for network/file, and Dispatchers.Default for computations.


7. Common Use Case: Android ViewModel

Here’s how you’d use coroutines in an Android ViewModel to fetch data.

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MyViewModel : ViewModel() {
    fun fetchData(onResult: (String) -> Unit) {
        viewModelScope.launch {
            val result = withContext(Dispatchers.IO) {
                // Simulate network call
                Thread.sleep(1000) // Use delay in real coroutines
                "Data from server"
            }
            onResult(result) // Update UI
        }
    }
}

Explanation:

  • viewModelScope is a built-in scope for ViewModels that cancels coroutines when the ViewModel is cleared.

  • withContext(Dispatchers.IO) runs the network task in the background.

  • The result is passed to onResult to update the UI.


8. Key Points to Remember

  • Coroutines make asynchronous code look synchronous, which is easier to read.

  • Use launch for fire-and-forget tasks, async for tasks that return results.

  • suspend functions are non-blocking and can only be called from coroutines.

  • Always manage coroutine lifecycles (scopes and cancellation) to avoid memory leaks.


9. Practice Exercise

Try this:

  1. Write a program that:

    • Launches two coroutines: one to "fetch user data" (returns "User: Alice" after 1 second) and another to "fetch settings" (returns "Settings: Dark Mode" after 2 seconds).

    • Prints both results when they’re ready.

  2. Use async and await to get the results.

  3. Cancel the "settings" coroutine after 1.5 seconds.

Solution:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val userDeferred = async {
        delay(1000)
        "User: Alice"
    }

    val settingsJob = async {
        delay(2000)
        "Settings: Dark Mode"
    }

    delay(1500) // Cancel settings after 1.5s
    settingsJob.cancel()

    try {
        println(userDeferred.await())
        println(settingsJob.await())
    } catch (e: CancellationException) {
        println("Settings fetch canceled")
    }
}

Output:

User: Alice
Settings fetch canceled

This covers the basics of Kotlin Coroutines! Practice these examples, and let me know if you want to dive deeper into specific topics like structured concurrency, flow, or Android-specific use cases.

0
Subscribe to my newsletter

Read articles from Singaraju Saiteja directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Singaraju Saiteja
Singaraju Saiteja

I am an aspiring mobile developer, with current skill being in flutter.