NashTech Insights

Coroutines: Exploring Asynchronous Programming in Android

Harsh Vardhan
Harsh Vardhan
Table of Contents

Problem Statement

In Android development, the main thread (also known as the UI thread) is responsible for handling UI interactions and rendering. If you perform long-running operations or tasks that might block this thread, it can lead to unresponsive user interfaces.

Asynchronous programming addresses this issue by allowing long-running tasks to be executed in the background, thus keeping the main thread responsive for user interactions.

Coroutines vs. Threads

While both coroutines and threads are mechanisms for executing instructions concurrently, coroutines are executed within threads. Some of the core benefits of coroutines are : 

Suspendable Nature

Coroutines can pause and resume, letting async code look like it runs step by step. This helps when tasks should wait for others. This suspendable nature enhances resource efficiency, responsiveness, and code clarity.

Context Switching

Coroutines can jump between places to work. Start in one spot (like the main thread), move to another (like the background), then come back – all in one go. This Is great for handling both UI and background tasks without freezing the main part.

Flexibility and Lightweight

Coroutines are lighter than threads. Both can do tasks at the same time, but threads need more work to set up and handle. Coroutines work well on a few threads, and you can use them again and again. This saves memory and works better for many tasks at once.

Getting Started with Coroutines

  • To use coroutines, add the following dependency in your build.gradle file.
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5'
  • In the main activity, launch the GlobalScope and  inside `GlobalScope.launch`, define the instructions that we want the coroutine to execute.

EXPLANATION  

Every coroutine should be launched within a specific coroutine scope, it’s not necessary for every coroutine to be launched in the global scope. When a scope ends or is canceled, the coroutines within it will be cancelled as well., as coroutines live within their associated scopes. 

Once the coroutine finishes its task, it should be terminated. But  In the global scope, if the app continues to run, the coroutine will keep running as well. And if the app terminates or exits, the coroutine launched in the global scope will also be terminated, even if it hasn’t finished its execution.Coroutines started from the global scope will be executed asynchronously in a separate thread, independent of the main program. The specific thread in which the coroutine is launched cannot be predicted.

Suspending Coroutines

Coroutines can be suspended, allowing for pausing and resuming their execution. In coroutines, we use the `delay` function instead of `sleep`. Unlike `sleep`, `delay` only pauses the coroutine without blocking the thread it’s running on, allowing other coroutines to execute concurrently on the same thread. Blocking a thread means that it’s unable to proceed with further tasks until the blocking condition is resolved.  This blocks the entire thread’s execution, potentially affecting other tasks running on the same thread.

Imagine a construction site where workers are building. Each worker is like a coroutine, having its tasks. Workers can take breaks (coroutine delay) without affecting others. The construction site represents a thread. If the site halts (thread sleep), all workers stop. When the project finishes (main thread ends), the site closes, and all tasks cease. Coroutines (workers) pause independently, threads (site) affect all tasks.

IMPLEMENTATION

EXPLANATION

Delay is a suspend function in coroutines, but you can also  create your own suspend functions. However, they should only be called from within a coroutine or another suspend function. Whereas sleep can be called from any context, including outside a coroutine. However, it blocks the thread where it’s called.

When you have two consecutive delay calls within the same coroutine, the delay times accumulate, which means the second delay call will execute after the total of both delay times.

Here’s a bit more detailed explanation:

  1. You have a coroutine with two delay(3000) calls one after the other.
  2. The first delay(3000) call will pause the coroutine for 3000 milliseconds.
  3. After the first delay, the second delay(3000) call will be executed, and it will also pause the coroutine for another 3000 milliseconds.

This results in a cumulative delay of 6000 milliseconds before the coroutine proceeds after the second delay call.

Understanding Context and Dispatchers

Coroutines are always started in a specific context, which determines the thread in which the coroutine will run. Previously, we used `GlobalScope.launch` to start a new coroutine, but it lacks control over the execution context. To gain more control, we can use dispatchers.

A dispatcher is responsible for determining the execution context of coroutines. The context provides necessary resources and information for code execution, including memory, threads or coroutines, input/output channels, synchronisation primitives, and access to external services or resources. Depending on the purpose of our coroutines, we should pass an appropriate dispatcher.

Dispatcher.Main

Starts a coroutine in the main thread, suitable for UI operations as UI changes should be made in the main thread.

