In the world of software development, Kotlin has quickly risen to prominence, particularly in the realm of Android development. Known for its modern features and seamless interoperability with Java, Kotlin has become a favorite among developers for building robust, efficient applications. One of the standout features of Kotlin is coroutines, which offer a powerful way to handle asynchronous programming.
Problem
Imagine a scenario where we must fetch data from the backend, process it, and display it on the UI. It may sound effortless, but there’s a catch. The code we write for this seemingly straightforward task can lead to a significant issue. Let’s delve into this problem.
Can you spot the problem here?
We have a function called loadPosts(), which will fetch data and display the result on the UI. As fetchPosts() is synchronous, it means the underlying thread will be blocked. So if we call loadPosts() from UI or Main thread, it will block the thread until the operation finishes. As a result the system won’t be able to call onDraw() and we will see an unresponsive UI.
So how can we solve this problem?
Thread
We can solve this problem by switching threads. We will run the blocking or time-consuming call in a separate thread and update our UI for the main thread.
However, this code can be problematic for several reasons:
- There is no mechanism to cancel the threads.
- Creating and managing multiple threads can consume significant system resources. Threads have their memory stacks, which can lead to increased memory usage.
- Writing and managing thread-safe code is complex and error-prone.
Imagine a scenario where you quickly close a view. While opening, you might have started one or more threads to fetch and process data. So, without proper cancellation, the threads would still be processing data and trying to modify the view. A view that no longer exists. Such a scenario will be the reason for the infamous Memory Leaks.
Callbacks
Callbacks are one approach for solving similar problems. We initiate a process using a callback function. Once the callback function finishes, we invoke another function. Programs designed this way often avoid the overhead of having many threads constantly starting and stopping. Let’s write the same example using a callback function.
fetchPosts() can be executed in another thread, freeing the main thread to perform other work. However, this might only work for some cases. We might have multiple API calls, one after another, and the structure may get messy in this case.
Let’s look at a more complex case. We need to fetch user data and configuration along with fetchposts(). We can write the code in the following manner.
If you look at the code, increasing angle brackets can cause headaches. Let’s look at another one 😐
So, while callback temporarily solves our problem, it introduces other ones:
- Callback Hell. The increasing number of indentations makes the code hard to read and understand.
- Error Handling: Managing errors across multiple callbacks can be challenging and lead to less maintainable code.
- Flow Control: Handling complex asynchronous workflows and sequencing with callbacks can be complicated and unintuitive.
That’s why the callback architecture could be better for non trivial cases. Additionally, some people need help grasping callbacks. Their steep learning curve, cognitive load, and lack of extensibility make people look elsewhere for a solution to asynchronous programming, where reactive programming comes to life.
Rx and other Reactive Streams
Reactive Extensions (ReactiveX or Rx) is the Observer Pattern with a series of extensions. These extensions empower you to operate on the data. This approach ensures that all operations occur within a data stream that you can start, process, and observe. These streams support thread-switching and concurrent processing, so they are often used to parallelize application processing.
This code might look weird at first. But in reality, it’s a stream of data modified using several operators. But consider the following points:
- Steep Learning Curve: Reactive programming introduces a different way of thinking about data flow and event handling, which can be difficult for developers to learn and master.
- Complexity: While powerful, RxJava can lead to complex code that is hard to debug and understand, especially for those unfamiliar with reactive programming concepts.
- Performance Overhead: The abstraction layers in RxJava can introduce performance overhead, particularly in terms of memory and CPU usage, due to the creation and management of numerous observables and subscriptions.
Library Size: RxJava can significantly increase the size of your application, which may be a concern for mobile apps with strict size limitations.
Coroutines
Kotlin Coroutines, a powerful tool recommended by Kotlin for handling asynchronous code, empowers you with the core concept of suspendable computations. This unique capability allows a function to pause its execution at a certain point and resume later, giving you more control and flexibility in your code. With Kotlin Coroutines, you can handle asynchronous programming with ease and confidence!
Writing asynchronous or nonblocking code with Kotlin Coroutines is remarkably similar to writing synchronous or blocking code. The transition is so seamless that you wouldn’t even notice the difference, providing a sense of relief from the complexities of traditional asynchronous programming. With Kotlin Coroutines, you can write more efficient and streamlined code, making your development process smoother and more enjoyable!
Let’s write the same code using Coroutines.
If you look at the code snippet, you can see that it is identical to the blocking version of the code we wrote earlier. The only difference is the suspend keyword. The suspension mechanism in Kotlin acts as the foundation for all other coroutine concepts.
Coroutines are not a new concept, let alone invented by Kotlin. They’ve been around for decades and are prevalent in other programming languages like Go. Kotlin libraries handle most of the functionality based on their implementation. In fact, beyond the suspend keyword, no other keywords are added to the language, which is different from languages such as C#, which have async and await as part of the syntax. With Kotlin, these are just library functions.
With the simplicity and power of coroutines, you can easily combine multiple requests or transformations of data without complex operators or strange stream mapping to pass around. All you need to do is mark functions as suspendable and call them in a coroutine block. This straightforward approach not only simplifies your code but also opens up exciting possibilities for your coding practices, making Kotlin Coroutines a compelling choice for handling asynchronous programming.
Why Coroutines?
The JVM ecosystem provides the Thread abstraction for executing asynchronous computations. However, JVM threads are mapped directly to operating system (OS) threads, making them resource-intensive. The OS must allocate significant context information on the stack for each thread. Additionally, when a computation encounters a blocking operation, the underlying thread is paused, and the JVM must switch to another thread, reloading its context. This process of context-switching is expensive.
In contrast, coroutines operate at the user level and are not directly mapped to OS threads. Instead, they use simple objects known as continuations. Switching between coroutines merely involves changing the reference to the continuation object, eliminating the need for the OS to reload another thread’s context.
Another essential thing to note about coroutines is that they’re not threads. They are a low-level mechanism that utilizes thread pools to shuffle work between multiple existing threads, which allows you to create millions of coroutines without overflowing memory. A million threads would take so much memory that even today’s state-of-the-art computers would crash.
Programmers often call coroutines “Lightweight Threads“. Coroutines sometimes create new threads; they can reuse existing ones from thread pools.
We will discuss two important concepts here.
Suspend Modifier
Suspending functions are at the center of everything in coroutines. A suspending function is simply a function that can pause and resume itself later. It can execute a long-running operation and wait for it to complete without blocking.
The syntax of a suspending function is similar to that of a regular function except for adding the suspend keyword. It can take a parameter and have a return type. However, suspending functions can only be invoked by another suspending function or within a coroutine.
Structured Concurrency
Picture a bustling restaurant kitchen. Chefs furiously prepare multiple dishes simultaneously. Cooks meticulously coordinate the various steps for each dish, ensuring timely completion. The kitchen staff must ensure that:
- Chefs complete all steps for a dish before serving.
- When a problem strikes, such as a missing ingredient, chefs halt the dish’s preparation and reassign staff to other tasks.
Structured concurrency confines a coroutine’s lifespan to the scope of its launch, making coroutine management easier and reducing concerns about its lifetime and cancellation.
Structured concurrency ensures that systems recover coroutines, prevent leaks, and complete them once all child coroutines have finished. It also guarantees proper error reporting and prevents errors from being lost.
Which coroutines library is the most convenient?
How can you add Coroutines to your application? Does Coroutines come with the Kotlin language itself? The answer is both Yes and No. Kotlin language has built-in support for Coroutines, which provides minimalistic or essential elements. These could be more convenient for our day-to-day usage. On the other hand, Kotlin delivers a separate library for all the support for Coroutines. It’s vibrant, handy, and effortless to use, which is the Kotlinx.coroutines library
Let’s see some differences between the two.
Feature | Built-in Coroutines (Kotlin Standard Library) | kotlinx.coroutines Library |
Basic Coroutine Support | Yes | Yes |
Advanced Features | No | Yes |
CoroutineScope | Limited | Comprehensive support |
Job and Deferred | No | Yes |
Channels | No | Yes |
Flow API | No | Yes |
Dispatchers | No | Yes |
Structured Concurrency | No | Yes |
Error Handling | Basic | Enhanced |
Integration and Extensions | No | Yes |
To get the most out of it, you can use the kotlinx.coroutines library
That’s all for this part. In the next part, we will explore the crucial concepts and many more of Coroutines.