Skip to main content

Kotlin Coroutines - Asynchronous Programming in Kotlin Android

· 15 min read
Kustiawanto Halim

Exception Handling

In modern mobile applications, it is common that the application is required to perform network calls. The developer needs to make sure this heavy work is not running in the main thread of the application, as this will block the UI, leading the application to be unresponsive and triggering an Application Not Responsive (ANR) error.

There have been many approaches to preventing apps from blocking, including threading, callbacks, futures, promises, and most importantly, coroutines.

Kotlin handles this issue effectively by delegating most functions to libraries and integrating coroutine support at the language level. A coroutine is a concurrency design concept that may be used on Android to facilitate asynchronous function operation. Based on the existing principles from other languages, coroutines were added to Kotlin in version 1.3.

Coroutines not only enable asynchronous programming—often known as non-blocking programming—but also offer up plenty of additional possibilities, such as concurrency and actors. Coroutines on Android manage lengthy operations that will otherwise render your app unresponsive by blocking the main thread.

This article discusses how to leverage coroutines in Kotlin to solve asynchronous programming challenges, which allows you to create clearer and more efficient code.

Alternatives to Coroutines

Before we get into coroutines, let's have a look at some of the alternative solutions.

Threading, or using a separate thread for long-running functions is the most well-known approach to avoid blocking the UI. However, it has several drawbacks: threading is costly, the number of threads is limited by your operating system, and debugging threads becomes yet another issue in multi-threaded programming.

Callbacks are another popular solution to concurrency problems. A callback is used to provide one function as a parameter to another function, which will then be called after the process is finished. Although it seems like a possible solution, It may cause callback hell which will make it hard to propagate and handle errors.

A future or promise (other languages use other names) entails a promise object that can be operated on after a function call. Using a promise requires us to use a specific design pattern and API to handle chaining calls. We also need to introspect the promise object to be able to get the real value being promised.

Kotlin's approach for working with asynchronous code is to adopt coroutines, instances of suspendable computations, in which a function can postpone its execution and restart it later. Promise and Coroutines differ primarily in that Promise returns an explicit result object, whereas Coroutines yields (which allows other functions to take control of the running thread), enabling us to suspend or resume a Coroutines. Coroutines aren’t really a new idea; in fact, they’ve been around for years and are common in several other programming languages, like Go.

One benefit of using a coroutine is that writing non-blocking code is almost the same as writing blocking code for developers. The programming paradigm itself does not change. Most of the functionality is delegated to libraries, and you won’t have to learn an entirely new set of APIs.

Kotlin Coroutines

Notable Features

There are several benefits to using coroutines:

  • Lightweight: Since suspending is supported, you can execute multiple coroutines on a single thread without blocking the thread where the coroutine is operating. Suspending, as opposed to blocking, saves memory while allowing for several concurrent tasks.
  • Fewer memory leaks: This is because coroutines run operations using structured concurrency inside a scope.
  • Built-in cancellation support: Cancellations seamlessly propagate across the running coroutine tree.
  • Jetpack integration: Various Jetpack libraries have extensions that offer comprehensive coroutine access; some libraries additionally offer their native coroutine scope for structured concurrency.

Coroutine Concepts

There are four main elements of a coroutine: dispatchers, CoroutineScope, jobs, and CoroutineContext.

Starting a Coroutine

There are two ways to start a coroutine:

  • launch creates a new coroutine but does not return the result to the caller; you can use it to start any job that is presumed "fire and forget."
  • async creates a new coroutine and lets you return a result using the await suspend method.

Because a normal function cannot call await, you should use the launch function to create a new coroutine from it. Only use async within another coroutine or within a suspend function.

Dispatchers

Coroutines in Kotlin require dispatchers to specify which threads will be used to execute a coroutine. You need to set Kotlin coroutines to the default or IO Dispatchers to run code outside of the main thread.

Kotlin offers three dispatchers that you can use to designate where the coroutine should run:

  • Dispatchers.Main launches a coroutine on the main Android thread; you can only use this for dealing with the user interface and executing short operations, e.g., updating the value of a TextView.
  • Dispatchers.IO is designed to handle disk and network I/O independently of the main thread, e.g., running any network activity.
  • Dispatchers.Default executes coroutines for CPU-intensive tasks outside of the main thread. E.g., parsing a huge JSON object.

