Enhancing Android App Performance with Kotlin Coroutines
As an Android developer, asynchronous programming can be considered as an indispensable issue in modern Android apps because you have to manage expensive and heavy tasks away from UI thread. Nowadays, Kotlin introduces an advanced and efficient approach of concurrency design pattern, which can be used on Android to simplify asynchronous codes. As a matter of fact, this approach is much more simple, comprehensive, and robust in comparison with other approaches in Android. This article will provide you with some key concepts and best-practices in Kotlin Coroutines for boosting Android app performance.
Introduction and Overview
As a software developer, preventing an application from blocking has been a critical problem for many years. There have been a number of practical approaches to solve this issue, such as Threading, Callbacks, Futures and Promises, and Reactive Extensions. In Kotlin coroutines, the idea is that a function can suspend its execution at some points and resume later on. Coroutines could be considered as light-weight threads, but there are lots of significant differences, which make their real-life usage completely different from threads.
It is conceptually similar to a thread, in the sense that it takes a block of code to run that works concurrently with the rest of the code. However, a coroutine is not bound to any particular thread. It may suspend its execution in one thread and resume in another one.
As a result, Kotlin coroutines can enable you to write clean and simplified asynchronous code, which keeps your Android app responsive when handling long-running tasks, like network calls or disk operations. For instance, after running the below code, you will see that Kotlin will be printed first, then Coroutines will be printed.
import kotlinx.coroutines.*fun main() = runBlocking {
launch {
delay(3000L)
println("Coroutines")
}
println("Kotlin")
}
Here, launch is a coroutine builder. This means it launches a new coroutine concurrently with the rest of the code for working independently. Besides, delay is a suspending function. It suspends the coroutine for a particular time. Suspending a coroutine does not block the underlying thread. So, it allows other coroutines to run and use the underlying thread for their code. Eventually, runBlocking is a coroutine builder that bridges the non-coroutine world and the code with coroutines inside of curly braces. In short, it runs a new coroutine and blocks the current thread until its completion.
Basically, Kotlin Coroutines follow Structured Concurrency Principle. This means new coroutines can be only launched in a specific CoroutineScope that determines the lifetime of the coroutine. If we want to extract the block of code inside launch() into a separate function, you will get a new function with the suspend modifier.
import kotlinx.coroutines.*fun main() = runBlocking {
launch { printContext() }
println("Kotlin")
}suspend fun printContext() {
delay(3000L)
println("Coroutines")
}
In fact, Suspend Function is a function that could be started, paused, and resumed. One of the most significant points to note is that they are just only allowed to be called from a coroutine or another suspend function.
CoroutineScope
Initially, it is also possible to mention your own scope using the CoroutineScope apart from the coroutine scope supported by various builders. This creates a coroutine scope, and does not complete until fulfillment of all launched children. The primary difference between runBlocking and CoroutineScope is that the runBlocking method blocks the current thread for waiting; whereas, CoroutineScope just suspends, which means it releases the underlying thread for other usages. Due to this reason, runBlocking is a regular function and CoroutineScope is a suspending function. For example, we can be able to write the previous code using CoroutineScope as follows:
import kotlinx.coroutines.*fun main() = runBlocking {
printContext()
}suspend fun printContext() = coroutineScope {
launch {
delay(3000L)
println("Coroutines")
}
println("Kotlin")
}
In Android development, some KTX libraries offer their own CoroutineScope for specific lifecycle classes. For instance, ViewModel has a viewModelScope
, and Lifecycle has lifecycleScope
. In addition, viewModelScope is used in the some cases in background threading with coroutines, but if you really require to build your own CoroutineScope in a certain layer of your Android app, you can have an opportunity to implement it.
Starting a coroutine
Essentially, you can be able to start coroutines in two different approaches: 1. launch: it starts a new coroutine and does not return the output to the caller (fire and forget). 2. async: it starts a new coroutine and allows you to return a result with a suspend function, known await. In other words, you should launch a new coroutine from a typical function because a typical function cannot call await. Also, you should use async just only when you are inside another coroutine, or when you are inside a suspend function and performing parallel decomposition. For instance, we can implement Structured Concurrency with async as follows:
import kotlinx.coroutines.*
import kotlin.system.*fun main() = runBlocking<Unit> {
val time = measureTimeMillis {
println("The result is ${concurrentSum()}")
}
println("Completed in $time ms")
}suspend fun concurrentSum(): Int = coroutineScope {
val one = async { doTaskOne() }
val two = async { doTaskTwo() }
one.await() + two.await()
}suspend fun doTaskOne(): Int {
delay(2000L)
return 20
}suspend fun doTaskTwo(): Int {
delay(2000L)
return 30
}
So, the output will be as printed below:
The result is 50
Completed in 2038 ms
Job
A Job is a handle to the launched coroutine and can be used to explicitly wait for its completion. A job instance identifies the coroutine and manages its lifecycle. In fact, whenever a new coroutine is launched, it will return a job. The returned job can be used in many places. For example, this can be applied to wait for the coroutine to perform some work, or it can be used to cancel the coroutine. The job can also be utilized to call a some functionalities, such as the join() method, which is used to wait for the coroutine, and the cancel() method which is used to cancel the execution of the coroutine.
A
Job
is a handle to a coroutine. Each coroutine that you create withlaunch
orasync
returns aJob
instance that uniquely identifies the coroutine and manages its lifecycle. You can also pass aJob
to aCoroutineScope
to further manage its lifecycle.
In the following example, you can wait for completion of the child coroutine and then print “Done!” string:
import kotlinx.coroutines.*fun main() = runBlocking {
val job = launch {
delay(3000L)
println("Coroutines")
}
println("Kotlin")
job.join()
println("Done!")
}
Moreover, you can pass a Job to a CoroutineScope to manage its lifecycle as follows:
class SampleClass {
fun sampleMethod() {
val job = scope.launch {
}
if (...) {
job.cancel()
}
}
}
Additionally, SupervisorJob is similar to a regular Job. The only difference is that its children can fail independently of each other. This means a child’s failure or cancellation does not make impacts on its other children. Thus, a SupervisorJob can create a unique policy for facing with its children’s failures.
Dispatchers
Basically, Kotlin coroutines use dispatchers to control which threads are used for coroutine execution. All Kotlin coroutines have to run in a dispatcher, even when they are running on the main thread. To determine where the coroutines should run, Kotlin supports three different dispatchers:
- Dispatchers.Main: This is used to run a coroutine on the main Android thread. This should be used only for interacting with the UI and performing quick work, such as calling suspend functions, running Android UI framework operations, and updating LiveData objects.
- Dispatchers.IO: This is applied for doing disk or network I/O outside of the main thread, such as working with Room, reading or writing files, and running network operations.
- Dispatchers.Default: This dispatcher is optimized to perform CPU-intensive work outside of the main thread, including sorting a list and parsing JSON.
For instance:
suspend fun fetchUser(): User { return GlobalScope.async(Dispatchers.IO) {
// make network call
// return user
}.await()
}
CoroutineContext
As a matter of fact, coroutines always execute in some context showed by a value of the CoroutineContext type, defined in the Kotlin standard library. a CoroutineContext (as an Interface) is an indexed set of elements that define the behavior of a Coroutine.
Persistent context for the coroutine. It is an indexed set of Element instances. An indexed set is a mix between a set and a map. Every element in this set has a unique Key.
There are four elements you can use and combine in a CoroutineContext as follows:
- CoroutineDispatcher: As mentioned earlier, a Dispatcher dispatches work to the proper thread by using several ways.
2. CoroutineExceptionHandler: Another element that can be added to the CoroutineContext is the CororoutineExceptionHandler, which is an optional that allows you to handle uncaught exceptions.
3. CoroutineName: It gives a name to the coroutine that might be useful for debugging.
4. Job: As already been said, it determines the lifecycle of the Coroutine, not only a coroutine, but also a CoroutineScope.
A
CoroutineContext
defines the behavior of a coroutine using the following set of elements:
Job
: Controls the lifecycle of the coroutine.
CoroutineDispatcher
: Dispatches work to the appropriate thread.
CoroutineName
: The name of the coroutine, useful for debugging.
CoroutineExceptionHandler
: Handles uncaught exceptions.
In the following example, the CoroutineScope function takes a CoroutineContext as a parameter. When creating a CoroutineScope, you can pass in a job to control its lifecycle. Nevertheless, if you do not perform it, the function will carry out it for you. Therfore, now, you can cancel the scope itself as before by calling scope.cancel(), or you can cancel the job. Both options are functionally equivalent.
class SampleClass {
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// It starts a new coroutine within the scope..
scope.launch {
// New coroutine that can call suspend functions..
fetchDocs()
}
}
fun cleanUp() {
scope.cancel()
}
}
The important point is that new coroutines created by this scope will inherit its CoroutineContext and will overwrite the job element with a new instance of job. Besides, you can combine multiple CoroutineContexts using the plus operator as the CoroutineContext is a set of elements. For instance,
class SampleClass {
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun sampleMethod() {
val jobOne = scope.launch {
// New coroutine with CoroutineName = "coroutine" (default)
}
val jobTwo = scope.launch(Dispatchers.Default + "BackgroundCoroutine") {
//New coroutine with CoroutineName= "BackgroundCoroutine(overridden)
}
}
}
withContext()
To modify the CoroutineContext within a coroutine, you should use withContext. That is a suspend function from the coroutine’s library. As withContext() is itself a suspend function, any function that is included it can also be a suspend function. For instance:
Class SampleClass {val scope = CoroutineScope(… )fun loadExample() {
//New instance of Job created..
val job = scope.launch {
//Using Dispatchers.Main here..
withContext() {
//Using Dispatchers.Default here..
}
}
}
}
withContext()
does not add extra overhead compared to the similar callback-based implementation. Hence, with coroutines, you can dispatch threads with fine-grained control. Since withContext() allows you control the thread pool of any line of code without introducing callbacks, you can apply it to very small functions, like reading from a database or doing a network request.
In conclusion
As a software developer, preventing an application from blocking has been a critical problem for many years. However, Kotlin is using coroutine to work with asynchronous codes that is based on the idea of suspend-able computations. A coroutine is a concurrency design pattern, which you can use in Android development to simplify code for executing asynchronous programs. This essay discusses some key concepts and best-practices in Kotlin Coroutines for improving Android app performance based on JetBrains and Google documents and resources.