Dispatcher.IO

Executes coroutines on a shared pool of threads allocated for I/O operations, such as networking, database operations, or file handling.

Dispatcher.Default

Provides a shared pool of threads optimised for executing computational tasks efficiently. Useful for long calculations that could block the main thread, which is crucial for a responsive UI.

Dispatcher.unconfined

This means a coroutine isn’t tied to any particular thread or thread pool. It starts executing in the current thread until it needs to pause (suspend). After suspension, the coroutine resumes execution in the thread that resumes it, which may or may not be the same thread.

EXPLANATION

It’s possible to switch between execution contexts within a coroutine as you launch a new thread exclusively for coroutines.For example, when making a network call and returning data to update the UI, we can start the coroutine with the `IO` dispatcher, retrieve the answer, and then switch the context to the `Main` dispatcher to update the UI. 

Using `runBlocking`

runBlocking is a function provided by the Kotlin coroutines library that allows you to start a new coroutine and block the current thread until the coroutine completes. If you want to do some asynchronous work in your program’s main function using coroutines. But, the main function is not a coroutine itself. In this case, you can use runBlocking to start a coroutine block within the main function and block the main thread until the coroutine completes its execution. This allows you to use suspending functions and coroutines as if you were inside a coroutine.

EXPLANATION

When you use runBlocking, the main thread itself is blocked until the coroutine block within runBlocking completes, but other coroutines you launch inside that block will still run concurrently with the coroutine in the main thread. So, the delay times of coroutines launched within `runBlocking` won’t add up.

Coroutine Jobs: Waiting and Cancellation

A job is like a task that you can cancel, and it has stages from start to finish. It represents a cancellable computation that can be performed asynchronously. Whenever we launch a coroutine, it returns a job that can be saved in a variable. We can use this job to wait for the coroutine to finish or cancel it. The `job.join()` function suspends the current thread until the associated job is done. 

Cancelling a coroutine job doesn’t immediately stop the underlying coroutine code from running. Cancellation is cooperative, meaning our coroutine needs to be correctly set up to be cancelled. Sometimes, coroutines may be busy with calculations and have no time to check for cancellation.  In such cases, we need to manually check if a coroutine has been cancelled,using a mechanism like isActive or by throwing a CancellationException, and they stop their execution gracefully when they detect a cancellation. 

If you want to implement a timeout for a function call and cancel the coroutine if it takes too much time, you can use the withTimeout function provided by the Kotlin coroutines library. This function allows you to set a maximum time limit for the execution of a coroutine, and if the coroutine doesn’t complete within that time, it will be cancelled. 

Async and Await

If we have two suspending functions (networkCall1 and networkCall2) in a single coroutine, they are invoked sequentially. As a result, the total time taken for the entire operation will be approximately 6 seconds, which is the sum of the individual delays of both network calls.

If you want to execute the network calls concurrently, you would need to use two separate coroutines (two separate launch statements) or use the async coroutine builder to run them concurrently and then await their results.

`async` starts a new coroutine but returns a `Deferred` object instead of a job. The `Deferred` object ensures that the coroutine waits for the result. To obtain the result, we use `answer.await()`, which blocks the current coroutine until the result is available. 

Coroutine Scopes

In Android, selecting the right coroutine scope is critical for effective concurrency. Steer clear of GlobalScope, which can lead to memory leaks. Instead, embrace specialised scopes tied to lifecycle components like activities and view models. These scopes automatically cancel coroutines when components are destroyed, preventing leaks and ensuring efficient resource usage.

Lifetime Scope

Scoped to the activity’s lifespan, ensuring coroutines end with the activity.

ViewModel Scope

Aligned with the view model’s existence, maintaining coroutines through view model changes.

Add the following dependency in your build.gradle file.

implementation ("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")

Conclusion

Coroutines offer a powerful and flexible way to handle asynchronous programming in Android. By leveraging coroutines, we can write clean, concise, and efficient code that avoids blocking the main thread and provides a better user experience. Understanding how to work with coroutines, handle exceptions, and manage cancellation is essential for developing robust and responsive Android applications.

Nadra Ibrahim is the owner of this blog, If you’re interested in exploring the complete code used throughout this blog, you can find it on GitHub:. here! and on Nadra’s Linkedin here!

Harsh Vardhan

Harsh Vardhan

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

%d bloggers like this: