Kotlin withContext() vs Async Await

Learn via video courses
Topics Covered

As technology is getting advanced day by day, developers are constantly searching for more efficient and readable ways to handle asynchronous programming. Asynchronous programming is a programming technique that allows tasks to be performed concurrently. In the Kotlin programming language, two popular mechanisms used for managing asynchronous tasks are async-await and withContext. In this article, we will delve into the details of Kotlin’s withContext and async-await constructs, providing examples and comparing their usage.

What is Kotlin Coroutine?

A Kotlin Coroutine is a feature introduced to make asynchronous programming easier. It allows developers to write asynchronous code with a fluent API sequentially, making it more readable and easier to maintain than the traditional callback-based approach. Coroutines are lightweight threads, allowing concurrent execution without the overhead associated with heavy threads.

A Coroutine in Kotlin is executed within a CoroutineContext which consists of several elements that define the behavior of the coroutine. Some of the key elements are:

  • CoroutineId is a unique identifier for the coroutine.
  • CoroutineName is the name of the coroutine which is useful for debugging.
  • CoroutineDispatcher determines what thread or threads the corresponding coroutine uses for its execution. It can confine coroutine execution to a specific thread, dispatch it to a thread pool, or let it run unconfined.
  • Job is a fundamental element that represents the lifecycle and state of the coroutine, such as active, completed, or canceled. We can use a job to cancel a coroutine or to wait for its completion.
  • CoroutineScope defines the scope and lifetime of the coroutine. A coroutine can access its scope through the coroutineScope function or this keyword. A scope can also launch new coroutines, which will be bound to the same scope and job.

To create and start a coroutine we need a coroutine builder (e.g. launch, asynchronous, runBlocking). A coroutine builder is an extension function on a coroutine scope that takes a coroutine context and a suspending lambda as parameters. The suspending Lambda is the block of code that executes the coroutine.

Here’s a basic example of a coroutine in Kotlin:

Code:

Output:

Explanation:

In the above code, a coroutine is initiated within the runBlocking builder, serving as the main function's coroutine scope. Inside this coroutine, a new asynchronous task is launched using the launch builder. The task introduces a deliberate one-second delay using delay(1000L) and then prints "World!" to the console. Simultaneously, outside the coroutine, the statement println("Hello") is executed without waiting for the asynchronous task to complete. This showcases the non-blocking nature of coroutines, allowing for concurrent execution of code.

Async-await in Kotlin

One of the coroutine builders that we can use to create and execute coroutines is async. The async function is an extension function on a coroutine scope, which takes a coroutine context and a suspending lambda as parameters. The suspending lambda is the block of code that will return a value when the coroutine is completed.

The async function returns a Deferred object, which is a subtype of Job. A Deferred object represents a future value that will be available when the coroutine gets completed. We can use the await function to get the value of the Deferred object, or the cancel function to cancel the coroutine.

The async function follows the principle of structured concurrency, which means that it will create a child coroutine within the current scope and job. This ensures that the coroutine will be canceled if the parent scope or job is canceled, or if the coroutine throws an exception.

By default, the async coroutine starts execution as soon as it is created. However, we can change this behavior by passing a CoroutineStart argument, such as CoroutineStart.LAZY, which will delay the execution until the await or start function gets called.

The async-await method is most commonly used to conduct multiple asynchronous operations concurrently while waiting for their results. For example, we can use the async-await to collect data from two different APIs and integrate their results.

Code:

Output:

Explanation:

In the above code, the calculateSumAsync function is a suspend function that takes two integers as parameters and returns a Deferred object. Inside this function, a coroutine is started using the async function. This coroutine simulates some asynchronous work by delaying for 1000 milliseconds (or 1 second) using the delay function. After the delay, it prints a message indicating that the asynchronous calculation is complete and then returns the sum of the two input integers.

The main function uses the runBlocking function to start a coroutine. Inside this coroutine, it starts two asynchronous computations by calling the calculateSumAsync function with different sets of input integers. These computations are started concurrently, meaning they run at the same time.

