How To Use Kotlin Coroutines
A Non-Blocking Example
This example will simulate fetching data from a remote service in the background and then updating the UI (or in this case, printing to the console, as we’re working within a text-based example) without blocking the main thread.
For this example, I’ll include a non-blocking suspending function that simulates fetching data from a remote service with a delay (to mimic network latency) and then continues with updating the UI on the main thread after the data is fetched.
import kotlinx.coroutines.*
fun main() {
// Create a CoroutineScope that runs on the main thread
val mainScope = CoroutineScope(Dispatchers.Main)
mainScope.launch {
println("Fetching data from the network...")
val data = fetchDataFromNetwork() // This will not block the main thread
println("Data fetched: $data")
// Update UI or process data here (still on the main thread)
println("Updating UI with the fetched data.")
}
// Assume Dispatchers.Main is simulated for the purpose of this example.
// In a real application, this would typically be our Android Activity or Fragment.
println("This will print while data is being fetched.")
Thread.sleep(3000) // Simulating some other work
}
// Simulate a network call
suspend fun fetchDataFromNetwork(): String {
withContext(Dispatchers.IO) { // Switch to a background thread
delay(1000) // Simulate network delay
return@withContext "Sample Data" // Return data
}
}
-
Non-Blocking Network Call: The
fetchDataFromNetwork
suspending function simulates a network call on a background thread usingwithContext(Dispatchers.IO)
. That way we can make sure the main thread isn’t blocked by the network call. -
Main-Safe: After fetching the data, the coroutine continues execution on the
Dispatchers.Main
context, allowing safe updates to the UI. -
Structured Concurrency: The coroutine is launched within a
CoroutineScope
tied to the main thread (Dispatchers.Main
). -
Simultaneous Execution: The
println
statement after launching the coroutine shows that the program continues execution without waiting for the coroutine to complete.
Use in MVI
In an MVI project I was working on, I used the following function to load a JSON file and parse it into a Data Model.
private fun loadLootTable() {
viewModelScope.launch {
lootTable = repository.getLootTable()
}
}
By using a coroutine to load the file, I make sure that my UI remains responsive while the data is being loaded.
Here, we use a different scope than the main dispatcher, viewModelScope
. This is a CoroutineScope tied to the ViewModel lifecycle. Using this scope lets me make sure that the coroutine is canceled when the ViewModel is cleared, preventing any potential memory leaks or unnecessary work if the ViewModel is no longer in use.
A Blocking Example
Imagine we have a simple user interface with a text view or label that we want to update with data fetched from a network. We’ll simulate the network request with a suspending function that delays for a short period to mimic the latency of a real network call.
import kotlinx.coroutines.*
import kotlin.random.Random
// Simulate a network request that fetches user data
suspend fun fetchUserData(): String {
delay(2000) // Simulate network delay
return "User ${Random.nextInt(1000)}" // Return a mock user name
}
fun main() = runBlocking {
val job = launch(Dispatchers.Main) { // Launch a coroutine in the UI thread
val userData = fetchUserData() // Fetch user data (this suspends the coroutine)
println("Fetched user data: $userData") // Update UI with fetched data
}
job.join() // Wait for the coroutine to finish
}
In this example:
- We define a
suspend
functionfetchUserData
that mimics fetching data from a network. It usesdelay
to simulate network latency and then returns a mock user name. - In the
main
function, which acts as our entry point, we userunBlocking
to create a coroutine scope.runBlocking
is usually used in main functions and tests to wait for a coroutine to complete. - Inside
runBlocking
, we use thelaunch
coroutine builder to start a new coroutine. We specifyDispatchers.Main
as the coroutine context. Note: In real applications,Dispatchers.Main
is available in environments with a UI main thread, such as Android or JavaFX applications. In a console application, we’d useDispatchers.Default
or another appropriate dispatcher. - The coroutine calls
fetchUserData
and suspends until the data is fetched without blocking the main thread. After receiving the data, it prints the fetched user data to simulate updating the UI. - Finally,
job.join()
waits for the coroutine to finish its execution. In a real application, especially in GUI applications like Android, we wouldn’t typically join the coroutine like this, as the coroutine lifecycle would be managed by the application’s architecture components (e.g., a ViewModel).
join()
is useful when we need to wait for a task to complete before proceeding. This is common in structured concurrency where the outcome of a coroutine impacts subsequent actions.
In UI applications, direct use of join()
is less common because it can lead to blocking the main thread, especially if not used carefully with appropriate dispatchers or within a non-blocking context.
Suspend
The suspend
keyword in Kotlin allows asynchronous and non-blocking programming. It marks a function as a suspending function, which means that the function can suspend the execution of a coroutine without blocking the thread on which it’s running. This capability is foundational for writing asynchronous code in Kotlin that’s both efficient and easy to manage.
Characteristics of Suspended Functions:
-
Non-Blocking: When a suspending function is called, it doesn’t block the thread it’s running on. Instead, it suspends the coroutine in which it’s called. This is crucial for performing long-running operations, such as network requests or database transactions, without freezing the application’s user interface.
-
Can Be Called from Coroutines or Other Suspending Functions: A suspending function can only be called from within another suspending function or a coroutine builder block (like
launch
orasync
). -
Cooperative: Suspending functions are cooperative. They suspend the execution at specific points, which are defined by either calling other suspending functions or explicitly suspending the coroutine using functions like
delay()
.
How It Works:
Under the hood, Kotlin transforms suspending functions into regular functions that take an additional parameter: a continuation. Continuations are part of Kotlin’s coroutines library, and they manage the state of the coroutine, including its suspension and resumption. This transformation is handled by the Kotlin compiler, which allows developers to write asynchronous code that looks synchronous, improving readability and maintainability.
Example:
Here’s a simple example of a suspending function that delays for one second:
suspend fun doSomething() {
delay(1000) // Suspends the coroutine for 1 second without blocking the thread
println("Done!")
}
This function can be called from within a coroutine or another suspending function like so:
fun main() = runBlocking { // Creates a coroutine scope
launch { // Launches a new coroutine
doSomething() // Calls the suspending function
}
}
Specifying a Coroutine Context or Dispatcher
Main vs Background Thread:
- Use
Dispatchers.Main
when we need to update the UI after performing a task. This is crucial in Android development, where UI changes must be made on the main thread. - Use
Dispatchers.IO
for I/O operations like reading from or writing to files, databases, or network calls. - Use
Dispatchers.Default
for CPU-intensive tasks that can be run concurrently.
Environment:
- In non-Android applications (like server-side or Kotlin/JVM applications),
Dispatchers.Main
might not be relevant, and we’d default toDispatchers.Default
orDispatchers.IO
based on the task type. - In Android,
Dispatchers.Main
is often used withinviewModelScope.launch
for tasks that result in UI updates.