Skip to main content

Kotlin Coroutines Basics

Introduction

Kotlin Coroutines offer a powerful solution for handling asynchronous operations in a sequential, straightforward manner. Unlike traditional threading mechanisms, coroutines provide a lightweight, more efficient approach to managing concurrent tasks. They essentially allow you to write asynchronous code as if it were synchronous, making your code more readable and maintainable.

In this tutorial, we'll explore the fundamental concepts of Kotlin Coroutines, learn how to set them up in your project, and see examples of how they can simplify asynchronous programming.

What Are Coroutines?

Coroutines can be thought of as light-weight threads. However, unlike threads:

  • Coroutines are not tied to any particular thread
  • Multiple coroutines can run on the same thread
  • Coroutines can suspend their execution without blocking the thread
  • They consume far less memory than traditional threads

This makes coroutines an excellent choice for handling operations like network calls, database operations, or any task that might cause your application to wait.

Setting Up Coroutines

Before you can use coroutines, you need to add the required dependencies to your project:

kotlin
// In your build.gradle (app level)
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" // If using Android
}

Creating Your First Coroutine

Let's start with a simple example of launching a coroutine:

kotlin
import kotlinx.coroutines.*

fun main() {
// Creating a coroutine in the Global scope
GlobalScope.launch {
// This code runs in a coroutine
delay(1000L) // Non-blocking delay for 1 second
println("World!")
}

// This code runs immediately after the coroutine is launched
println("Hello,")

// We need to keep the main thread alive until the coroutine completes
Thread.sleep(2000L) // Block the main thread for 2 seconds
}

Output:

Hello,
World!

Let's break down what's happening here:

  1. We launch a coroutine using GlobalScope.launch
  2. Inside the coroutine, we delay for 1 second using delay(1000L)
  3. After launching the coroutine, the main function continues execution and prints "Hello,"
  4. We use Thread.sleep(2000L) to keep the main thread alive long enough for our coroutine to complete

Note that delay() is a suspending function, which means it can pause the coroutine without blocking the thread.

Coroutine Builders

Kotlin provides several ways to start a coroutine:

1. launch

The launch builder starts a new coroutine without blocking the current thread and returns a Job reference that can be used to cancel the coroutine:

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
// Launch a coroutine in the current scope
val job = launch {
delay(1000L)
println("Task completed!")
}

println("Waiting for the task...")
job.join() // Waits for the coroutine to complete
println("Main function continues")
}

Output:

Waiting for the task...
Task completed!
Main function continues

2. runBlocking

The runBlocking builder blocks the current thread until all coroutines inside its block complete:

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
// This block blocks the main thread until all coroutines complete
println("Start")

launch {
delay(1000L)
println("Task 1 completed")
}

launch {
delay(800L)
println("Task 2 completed")
}

println("End of runBlocking")
}

Output:

Start
End of runBlocking
Task 2 completed
Task 1 completed

3. async

The async builder is similar to launch but returns a Deferred<T> which can provide a result:

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
println("Calculating values...")

val valueA = async {
delay(1000L)
10
}

val valueB = async {
delay(1000L)
20
}

// await() gets the result when it's ready
val sum = valueA.await() + valueB.await()
println("The sum is: $sum")
}

Output:

Calculating values...
The sum is: 30

Suspending Functions

A suspending function is a function that can be paused and resumed later. These functions are marked with the suspend keyword:

kotlin
import kotlinx.coroutines.*

suspend fun fetchData(): String {
delay(1000L) // Simulating network delay
return "Data fetched successfully"
}

fun main() = runBlocking {
println("Fetching data...")
val data = fetchData() // This call suspends the coroutine, not the thread
println(data)
}

Output:

Fetching data...
Data fetched successfully

Important characteristics of suspending functions:

  • They can only be called from other suspending functions or from within a coroutine
  • They can suspend execution without blocking the thread
  • When a suspending function is called, it doesn't necessarily suspend immediately

Coroutine Context and Dispatchers

The coroutine context is a set of elements that define the behavior of a coroutine. One of the most important elements is the dispatcher, which determines what thread or threads the coroutine will use.

Kotlin provides several dispatchers:

1. Dispatchers.Default

Optimized for CPU-intensive tasks:

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
launch(Dispatchers.Default) {
// CPU-intensive work
val result = (1..1_000_000).sum()
println("Sum calculated: $result")
}

println("Calculation started")
}

2. Dispatchers.IO

Optimized for I/O-intensive tasks:

kotlin
import kotlinx.coroutines.*
import java.io.File

fun main() = runBlocking {
launch(Dispatchers.IO) {
// I/O operations like file reading/writing or network calls
val content = File("example.txt").readText()
println("File content length: ${content.length}")
}

println("File reading started")
}

3. Dispatchers.Main

Used for UI-related operations (primarily in Android):

kotlin
import kotlinx.coroutines.*

// In an Android context
fun updateUI() = runBlocking {
// Background work
val data = withContext(Dispatchers.IO) {
fetchDataFromNetwork()
}

// UI update
withContext(Dispatchers.Main) {
// Update UI with data
textView.text = data
}
}