CoroutineScope

CoroutineScope keeps track of each coroutine it generates using launch or async. In Android, several KTX libraries provide their CoroutineScope for specific lifecycle types. ViewModel, for example, has a viewModelScope, whereas Lifecycle has a lifecycleScope. However, unlike a Dispatcher, a CoroutineScope does not run the coroutine.

The following code will demonstrate how to create your CoroutineScope:

class MyCoroutineScope {

// To create a scope, we will combine a Job and Dispatchers.
val myScope = CoroutineScope(Job() + Dispatchers.Main)

fun printNumberAfterDelay() {
// Launch a new coroutine with given scope
myScope.launch {
// Delay the function for oneseveral seconds
delay(1000L)
// Print desire number
println(97)
}
}

fun cancel() {
// To cancel ongoing coroutines work, simply cancel the scope
myScope.cancel()
}
}

A scope that has been canceled cannot launch any new coroutine. As a result, you should always use scope.cancel() when the class that controls its lifecycle is about to be destroyed. When you use viewModelScope, the ViewModel class automatically cancels the scope in the ViewModel's onCleared() function.

Job

A Job is the coroutine's handler. Each coroutine you create using launch or async produces a Job instance that has a unique identifier and maintains the coroutine's lifecycle. You also could provide a Job to a CoroutineScope to control its lifecycle, as illustrated in the example below:

class MyCoroutineScope {

fun printNumberAfterDelay() {
// Newly launched coroutines will be assigned to job
val job = myScope.launch {
// Long running task
}

if (...) {
// If we cancel the coroutine launched above,
// it will not affect the scope where the coroutine launched in
job.cancel()
}
}
}

CoroutineContext

The behavior of coroutines can be defined using CoroutineContext. Several things you can configure within CoroutineContext are:

  • A job, to control the coroutine’s lifecycle
  • Dispatchers, to control in which thread the coroutine runs
  • A name for the coroutine, for easier debugging process
  • A CoroutineExceptionsHandler, to catch exceptions
class MyCoroutineScope {

val myScope = CoroutineScope(Job() + Dispatchers.Main)

fun printNumberAfterDelay() {
// Launch coroutine on Dispatchers.Main
val job1 = myScope.launch {
// CoroutineName = "coroutine" (default)
delay(1000L)
println(98)
}

// Launch a new coroutine on Dispatchers.IO
val job2 = myScope.launch(Dispatchers.IO + "RunningOnIO") {
// CoroutineName = "RunningOnIO" (overridden)
delay(2000L)
println(99)
}
}
}

A new Job instance is assigned to a new coroutine launched within a Scope, while the other CoroutineContext properties are inherited from the parent Scope. By providing a new CoroutineContext to the launch or async function, you can override the inherited properties.

Now, let's discuss how to use coroutines with a real example.

Creating a Background Task

A network request is a common use case for all applications. We cannot call a network request on the main thread because it will block it and cause an Application Not Responding (ARN) error. That's why we need to call network requests in the background thread, and to do that, we can use a coroutine.

Take a look at the following example of repository code:

class DownloadFileRepository(private val fileHelper: FileHelper) {

// Function that makes the network request, blocking the current thread
fun makeDownloadRequest(url: String): Result<String> {
val url = URL(url)
(url.openConnection() as? HttpURLConnection)?.run {
requestMethod = "GET"
doOutput = true
fileHelper.save(inputStream)
return Result.Success(fileHelper.destinationPath)
}
return Result.Error(Exception("Cannot open HttpURLConnection"))
}
}

The makeDownloadRequest(url:) is a synchronous function that blocks the calling of the main thread. If you are not familiar with Result, it is basically just an encapsulation of the success value of generic type T or failure with a throwable exception.

Kotlin already provides the Result class, as defined in its documentation:

sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}

A ViewModel will have a function that calls the network request when the user performs an action, like that below:

class DownloadViewModel(
private val repository: DownloadFileRepository
): ViewModel() {

fun download(url: String) {
repository.makeDownloadRequest(url)
}
}