The await function is then called on each Deferred object returned by the calculateSumAsync function. This suspends the coroutine until the result of the computation is available. Once the result is available, it is printed to the console.

withContext in Kotlin

Coroutines can also be created and executed using the withContext method. The withContext function is a suspending function that accepts a coroutine context and a suspending lambda as parameters. The suspending lambda is the block of code that will return a value once the coroutine is completed.

The withContext function creates a new coroutine with the specified context and executes the suspending lambda within it. The function then suspends the existing coroutine until the new coroutine gets completed, and returns its result. The function also ensures that the new coroutine gets canceled if the current coroutine is canceled, or if the suspending lambda throws an exception.

The withContext method is most commonly used to switch between different dispatchers, such as switching from a background thread to the main thread, or vice versa. For example, we can use the withContext function to conduct heavy computation on a background thread, and then update the UI on the main thread.

Code:

Output:

Explanation:

In the above code, we have created a coroutine using runBlockingBuilder in our main function. A task is executed within this coroutine by calling the launch builder, which initiates a new coroutine. The performTask function, marked as suspend, is then invoked within this coroutine. Inside the performTask function, there is a demonstration of context switching using withContext(Dispatchers.IO). This allows a specific block of code to run on the IO dispatcher, simulating an IO-bound operation with a one-second delay. After this operation, the code switches back to the original dispatcher, likely the main dispatcher. The results of the operation are printed, indicating the start, progress, and completion of the task, along with the corresponding thread names for each phase. The entire process is encapsulated within the runBlocking coroutine, ensuring that the main function doesn't complete until all coroutines launched within it, including the one performing the task, have finished. The printed output demonstrates the interleaved execution of the coroutines, highlighting the asynchronous nature of the operations.

Async-await vs withContext in Kotlin

Some of the main differences between async-await and withContext in Kotlin are as follows:

FeatureAsync-awaitwithContext
Purposeasync-await is used when you want to execute multiple asynchronous operations and wait for their results.withContext is used when you want to switch between multiple dispatchers, such as switching from a background thread to the main thread.
Return Typeasync returns a Deferred object that holds the future result of the asynchronous operation.withContext does not return a specific result; it simply switches the context for the specified block of code. The result can be directly assigned or used within the block.
ConcurrencyIf we have multiple tasks that are independent of each other, we can use async to execute them concurrently.Does not inherently provide concurrency. It's more about changing the execution context.
Exception HandlingWith async, we have to catch the exceptions of the code block inside the async body. Otherwise, it can terminate the parent scope.withContext does not require explicit exception handling.
Suspend FunctionCan be used with or without a suspend function. Typically used with other suspending functions to perform asynchronous operations.Typically used within a suspending function, especially for IO-bound operations or other context-specific tasks.
Use caseSuitable for concurrent, independent tasks that can run in parallel. For example, making multiple network requests concurrently.Suitable for changing the context of a specific operation, such as moving to an IO dispatcher for blocking IO operations or to a custom thread pool.
Cancellation PropagationPropagates cancellation to all children. If one child gets canceled, then all others will also get canceled.Propagates cancellation only to the current block of code within withContext. The coroutine itself is not canceled.

Conclusion

  • Coroutines are a way of writing asynchronous and non-blocking code in a sequential and readable manner.
  • async-await and withContext are two methods that allow us to create and execute coroutines in Kotlin, but they have different purposes and characteristics.
  • The async-await creates a new coroutine for each task and returns a Deferred object that holds the future result of the task. It requires calling the await method on each Deferred object to get the result of the task.
  • The withContext is used to switch between different dispatchers within a single coroutine. It creates a new coroutine for the whole block and returns the block result as its return value. It does not require calling any method to get the result of the block, as it is returned directly by the function.
  • Async-await supports concurrency by executing tasks in parallel, allowing them to be awaited in any order. On the other hand, withContext does not support concurrency because the block is executed sequentially, suspending the original coroutine until the block gets completed.