suspend fun fetchDataFromNetwork(): String {
delay(1000L)
return "Network data"
}

Coroutine Scopes

Coroutine scopes help manage the lifecycle of coroutines:

1. GlobalScope

Coroutines launched in GlobalScope live as long as the application:

kotlin
import kotlinx.coroutines.*

fun main() {
GlobalScope.launch {
delay(1000L)
println("GlobalScope coroutine executed")
}

// Keep the main thread alive
Thread.sleep(2000L)
}

⚠️ Warning: Using GlobalScope is generally not recommended as coroutines launched this way can live indefinitely.

2. CoroutineScope

You can create your own scope to better manage coroutines:

kotlin
import kotlinx.coroutines.*

fun main() {
// Create a new scope
val scope = CoroutineScope(Dispatchers.Default)

// Launch coroutines in this scope
scope.launch {
delay(1000L)
println("Task executed in custom scope")
}

Thread.sleep(1500L)

// Cancel all coroutines in this scope
scope.cancel()
println("Scope cancelled")

Thread.sleep(1000L)
}

Real-World Example: Fetching Data from an API

Let's put these concepts together in a practical example that simulates fetching data from an API:

kotlin
import kotlinx.coroutines.*
import kotlin.random.Random

// Simulates an API service class
class UserService {
// Simulated network call
suspend fun fetchUser(userId: Int): User {
delay(1000L) // Simulate network delay
return User(userId, "User $userId", Random.nextInt(18, 70))
}
}

data class User(val id: Int, val name: String, val age: Int)

// ViewModel-like class
class UserViewModel {
private val userService = UserService()
private val viewModelScope = CoroutineScope(Dispatchers.Main + Job())

fun loadUserData(userId: Int, onComplete: (User) -> Unit) {
viewModelScope.launch {
try {
// Perform network call on IO dispatcher
val user = withContext(Dispatchers.IO) {
userService.fetchUser(userId)
}

// Switch back to Main dispatcher to update UI
onComplete(user)
} catch (e: Exception) {
println("Error fetching user: ${e.message}")
}
}
}

fun onDestroy() {
viewModelScope.cancel()
}
}

fun main() = runBlocking {
val viewModel = UserViewModel()

println("Loading user data...")
viewModel.loadUserData(42) { user ->
println("User loaded: $user")
}

delay(2000L) // Wait for the operation to complete
viewModel.onDestroy()
}

Output:

Loading user data...
User loaded: User(id=42, name=User 42, age=35)

In this example:

  1. We define a UserService with a suspending function that simulates an API call
  2. Our UserViewModel creates a coroutine scope that combines the Main dispatcher with a Job
  3. When loadUserData is called, we launch a coroutine in our viewModelScope
  4. We use withContext(Dispatchers.IO) to perform the network operation on an IO-optimized thread
  5. After getting the result, we call the onComplete callback (which in a real app would update the UI)
  6. We properly clean up by cancelling the scope when it's no longer needed

Error Handling in Coroutines

Handling exceptions in coroutines follows some specific patterns:

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
// Method 1: try-catch block
launch {
try {
println("Starting risky operation...")
throw RuntimeException("Something went wrong!")
} catch (e: Exception) {
println("Caught exception: ${e.message}")
}
}

// Method 2: Using exception handler
val handler = CoroutineExceptionHandler { _, exception ->
println("Handled in CoroutineExceptionHandler: ${exception.message}")
}

val job = launch(handler) {
throw ArithmeticException("Division by zero!")
}

job.join()
}

Output:

Starting risky operation...
Caught exception: Something went wrong!
Handled in CoroutineExceptionHandler: Division by zero!

Summary

We've covered the foundational aspects of Kotlin Coroutines:

  • Coroutine Basics: Light-weight threads that can suspend execution without blocking
  • Coroutine Builders: launch, runBlocking, and async to start coroutines
  • Suspending Functions: Functions that can pause and resume
  • Contexts and Dispatchers: How to control which threads your coroutines run on
  • Coroutine Scopes: How to manage coroutine lifecycles
  • Error Handling: How to handle exceptions in coroutines

Coroutines provide a powerful, clean approach to asynchronous programming in Kotlin. By understanding these fundamentals, you're now equipped to write more efficient, readable, and maintainable asynchronous code.

Additional Resources and Exercises

Resources

Practice Exercises

  1. Basic Exercise: Write a program that launches two coroutines in parallel, each printing numbers from 1 to 5 with different delays.

  2. Intermediate Exercise: Create a function that simulates downloading multiple files concurrently using coroutines and reports progress as each download completes.

  3. Advanced Exercise: Implement a simple weather app that fetches data from multiple endpoints (current weather, forecast, historical data) concurrently using coroutines, then combines the results.

  4. Challenge: Create a coroutine-based implementation of a rate limiter that allows only a certain number of operations to be performed within a given time period.

Happy coding with coroutines!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)