Calling DownloadViewModel.download(url:) will block the UI thread from which you’re making the request. We must avoid doing this since it will freeze the application UI so that the user cannot interact.

To make the download(url:) function run outside the main thread, create a new coroutine and run the function on an IO dispatcher as follows:

class DownloadViewModel(
private val repository: DownloadFileRepository
): ViewModel() {

fun download(url: String) {
// Launch network request on I/O Dispatchers
viewModelScope.launch(Dispatchers.IO) {
repository.makeDownloadRequest(url)
}
}
}

viewModelScope is the CoroutineScope included in the ViewModel KTX Extension. If you use ViewModel class, it’s recommended to start a coroutine in viewModelScope, which will be canceled; any running coroutine calls are also canceled when the ViewModel is destroyed.

When you call the download(url:) function from the View layer of the main thread, the launch function will create a new coroutine with IO dispatchers as the thread resolver. The download(url:) function will continue running until you get a success or failure response.

You already made sure that the download(url:) function does not block the main thread, but you still need to ensure that makeDownloadRequest(url:) will always be called from the IO dispatchers each time you want to use it. So, let’s refactor our code to make sure makeDownloadRequest(url:) is also a main-safe function.

Making Coroutine Main-Safe Functions

A function that does not block UI updates on the main thread is a main-safe function. makeDownloadRequest(url:) is not main-safe, as calling it from the main thread blocks the UI update.

To make this function main-safe, use the withContext(Dispatchers.IO) function provided by the coroutine to change the resolver thread to an IO Thread. Adding withContext will also make the makeDownloadRequest(url:) function become a suspend function. The keyword “suspend” is the way Kotlin forces us to call the function marked within a coroutine call.

Refactoring the code will look like the following:

class DownloadFileRepository(private val fileHelper: FileHelper) {

suspend fun makeDownloadRequest(url: String): Result<String> {
// Add coroutine context to IO Dispatchers
withContext(Dispatchers.IO) {
val url = URL(url)
(url.openConnection() as? HttpURLConnection)?.run {
requestMethod = "GET"
doOutput = true
fileHelper.save(inputStream)
return Result.Success(fileHelper.destinationPath)
}
return Result.Error(Exception("Cannot open HttpURLConnection"))
}
}
}

Now we do not need to specifically define the dispatcher in ViewModel layer, since we already moved the execution to a repository layer. Take a look at the refactored code:

class DownloadViewModel(
private val repository: DownloadFileRepository
): ViewModel() {

fun download(url: String) {
// We do not need to specify thread anymore
viewModelScope.launch {
val result = repository.makeDownloadRequest(url)

when (result) {
is Result.Success<DownloadResponse> -> // handle success response
else -> // handle error response
}
}
}
}

With the refactored code, we do not specify the dispatchers on the download(url:) function anymore. The coroutine called from viewModelScope is now running in the main thread but it is not blocking the UI update because makeDownloadRequest(url:) is running in the IO Dispatchers.

We also add some response handlers to the network request function, but is it safe from exception?

Exception Handling

Our repository layer can still throw an exception because it has a function to write network responses to a file. We can add try-catch function to handle this kind of exception as follows:

class DownloadViewModel(
private val repository: DownloadRepository
): ViewModel() {

fun download(url: String) {
viewModelScope.launch {
// Add try-catch to handle exceptions
val result = try {
repository.makeDownloadRequest(url)
} catch(e: Exception) {
Result.Error(Exception("Network request failed"))
}

when (result) {
is Result.Success<DownloadResponse> -> // handle success response
else -> // handle error response
}
}
}
}

Now, any unexpected error thrown from the repository layer will be treated as "Network request failed" and can be handled by the UI layer safely.

Best Practices

In this section, we’ll discuss the best practices for using coroutines to make your applications scalable and testable.

Suspend Function Must Be Main-Safe

A function must be main-safe if it is to be treated as a suspend function. If a class is responsible for long-running operations like network calls, that class is also responsible to move its execution from the main thread using withContext. In our example above, we already refactored our repository class (which is responsible for network calls) to call its function from the IO Thread.

