How to Improve App Performance With Kotlin Coroutines in Android

How to Improve App Performance With Kotlin Coroutines in Android

A coroutine is a concurrency design pattern used by Android developers to simplify code that executes asynchronously**.** Many Android developers find that coroutines can increase their efficiency when creating mobile applications, allowing them to write code that is sequential, simple, and easy to read.

In this article, we will dive into the concept of coroutines in Android using Kotlin and learn how you can use them in your next Android application.

But before we move forward, let’s take a look at some basic concepts that will help you to understand coroutines more easily. Let’s begin!

What is Asynchronous Programming?

All android developers may have encountered both synchronous and asynchronous approaches to mobile app development, which can be performed on one or more threads.

In synchronous programming, tasks are executed one at a time; once one task is finished, the next one begins and it is not possible to pause the execution of a task in order to carry out another.

Meanwhile, with asynchronous programming, tasks can be executed concurrently; the thread that initiated the task can pause the execution of that task, and then start the execution of another.

Having understood that, let’s move on to the concept of coroutines in Kotlin.

What Are Kotlin Coroutines?

The use of asynchronous programming is essential for development. Android offers several approaches for this, such as Coroutines. Coroutines are more efficient than JVM threads in terms of resource usage. If you're interested in learning more, the official Kotlin Coroutines documentation provides a great deal of information.

Coroutines are a great way to ensure that your application runs smoothly and efficiently, as they allow you to run tasks without blocking the main thread. The main thread is responsible for responding to user interaction, such as clicks, updates, and other UI callbacks, so it is vital to avoid blocking it.

Kotlin offers a solution to the challenge of needing to download data from a server or database and display it on the screen without blocking the UI. This is made possible through the language's support for coroutines, which allow for asynchronous tasks to be run.

To gain a better understanding of how coroutines work, let's explore their key features.

Suspend function

The keyword "suspend" marks functions that are able to be started, paused and resumed from within a coroutine. It is essential to remember that these functions can only be invoked from a coroutine or from another suspend function.

We have marked the function loadUser() as a suspend function in this example:

suspend fun loadUser(): User {
   return // for example, some long task for retrieve user from REST or database
}

CoroutineScope

Only coroutines created within a certain area of CoroutineScope can be performed. This area is represented by the scope of the coroutine, which is defined by a CoroutineContext and controls the life cycle of coroutines created within it.

CoroutineContext

CoroutineContext is an interface that contains a collection of Elements, configuring the coroutine with a set of attributes.

Dispatchers

The CoroutineContext determines which thread or threads will be used to execute the coroutine, with the Dispatcher being one element of the context.

Dispatchers.Default

If no dispatcher type is explicitly stated, then the Dispatchers.Default type is used. This type is employed for computationally intensive tasks that require a large amount of CPU power.

launch(Dispatchers.Default) {
    // some calculations
}

Dispatchers.IO

Disk and network I/O operations can be performed outside of the main thread using Dispatchers.IO, such as using the database component, reading from or writing to files, and running any network operations.

launch(Dispatchers.IO) {
     // some network request
}

Dispatchers.Main

Use this Dispatchers.Main to run coroutines on the main Android thread only for tasks that require minimal effort, such as calling suspend functions, and running Android UI framework operations.

launch(Dispatchers.Main) {
    // some logic for UI
}

Dispatchers.Unconfined

The coroutine in Dispatchers.Unconfined initially runs on the current thread and does not switch to any specific thread or thread pool. After the coroutine is resumed, it executes on one of the threads.

launch(Dispatchers.Unconfined) {
    // do something, no matter in what thread
}

Method launch()

launch() is a coroutine builder that creates a new coroutine, allowing us to execute code within the coroutine sequentially. It returns a reference to the coroutine as a Job, which can be used to monitor the progress of the coroutine.

This is often used when we don't need to return a result from a coroutine, and when we need to execute it simultaneously with other code.

For example, we can use it to launch a task program that will perform various tasks:

fun someMethod() {
    val scope = CoroutineScope(Dispatchers.IO)

     val job: Job = scope.launch {
          // some long task
     }   
}

Job

Job is an element of CoroutineContext that allows for the management of the life cycle of a coroutine. Through Job, the execution of the coroutine can be stopped, and a parent coroutine can be created to run other coroutines inside of it and manage their life cycles.

If an error or exception occurs in the child coroutine, the parent coroutine and all of its child coroutines will be canceled.

Method withContext()

The withContext() function allows us to create a new coroutine and pass a dispatcher to it so that the block's performance will occur on the thread of the passed dispatcher. After the task within the block has been executed, the control will be returned to its previous dispatcher.

If there are multiple withContext blocks within a parent block, they will be suspended by the parent thread and will execute one after the other in sequence.

