Getting Started with Kotlin Coroutines in Android
A brief introduction to coroutines that will help you increase productivity and write cleaner and more concise code in your applications.
- What are Coroutines?
- What is the function of a Coroutine?
- What is launch?
- Types of CoroutineScope
- What does Dispatchers handle?
- Handling exceptions using Coroutines
What is the difference between Asynchronous and Synchronous?
The term “asynchronous” refers to something that happens independently of another process and not necessarily in the same order. For example, if you have an application that performs an asynchronous task, that task can be performed while the application continues to run and do other things. This is useful when you want to avoid blocking the user interface (UI) thread while performing a task that may take a long time, such as downloading data from the Internet.
On the other hand, the term “synchronous” refers to something that happens in a specific order and a synchronous task must be completed before another task can start. For example, if you have an application that performs a synchronous task, the application cannot do anything else until that task is completed. This can be useful when you want to ensure that a task is completed before another task begins, but it can be inconvenient if the task takes a long time and blocks the user interface (UI).
An applied example in Android could be an “asynchronous” task to download data from the Internet while a loading screen is shown to users. Instead of blocking the user interface (UI) while the download is performed, a loading screen can be shown and the download can be performed asynchronously in the background. This way, users can continue interacting with the application while the download is performed and the user interface (UI) can be updated once the download is completed.
An example of a synchronous task in an Android application could be reading an internal storage file and processing the data contained in it. In this case, it is important to ensure that the entire file is read and processed before performing any other task, so a synchronous task must be used
What are Coroutines?
Coroutines are a way of writing asynchronous code in a synchronous way. This means you can write code that runs asynchronously in a similar way to how you would write synchronous code, but without having to deal with nested callbacks and without blocking the user interface (UI) thread. This makes it easier to write applications that perform asynchronous tasks in a clearer and more readable way.
Coroutines are available from Android 3.0 (Honeycomb) and can be used with the AndroidX support library.
What is the function of a Coroutine?
The main function of coroutines in Android is to facilitate readability when writing asynchronous code in a clearer way. Coroutines allow you to write asynchronous code in a similar way to how you would write synchronous code, making it easier to read and understand.
In addition, coroutines allow you to perform asynchronous tasks more efficiently and avoid the use of nested callbacks, which can make the code easier to maintain. They also have less impact on performance than other asynchronous solutions, such as threads, so they are an attractive option for performing asynchronous tasks in Android.
In summary, coroutines are a useful tool for writing asynchronous code in a clearer and more readable way, and for making the code easier to maintain and have less impact on performance.
To declare a coroutine in Android, you must first include the AndroidX support library in your project and add the dependency in your build.gradle file.
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}
Then, in your class, you must import the CoroutineScope class and create an instance of this class.
To create a coroutine, you must use the launch or async method of CoroutineScope. The launch method is used to create a coroutine that does not return a result, while the async method is used to create a coroutine that returns a result. Both functions take a lambda code block as an argument that contains the logic of the coroutine.
Example of a coroutine that does not return a result:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class MyViewModel : ViewModel() {
fun doWork() {
viewModelScope.launch {
// Do the work here
}
}
}
Example of how a coroutine that returns a result could be declared in Android:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.async
class MyViewModel : ViewModel() {
fun getData(): Deferred<Data> {
return viewModelScope.async {
// Get the data here and return the results
}
}
}
In both examples, an instance of CoroutineScope is created using the viewModelScope extension property and the launch or async method is used to create the coroutine. The lambda code block passed as an argument contains the logic of the coroutine and will be executed asynchronously when the coroutine is called.
What is launch?
launch is a method of the CoroutineScope class that is used to create a coroutine that does not return a result. It is used to perform asynchronous tasks that do not need to return a value to the caller, such as sending a network request or updating the user interface (UI).
The launch method takes a lambda code block as an argument that contains the logic of the coroutine and creates a new coroutine that will be executed asynchronously. The launch method returns a Job object that you can use to cancel the coroutine if necessary.
Example using the launch method to create a coroutine that updates the user interface (UI) in an Android application:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class MyViewModel : ViewModel() {
fun updateUI() {
viewModelScope.launch {
// Get the data here
withContext(Dispatchers.Main) {
// Update the ui with de data here
}
}
}
}
In this example, an instance of CoroutineScope is created using the viewModelScope extension property and the launch method is used to create a coroutine that will be executed asynchronously. The coroutine retrieves the necessary data and then updates the user interface (UI) with the data using the withContext(Dispatchers.Main) method, which executes the code on the user interface (UI) thread.
Types of CoroutineScope
There are several types of CoroutineScope that you can use to create coroutines in Android. Some of the most common ones are:
- GlobalScope: It is a CoroutineScope instance that runs on a global thread and is not tied to any Android component, such as an Activity or a Fragment. It can be used to perform tasks that are not linked to the user interface (UI) or to start a coroutine that is independent of the lifetime of any Android component.
- viewModelScope: It is a ViewModel extension property that can be used to create coroutines that are cancelled when the ViewModel is destroyed. It is useful for performing asynchronous tasks that are related to the view, but that do not depend on the lifetime of a particular Activity or Fragment.
- lifecycleScope: It is a LifecycleOwner (such as an Activity or Fragment) extension property that can be used to create coroutines that are cancelled when the LifecycleOwner enters a destruction state. It is useful for performing asynchronous tasks that depend on the lifetime of a particular Android component.
Example of how different types of CoroutineScope could be used to create coroutines in an Android application:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class MyViewModel : ViewModel() {
fun doWork() {
// Creaate a coroutine in viewModelScope
viewModelScope.launch {
// Do the work here
}
}
}
import androidx.lifecycle.ViewModel
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class MyViewModel : ViewModel() {
fun doWork() {
// Create a coroutine in GlobalScope
GlobalScope.launch {
// Do the work here
}
}
}
class MyFragment : Fragment, LifecycleOwner {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Create a coroutine in lifecycleScope
lifecycleScope.launch {
// Do the work here
}
}
What is the role of Dispatchers?
The Dispatchers object is a class provided by Kotlin that gives access to different execution threads and is often used with coroutines to specify which thread a task should be run on.
For example:
- Dispatchers.Main is used to run a task on the UI thread and is useful for updating the UI from a coroutine.
- Dispatchers.IO is used to run I/O tasks such as network requests and Dispatchers.Default is used to run tasks on a default worker thread. Example of how to use the Dispatchers object to specify which thread a task should run on in a coroutine in an Android application:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MyViewModel : ViewModel() {
fun doWork() {
viewModelScope.launch {
// Do the work on the default thread
val result = withContext(Dispatchers.Default) {
// Do the work here
}
// Update the user interface (UI) with the thread result of the user interface (UI)
withContext(Dispatchers.Main) {
// update the user interface (UI) here
}
}
}
}
In this example, the coroutine starts on the default worker thread and then uses the withContext(Dispatchers.Default) method to execute a task on the default worker thread. Then, the withContext(Dispatchers.Main) method is used to update the user interface (UI) with the result on the user interface thread.
Handling exceptions using Coroutines
Coroutines allow you to handle exceptions in a similar way as you would in synchronous code. You can use the try-catch statement to catch exceptions and the throw statement to throw exceptions.
For example, here is an example of how you could handle an exception in a coroutine in an Android app:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class MyViewModel : ViewModel() {
fun doWork() {
viewModelScope.launch {
try {
// Do the work here
} catch (e: Exception) {
// Manage the exception here
}
}
}
}
In this example, a coroutine is created and the throw statement is used to throw an exception from within the coroutine. The exception can then be caught and handled using a try-catch block. It’s important to note that exceptions thrown from within a coroutine will propagate through the coroutine’s parent job, allowing them to be caught and handled at higher levels if needed. ie:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class MyViewModel : ViewModel() {
fun doWork() {
viewModelScope.launch {
// Do the work here
if (errorOccurred) {
throw Exception("Error occurred")
}
}
}
}
In this example, an exception is thrown if an error occurs during the execution of the coroutine. The exception can be caught and handled elsewhere in the code, as shown in the first example.
It is important to note that exceptions thrown from a coroutine will not automatically be propagated to the thread in which the coroutine was created. Instead, you must catch the exception in the coroutine and handle it there or use the async method and the await method to propagate the exception to the thread that called the coroutine.
Handling exceptions with Async & Await
Another way to handle exceptions with coroutines is to use the async method and the await method to propagate the exception to the thread that called the coroutine.
The async method is similar to the launch method, but it returns a Deferred object that you can use to get the result of the coroutine once it is complete. The await method is used to wait for the coroutine to complete and get its result. If the coroutine throws an exception, the await method will propagate the exception to the thread that called the coroutine.
Here is an example of how you could handle an exception using the async method and the await method in an Android app:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
class MyViewModel : ViewModel() {
fun doWork(): List<Result> {
val deferredResults = listOf(
viewModelScope.async {
// Do the work and return the result here
if (errorOccurred) {
throw Exception("Error occurred")
}
return@async result
}
)
return try {
deferredResults.awaitAll()
} catch (e: Exception) {
// Manage the exception here
emptyList()
}
}
}
In this example, a list of Deferred objects is created using the async method. Each Deferred object represents a coroutine that will be executed asynchronously. If any of the coroutines throw an exception, the awaitAll method will propagate the exception to the thread that called the coroutine. The exception is caught in the catch block and handled there.
If you use the awaitAll method to wait for several coroutines to complete and one of them throws an exception, the awaitAll method will propagate the exception and all remaining coroutines will be canceled.
For example, in the following code:
val deferredResults = listOf(
viewModelScope.async {
// Do the work and return the result here
if (errorOccurred) {
throw Exception("Error occurred")
}
return@async result1
},
viewModelScope.async {
// Do the work and return the result here
return@async result2
}
)
try {
deferredResults.awaitAll()
} catch (e: Exception) {
// Manage the exception here
}
If the first coroutine throws an exception, the awaitAll method will propagate the exception and cancel the second coroutine. If no exception occurs, the awaitAll method returns a list with the results of the coroutines.
You can also use the async method and the await method to execute several coroutines independently and handle each exception individually:
val deferredResult1 = viewModelScope.async {
// Do the work and return the result here
if (errorOccurred) {
throw Exception("Error occurred")
}
return@async result1
}
val deferredResult2 = viewModelScope.async {
// Do the work and return the result here
return@async result2
}
try {
val result1 = deferredResult1.await()
val result2 = deferredResult2.await()
} catch (e: Exception) {
// Manage the exception here
}
In this example, each coroutine is executed independently and each exception is handled individually in the catch block.
A more elegant way to handle coroutine exceptions in Kotlin is to use the runCatching method or the try method of the Deferred class.
The runCatching method is a top-level function that executes a block of code and returns a Result object that contains the result of the operation or the exception that was thrown. You can use the isSuccess method to check if the operation was successful and the exceptionOrNull method to get the thrown exception, if any.
Here’s an example of how you could use the runCatching method to handle exceptions in a coroutine in an Android application:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class MyViewModel : ViewModel() {
fun doWork() {
viewModelScope.launch {
val result = runCatching {
// Do the work here
if (errorOccurred) {
throw Exception("Error occurred")
}
return@runCatching result
}
if (result.isSuccess) {
// The result has been successfully
} else {
val exception = result.exceptionOrNull()
// Manage the exception here
}
}
}
}
In this example, a coroutine is created and the runCatching method is used to execute a block of code. If the operation was successful, the runCatching method returns a Result object with the result of the operation. If an exception was thrown, the runCatching method returns a Result object with the exception. You can use the isSuccess method to check if the operation was successful and the exceptionOrNull method to get the thrown exception, if any.
Another way to handle coroutine exceptions is to use the try method of the Deferred class.
The way to handle coroutine exceptions using the try method of the Deferred class is similar to the way to handle exceptions using the runCatching method. The try method is an extension function that returns a Result object that contains the result of the coroutine or the exception that was thrown.
Example of how the try method could be used to handle exceptions in a coroutine in an Android application:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
class MyViewModel : ViewModel() {
fun doWork(): List<Result> {
val deferredResults = listOf(
viewModelScope.async {
// Do the work and return the result here
if (errorOccurred) {
throw Exception("Error occurred")
}
return@async result
}
)
return deferredResults.map { it.try() }
}
}
In this example, a list of Deferred objects is created using the async method. Then the try method is used to get a list of Result objects that contain the result of the coroutines or the exceptions that were thrown. You can use the isSuccess method to check if the coroutine completed successfully and the exceptionOrNull method to get the thrown exception, if any.
It is important to note that the try method can only be used with Deferred objects and not with coroutines launched with the launch method. If you want to handle exceptions of coroutines launched with the launch method, you must use the runCatching method or the try-catch statement.
I hope this post has served you to get started with the use of coroutines and how to handle exceptions in different ways! :D.