Coroutines : Exploring Asynchronous Programming in Android

Coroutines : Exploring Asynchronous Programming in Android

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.

No alt text provided for this image
Hanged UI












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 : 

  1. 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.
  2. 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.
  3. 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:

  1. To use coroutines, add the following dependency in your build.gradle file.

No alt text provided for this image
Dependency for coroutine

2. In the main activity, launch the GlobalScope and  inside `GlobalScope.launch`, define the instructions that we want the coroutine to execute.

No alt text provided for this image
Basic Coroutine Application

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 : 

No alt text provided for this image
Suspended Coroutine Demo

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.

  1. `Dispatcher.Main`: Starts a coroutine in the main thread, suitable for UI operations as UI changes should be made in the main thread.
  2.  `Dispatcher.IO`: Executes coroutines on a shared pool of threads allocated for I/O operations, such as networking, database operations, or file handling.
  3. `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.
  4. 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.

No alt text provided for this image
Dispatchers Demo

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.

No alt text provided for this image
runBlocking Demo

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. 

No alt text provided for this image
Coroutine's Job Demo

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. 

No alt text provided for this image
Job Cancellation Demo

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. 

No alt text provided for this image
Timeout Demo

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. 

No alt text provided for this image
Async And Await Demo

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.

  1. Lifetime Scope: Scoped to the activity's lifespan, ensuring coroutines end with the activity.
  2. ViewModel Scope: Aligned with the view model's existence, maintaining coroutines through view model changes.

Add the following dependency in your build.gradle file :

No alt text provided for this image
Dependency for model scope
No alt text provided for this image
Lifecycle Scope Demo

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.

If you're interested in exploring the complete code used throughout this blog, you can find it on GitHub:.Github Repository







To view or add a comment, sign in

More articles by Nadra Ibrahim

  • OAuth 2.0 Simplified: Secure Your Kotlin Apps

    Introduction This blog is your shortcut to understand the essentials of OAuth 2.0, so you can confidently build secure…

    4 Comments

Others also viewed

Explore content categories