Kotlin Coroutine Builders
Introduction
Coroutine builders are special functions in Kotlin that create and launch coroutines. They serve as an entry point from the regular (non-suspending) code into the suspending world of coroutines. Understanding these builders is crucial for effectively working with Kotlin's coroutines framework as they determine how and when coroutines are executed.
In this tutorial, we'll explore the main coroutine builders available in Kotlin:
launch
async
runBlocking
coroutineScope
withContext
Each builder has its specific use cases, return types, and behaviors, which we'll cover in detail.
Prerequisites
Before diving in, you should:
- Have basic knowledge of Kotlin syntax
- Understand the concept of suspending functions
- Have the
kotlinx.coroutines
dependency added to your project
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
The launch
Builder
The launch
coroutine builder starts a new coroutine without blocking the current thread. It returns a Job
object that can be used to manage the lifecycle of the coroutine.
Basic Syntax
val job = scope.launch {
// Coroutine code here
}
Key Features
- Fire-and-forget operation (doesn't return a result)
- Returns a
Job
object for lifecycle management - Executes asynchronously
- Exceptions are propagated to the parent coroutine scope
Example: Using launch
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
delay(1000L) // Non-blocking delay for 1 second
println("World!")
}
println("Hello, ")
job.join() // Wait for the coroutine to complete
println("Done!")
}
Output:
Hello,
World!
Done!
In this example:
- We launch a coroutine that prints "World!" after a 1-second delay
- Immediately print "Hello, "
- Wait for the launched coroutine to complete with
job.join()
- Print "Done!" after the coroutine completes
Controlling Coroutine Execution
You can control how your coroutine is executed by specifying a CoroutineDispatcher
:
launch(Dispatchers.IO) {
// This code runs on the IO dispatcher, optimized for I/O operations
val data = fetchDataFromNetwork()
println("Data fetched on thread: ${Thread.currentThread().name}")
}
The async
Builder
When you need to compute a value asynchronously, the async
builder is the right choice. It returns a Deferred<T>
object, which represents a future result.
Basic Syntax
val deferred = scope.async {
// Coroutine code that returns a result
calculateSomething()
}
// Later, get the result
val result = deferred.await()
Key Features
- Returns a
Deferred<T>
object that represents a future result - Use
await()
to get the result (suspending call) - Similar to JavaScript's Promises or Java's CompletableFuture
- Useful for concurrent computations
Example: Concurrent Calculation with async
import kotlinx.coroutines.*
fun main() = runBlocking {
val time = measureTimeMillis {
val firstDeferred = async { getFirstValue() }
val secondDeferred = async { getSecondValue() }
// Wait for both results and sum them
val result = firstDeferred.await() + secondDeferred.await()
println("Result: $result")
}
println("Completed in $time ms")
}
suspend fun getFirstValue(): Int {
delay(1000L) // Simulating work
return 10
}
suspend fun getSecondValue(): Int {
delay(1000L) // Simulating work
return 20
}
Output:
Result: 30
Completed in 1015 ms
In this example:
- We launch two coroutines concurrently using
async
- Each coroutine performs a calculation that takes around 1 second
- We wait for both results with
await()
and sum them - The total time is approximately 1 second instead of 2 seconds, demonstrating concurrency
Handling Exceptions with async
By default, exceptions in async
are deferred until you call await()
:
val deferred = async {
throw Exception("Error in async")
}
try {
deferred.await() // Exception will be thrown here
} catch (e: Exception) {
println("Caught exception: ${e.message}")
}
The runBlocking
Builder
The runBlocking
coroutine builder bridges the non-coroutine world with the coroutine world by blocking the current thread until the coroutine completes.
Basic Syntax
runBlocking {
// Coroutine code here
}
Key Features
- Blocks the current thread until the coroutine completes
- Mainly used as an entry point to coroutines from non-suspending code
- Great for tests and
main
functions, but rarely used in production code - Creates a coroutine scope
Example: Using runBlocking
import kotlinx.coroutines.*
fun main() {
println("Before runBlocking")
runBlocking {
println("Inside runBlocking: start")
delay(1000L)
println("Inside runBlocking: end")
}
println("After runBlocking")
}
Output:
Before runBlocking
Inside runBlocking: start
Inside runBlocking: end
After runBlocking
In this example:
- "Before runBlocking" is printed immediately
- The coroutine starts and prints "Inside runBlocking: start"
- The coroutine delays for 1 second (the current thread is blocked during this time)
- After the delay, "Inside runBlocking: end" is printed
- Finally, "After runBlocking" is printed
When to Use runBlocking
- In tests to ensure all coroutines complete before the test ends
- In
main
functions as the entry point to coroutine code - To adapt suspending functions to blocking APIs
⚠️ Warning: Avoid using runBlocking
in production code, especially on the main/UI thread, as it defeats the purpose of using coroutines for asynchronous programming.
The coroutineScope
Builder
The coroutineScope
builder creates a new coroutine scope and suspends until all launched children complete.
Basic Syntax
suspend fun someFunction() {
coroutineScope {
// Launch child coroutines here
}
// Execution continues only when all children complete
}
Key Features
- Creates a new coroutine scope
- Suspends the current coroutine until all children complete
- Doesn't block the thread (unlike
runBlocking
) - Propagates cancellation bidirectionally
- If any child fails, all other children are cancelled
Example: Using coroutineScope
import kotlinx.coroutines.*
suspend fun fetchUserData() = coroutineScope {
val userDeferred = async { fetchUser() }
val postsDeferred = async { fetchUserPosts() }
// Create user data only when both fetches complete
UserData(userDeferred.await(), postsDeferred.await())
}
suspend fun fetchUser(): User {
delay(1000L)
return User("John Doe", 25)
}
suspend fun fetchUserPosts(): List<Post> {
delay(1500L)
return listOf(Post("Hello World"), Post("Kotlin Coroutines"))
}
data class User(val name: String, val age: Int)
data class Post(val title: String)
data class UserData(val user: User, val posts: List<Post>)
fun main() = runBlocking {
val userData = fetchUserData()
println("User: ${userData.user.name}, Posts: ${userData.posts.size}")
}
Output:
User: John Doe, Posts: 2
In this example:
- The
fetchUserData
function creates acoroutineScope
- Within that scope, we asynchronously fetch the user and their posts
- The function only returns when both fetches complete
- If either fetch fails with an exception, the other is automatically cancelled
The withContext
Builder
The withContext
builder is used to switch the context of a coroutine, typically to change the dispatcher.
Basic Syntax
withContext(Dispatchers.IO) {
// Code in this block executes on IO dispatcher
}
Key Features
- Changes the context of the current coroutine
- Returns the result of the block
- Suspends the current coroutine until the block completes
- Useful for changing dispatchers within a coroutine
Example: Using withContext
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Start on thread: ${Thread.currentThread().name}")
val result = withContext(Dispatchers.Default) {
println("Computing on thread: ${Thread.currentThread().name}")
// Simulate CPU-intensive calculation
var sum = 0
for (i in 1..1_000_000) {
sum += i
}
sum
}
println("Result: $result")
println("Back to thread: ${Thread.currentThread().name}")
}
Output:
Start on thread: main
Computing on thread: DefaultDispatcher-worker-1
Result: 500000500000
Back to thread: main
In this example:
- We start in the
main
thread - Use
withContext
to switch to theDefault
dispatcher for CPU-intensive work - Perform a calculation
- Return to the original thread with the result
Practical Example: Network Request Pattern
A common pattern for network requests in Android:
suspend fun fetchDocument(documentId: String): Document {
return withContext(Dispatchers.IO) {
// Network request happens on IO thread
api.getDocument(documentId)
}
// Back to the original context after this line
}
Choosing the Right Builder
Here's a quick guide for choosing the appropriate builder:
launch
- When you want to "fire and forget" (no return value needed)async
- When you need a result from the coroutinerunBlocking
- When you need to call suspending functions from non-suspending codecoroutineScope
- When you need to create a scope that waits for all child coroutineswithContext
- When you need to switch context/dispatcher within a coroutine
Real-World Example: Weather App
Let's see a more complex example that combines multiple builders in a simulated weather app:
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
// Simulated API service
class WeatherService {
suspend fun getCurrentWeather(cityId: String): Weather {
delay(1000L) // Simulate network delay
return Weather(cityId, "Sunny", 25)
}
suspend fun getForecast(cityId: String): List<Weather> {
delay(1500L) // Simulate network delay
return listOf(
Weather(cityId, "Cloudy", 20),
Weather(cityId, "Rainy", 15)
)
}
}
data class Weather(val cityId: String, val condition: String, val temperature: Int)
data class WeatherData(val current: Weather, val forecast: List<Weather>)
class WeatherRepository(private val service: WeatherService) {
// Use coroutineScope to get current weather and forecast concurrently
suspend fun getWeatherData(cityId: String): WeatherData = coroutineScope {
val currentDeferred = async { service.getCurrentWeather(cityId) }
val forecastDeferred = async { service.getForecast(cityId) }
WeatherData(currentDeferred.await(), forecastDeferred.await())
}
// Simulate data processing on a background thread
suspend fun processWeatherData(data: WeatherData): String {
return withContext(Dispatchers.Default) {
delay(500L) // Simulate processing
"Processed: ${data.current.condition}, ${data.forecast.size} forecast items"
}
}
}
fun main() = runBlocking {
val weatherService = WeatherService()
val weatherRepository = WeatherRepository(weatherService)
println("Fetching weather data...")
val time = measureTimeMillis {
val weatherData = weatherRepository.getWeatherData("NYC")
println("Current: ${weatherData.current.condition}, ${weatherData.current.temperature}°C")
println("Forecast items: ${weatherData.forecast.size}")
val processed = weatherRepository.processWeatherData(weatherData)
println(processed)
}
println("Completed in $time ms")
}
Output:
Fetching weather data...
Current: Sunny, 25°C
Forecast items: 2
Processed: Sunny, 2 forecast items
Completed in 2010 ms
In this example:
- We use
runBlocking
as the entry point to our coroutine code - Inside
getWeatherData
, we usecoroutineScope
with twoasync
calls to fetch data concurrently - We use
withContext
to process the data on a background thread - The total time is approximately 2 seconds, which is the sum of the longest API call (1.5s) and the processing time (0.5s), instead of 3 seconds if we ran everything sequentially
Summary
Coroutine builders are the gateway to Kotlin's coroutine system, allowing you to launch and manage coroutines effectively. Let's recap the main builders:
launch
- Starts a new coroutine without returning a resultasync
- Starts a new coroutine and returns aDeferred<T>
with the resultrunBlocking
- Blocks the current thread until all coroutines completecoroutineScope
- Creates a new scope and suspends until all children completewithContext
- Changes the context of a coroutine, typically to switch dispatchers
Each builder serves a specific purpose, and choosing the right one depends on your specific needs. Understanding these builders is fundamental to mastering Kotlin coroutines and writing efficient concurrent code.
Additional Resources
Exercises
- Create a program that uses
async
to fetch data from two different simulated API endpoints concurrently and combines the results. - Implement a function that downloads multiple files concurrently using
launch
and reports progress. - Write a program that demonstrates how exceptions are propagated in different coroutine builders.
- Create a function that uses
withContext
to perform different operations on different dispatchers (IO, Default, Main). - Implement a retry mechanism using coroutines that attempts to fetch data up to 3 times with delays between retries.
Happy coding with Kotlin Coroutines!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)