For example, when we start the coroutine with Dispatchers.Main, the withContext() method will stop the parent coroutine and launch the child one with its Dispatchers.IO.

This will cause the new coroutine to execute the task in another thread.

Upon completion, the child program will resume the suspended parent coroutine with Dispatchers.Main, thus allowing the method showUserInfo() to be executed in the main thread.

private fun switchDispatcher() {
   val scope = CoroutineScope(Dispatchers.Main)

   scope.launch {
       val user: User = withContext(Dispatchers.IO) {
          // some long task for retrieve user from REST or database
          loadUser()
       }
       showUserInfo(user)
   }
}

private fun showUserInfo(user: User) {
    // work in the main thread, displaying data on the screen
}

Method async()

The async() function is a coroutine builder that launches a new coroutine similar to the launch() function. It returns a Deferred object which is an implementation of the Job interface and provides a way to access the result of the coroutine.

The await() function is used to retrieve the result, which suspends the current coroutine until the result of the Deferred object is received.

suspend fun loadFile() {
    val scope = CoroutineScope(Dispatchers.IO)

    val deferred: Deferred = scope.async {
         // for example, some long task for load file from REST
    }

    val file: file = deferred.await();
}

We can also utilize async to concurrently execute multiple coroutines.

suspend fun loadTwoFiles() {
   val scope = CoroutineScope(Dispatchers.IO)

   val firstDeferred: Deferred = scope.async {
       // for example, some long task for load file from REST
   }
   val secondDeferred: Deferred = scope.async {
       // for example, some long task for load file from REST
   }

  val firstFile: File = firstDeferred.await()
  val seconbFile: File = secondDeferred.await()
}

Two coroutines are launched simultaneously in this example, each of which loads files and return a Deferred <File> object. Calling the await() method on this object will return a File object. These coroutines will execute asynchronously. If there is an error or exception in one of the coroutines, both coroutines will be left.

Handle exception

Using the coroutine builder - launch(), we can use the well-known block try/catch to handle any exceptions that might occur while executing some task. The try block contains the code that can throw the exception, and the catch block is used to handle it.

fun handleExcWithLaunch() {
    val scope = CoroutineScope(Dispatchers.IO)

    scope.launch {
        try {
            // some long task 
        } catch (exception: Exception) {
            println("Handle Exception: $exception")
        }
    }
}

The async() coroutine builder also uses block tracks for exception handling, but instead of wrapping a suspended function in a try/catch block, it wraps an object Deferred.

fun handleExcWithAsync() {
   val scope = CoroutineScope(Dispatchers.IO)

   val deferred: Deferred = scope.async {
       // some long task for retrieve user from REST or database
   }

   try {
       val user = deferred.await() 
    } catch (exception: Exception) {
       println("Handle Exception: $exception")
   }
}

For example, consider the following example. We will launch the coroutine in a try/catch block and independently throw exceptions to test it.

fun handleExcWithLaunch() {
   val scope = CoroutineScope(Dispatchers.IO)

   scope.launch {
       try {
           launch {
               throw RuntimeException("Thrown RuntimeException")
           }
       } catch (exception: Exception) {
           println("Handle Exception: $exception")
       }
   }
}

If a try/catch block is not used to handle exceptions in the internal coroutine, the exception will not be thrown and thus cannot be caught by the outer try/catch.

As a result, the exception is propagated through the Job hierarchy and can be handled by a CoroutineExceptionHandler, preventing the application from crashing.

CoroutineExceptionHandler

We can use the CoroutineExceptionHandler ContextElement to handle uncaught exceptions and exceptions that occur in child coroutines.

private val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, exception ->
    println("Handle Exception: $exception")
}

private fun handleExcWithHandler() {
   val scope = CoroutineScope(Dispatchers.IO)

   scope.launch(coroutineExceptionHandler) {
       launch(Dispatchers.IO) {
           try {
               throw RuntimeException("Thrown RuntimeException")
           } catch (exception: Exception) {
               println("Handle Exception: $exception")
           }
       }
   }
}

Conclusion

Kotlin coroutines provide an efficient way to engage in app development work, as it eliminates the need to block the main thread of the application during the execution of time-consuming tasks.

Coroutines make asynchronous programming easier as they keep all complexities within the libraries while allowing the code to be written sequentially and be more understandable and readable.

Additionally, they help to avoid the 'callback hell', which arises with callbacks when tasks need to be performed sequentially. Coroutines are a powerful and useful tool for creating fast and high-quality mobile applications.

If you are interested in Android mobile app development, explore the QuickBlox Android SDK and the code samples available.

Join the QuickBlox Dev Discord Community ! Share ideas, learn about software, and get support.