Coroutines in ViewModel

If you use the ViewModel class in your applications, it is responsible for creating and launching the coroutine, instead of exposing it and launching it in the view layer. You should also use viewModelScope to create the coroutine since it will handle its lifecycle scope based on the ViewModel lifecycle scope:

class DownloadViewModel(
private val repository: DownloadRepository
): ViewModel() {

// DO THIS, create coroutine based on viewModelScope
fun download(url: String) {
viewModelScope.launch {
repository.makeDownloadRequest(url)
}
}

// DON'T DO THIS, do not make download suspend
suspend fun download(url: String) =
repository.makeDownloadRequest(url)
}

Making Coroutines Cancelable

A coroutine is not canceled when its job is canceled. If you block an operation in a coroutine call, you must make sure the coroutine is cancelable. To make sure it’s not canceled before calling it, we can add an ensureActive function.

class DownloadViewModel(
private val repository: DownloadRepository
): ViewModel() {

fun download(url: String) {
viewModelScope.launch {
// Add try-catch to handle exceptions
val result = try {
ensureActive() // Make sure not canceled
repository.makeDownloadRequest(url)
} catch(e: Exception) {
Result.Error(Exception("Network request failed"))
}

when (result) {
is Result.Success<DownloadResponse> -> // handle success response
else -> // handle error response
}
}
}
}

The Dispatcher Should Be Injected

When creating a new coroutine or calling it inside withContext, it is recommended to inject the dispatchers instead of hardcoding it. This will make your code testable when you want to test the coroutine by changing it to TestDispatchers, which we will discuss in a later section.

Your repository layer code will be refactored as follows:

class DownloadFileRepository(
Private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
private val fileHelper: FileHelper
) {

suspend fun makeDownloadRequest(url: String): Result<String> {
// now we call inside injected dispatchers
withContext(dispatcher) {
val url = URL(url)
(url.openConnection() as? HttpURLConnection)?.run {
requestMethod = "GET"
doOutput = true
fileHelper.save(inputStream)
return Result.Success(fileHelper.destinationPath)
}
return Result.Error(Exception("Cannot open HttpURLConnection"))
}
}
}

Testing Coroutines

When testing the coroutine function, you need to inject TestDispatcher into the coroutine dispatcher.

kotlinx-coroutine-test has two implementations of TestDispatcher:

  • StandartTestDispatcher will run the coroutine with a scheduler when the test thread is ready. Use this dispatcher to simulate a queue like a “real” dispatcher.
  • UnconfinedTestDispatcher will run the coroutine eagerly and write the test more easily. However, it gives you less control when the coroutine is executed during the test.

The runTest function tests a coroutine, while the runTest function will use a TestCoroutineScheduler to skip the delay in the suspend function tested. Take a look at the following sample test class:

    class DownloadFileRepositoryTest {

@Test
fun testMakeDownloadRequest() = runTest {
// Create a test dispatcher and inject it to the repository
val testDispatcher = UnconfinedTestDispatcher(testScheduler)

val fileHelper = FakeFileHelper()
val repository = DownloadFileRepository(
testDispatcher,
fileHelper
)

repository.makeDownloadRequest("http://www.shipbook.io")
assertThat(fileHelper.destinationPath.isNotEmpty())
}
}

When creating a test class, make sure to share TestDispatcher with the same scheduler. Doing this will allow your coroutine to run on a single thread to make your test deterministic.

Conclusion

Coroutines offer a lightweight and easy-to-use way to handle multi-threaded programming in Kotlin, as well as in Android development. Since coroutines are fully supported by Android and Kotlin itself, it is highly recommended that you use coroutines when dealing with asynchronous programming. Remember, although it is convenient to use coroutines, you also need to make sure your application handles the suspend function in the correct way.

Due to the fact that we can't really determine which instructions are being executed at any particular time, asynchronous programming can occasionally produce errors. If we want to debug asynchronous code, we may utilize logs. Shipbook captures your application logs and exceptions, allowing you to remotely gather, monitor, and evaluate your user mobile-app logs and crashes in the cloud on a per-user and session basis. This will help you to quickly examine relevant data and solve errors.