KOTLIN COROUTINES
Coroutines are a features in kotlin programming languages allows to write asynchronous code in a sequential and more readable manner. They were introduced as an experimental feature in kotlin 1.1 and it became stable later in kotlin 1.3. Coroutines work by suspending the execution of a function at a certain point and resuming it later. This will allows to write code that can run concurrently with other code, without blocking the main thread.
Before going to the deep in coroutine let’s see about what is asynchronous and synchronous.
Asynchronous
Asynchronous refers where the task might take some time to complete (like fetching data from internet). The program does not pause and wait for the task to complete. Instead it continue to execute other task or handling events. This helps us to keep everything moving smoothly and efficiently.
Synchronous
On the other hand, the synchronous refers where the task are executed one after another in a sequential manner and the program waits for the task to complete before moving on to the next task. If a task takes a longer time to complete (such as reading a large file) the program will be unresponsive until the task is finished.
Suspend function
A suspend function is a type of function that can be paused and resumed without blocking the current thread. This feature is part of Kotlin’s coroutine framework, which enables you to write asynchronous code in a more sequential and readable manner.
suspend fun doSomething() {
// Do something useful here
}
What is Coroutines?
Coroutines are always useful for writing asynchronous task more efficiently and avoid nested callbacks which makes our code more readable and easy to maintain. To declare a coroutine in android you must first include AndroidX
that support library in your project and add the dependency in your build.gradle
file.
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:$latest_dependency '
}
Then in your class, you have to import CoroutineScope class and create an instance to this class. To create a coroutine, you must use the launch
or aync
method of CoroutineScope. The launch
method starts a new coroutine that doesn't return a result, while the async
method also used to create a new coroutine but it returns a result.
Example for launch method
class MyViewModel : ViewModel() {
fun fetchDataFromApi() {
viewModelScope.launch {
// This code runs in a background thread
}
}
}
Example for async method
class MyViewModel : ViewModel() {
fun fetchDataFromApi() : Deferred<Data>{
return viewModelScope.async {
// Get the data and returns the result
}
}
}
As we can see above in both examples, an instance of CoroutineScope
is created and the launch
or async
method is used to create the coroutine. Both the function takes lambda code block as an argument that contains logic of the coroutine. We will see more details about the launch
and async
.
What is CoroutineScope?
The CoroutineScope
defines the lifetime of coroutines. In android you often use ViewModel and the lifecycleScope provides the android.lifecycle.lifecycle-runtime.ktx
” library to manage the lifecycle of an android component (like an activity or fragment).
Types of coroutineScope:
There are several types of CoroutineScope that we can create coroutines in Android they are:
a) GlobalScope
b) viewModelScope
c) lifecycleScope
a) GlobalScope:
The globalScope is not tied into any specific lifecycle and it would exist throughout the entire application. GlobalScope
are not automatically canceled when an android component (Activity or Fragment ) lifecycle ends, so it can leads to memory leaks if it is not managed properly.
GlobalScope.launch {
// Coroutine code
println("Running in GlobalScope")
}
b) viewModelScope:
The viewModelScope
is an extension property provided by the androidx.lifecycle:lifecycle-viewmodel-ktx
library in Kotlin for Android. It is used to create coroutines associated with the ViewModel and automatically cancels all its coroutines when the ViewModel is cleared. It is useful for performing asynchronous tasks that are related to the ViewModel and its lifecycle, allowing you to write asynchronous code that is tightly integrated with the Android architecture components and doesn't depend on the lifetime of a particular Activity or Fragment.
class MyViewModel : ViewModel() {
fun performTask() {
viewModelScope.launch {
// Coroutine code
println("Running in ViewModelScope")
}
}
}
c) lifecycleScope:
It is a lifecycle Owner (such as Activity or Fragment) that can be used to create coroutines and it is tied to the lifecycle of a LifecycleOwner
. Any coroutine launched in this scope will be canceled when the LifecycleOwner
is destroyed.
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Get the lifecycle scope for this activity
val lifecycleScope = lifecycleScope
// Launch a coroutine that fetches data from the network
lifecycleScope.launch {
val data = fetchDataFromNetwork()
// Do something with the data
}
}
}
Dispatchers
It will determine which thread the coroutine will be executed. Dispatchers provides a way to switch between different thread without directly managing the thread manually. In kotlin’s it provides several built-in dispatchers that you can use to control the execution context of coroutines. some of the commonly used dispatchers include :
- Dispatchers.Main
It is used for UI-related work on the main (UI) thread and it is useful for updating the UI from a coroutine.
2. Dispatchers.IO
It is used to performing I/O related task like network request or file operation.
3. Dispatchers.Default
It is a shared dispatcher that uses a pool of threads. When you launch
a coroutine on Dispatchers.Default
, the coroutine will be assigned to one of the threads in the pool, this will be choosen automatically by the kotlin runtime. If the kotlin runtime cannot find a free thread, the coroutine will be placed in the queue and will wait for a thread to become available. It’s a good idea to choose Dispatchers.Default
for CPU-intensive operations.
4. Dispatchers.Unconfined
It allows coroutines to run on any thread. It is often used for testing and debugging.
Coroutine Builder
Coroutine Builders are functions that help create and start coroutines. They can be called from regular functions, but many of them are suspending functions that allow for asynchronous and non-blocking execution within coroutines. Some of the commonly used Coroutine Builder are:
- launch
This builders starts a new coroutine that is fire and forget
and does not return anything
fun main() {
launch {
// Do something in a coroutine
}
}
2. async
You use async
to start a new coroutine that performs some asynchronous computation or task. The async
builder immediately returns a Deferred
object, which represents the result of that computation. You can think of it as a promise to hold the result. You can continue with other tasks or coroutines while the original coroutine started by async
is running in the background. When you need the result of the computation, you can use the await()
function on the Deferred
object. This function suspends the current coroutine until the result is available, but it does not block the calling thread.
Using async
and Deferred
allows you to perform concurrent asynchronous operations and retrieve their results in a non-blocking manner, making it a powerful tool for writing efficient and responsive asynchronous code.
val deferred = async {
// Simulate some work in a coroutine and return a value
delay(1000) // Simulate work (e.g., network request or computation)
"Hello, World!"
}
// Do something else while the async task is running
println("Do something else while waiting...")
// When you need the result, use await() to retrieve it.
val value = deferred.await()
println("Result 1: $value")
3. runBlocking
This builders blocks the current thread until the coroutine completes.
runBlocking {
// Code inside this block runs in a coroutine
println("Coroutine is running...")
delay(1000) // Simulate some work (e.g., a delay)
println("Coroutine has completed.")
}
// Code here runs after the coroutine has completed
println("Main function continues...")
Exception Handling in Coroutines
In Kotlin coroutines, you can handle exceptions using several mechanisms to ensure that your code remains robust and can recover from errors. Here are some of the key exception handling mechanisms in Kotlin coroutines:
- try-catch Blocks:
You can use traditional try-catch blocks to catch exceptions that occur within a coroutine. Here’s an example:
import kotlinx.coroutines.*
fun main() = runBlocking {
try {
val result = withContext(Dispatchers.IO) {
// Perform some potentially throwing operation
throw IllegalStateException("An error occurred")
}
println("Result: $result")
} catch (e: Exception) {
println("Caught an exception: ${e.message}")
}
}
2. CoroutineExceptionHandler:
You can set a custom CoroutineExceptionHandler
for a specific scope or for the entire coroutine context to handle exceptions. This allows you to centralize exception handling for multiple coroutines. Here's an example:
import kotlinx.coroutines.*
fun main() = runBlocking {
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
println("Caught an exception: ${exception.message}")
}
val job = GlobalScope.launch(exceptionHandler) {
// Perform some potentially throwing operation
throw IllegalStateException("An error occurred")
}
job.join()
}
3. SupervisorJob:
When using a SupervisorJob
, exceptions in one child coroutine do not affect other child coroutines. You can handle exceptions individually for each child coroutine. Example:
import kotlinx.coroutines.*
fun main() = runBlocking {
val supervisorJob = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + supervisorJob)
val job1 = scope.launch {
// Child coroutine 1
throw IllegalStateException("Error in job1")
}
val job2 = scope.launch {
// Child coroutine 2
throw ArithmeticException("Error in job2")
}
job1.join()
job2.join()
}