In today’s world of modern applications, the demand for non-blocking or asynchronous programming has become paramount. The need to execute multiple tasks in parallel, all while ensuring that heavy operations don’t hinder the user interface (UI) thread, has led to the emergence of coroutines as a solution in modern programming languages. In particular, Kotlin has embraced coroutines as an alternative to traditional threads, offering a more streamlined approach to asynchronous execution.

Coroutine Features:

1. Lightweight and Memory-Efficient:

One of the standout features of coroutines is their lightweight nature. Unlike traditional threads, which can be resource-intensive due to the creation of separate stacks for each thread, coroutines make use of suspension. This suspension allows multiple coroutines to operate on a single thread without blocking it, thereby conserving memory and enabling concurrent operations without the overhead of thread creation.

2. Built-in Cancellation Support:

Coroutines come with a built-in mechanism for automatic cancellation. This feature ensures that when a coroutine hierarchy is in progress, any necessary cancellations can be triggered seamlessly. This not only simplifies the code but also mitigates potential memory leaks by managing the cancellation process more efficiently.

3. Structured Concurrency:

Coroutines embrace structured concurrency, which means they execute operations within a predefined scope. This approach not only enhances code organization but also helps in avoiding memory leaks by tying the lifecycle of coroutines to the scope in which they are launched.

4. Jetpack Integration: 

Coroutines seamlessly integrate with Jetpack libraries, providing extensive support for Android development. Many Jetpack libraries offer extensions that facilitate coroutine usage. Some even provide specialized coroutine scopes, making structured concurrency an integral part of modern Android app development.

5. Callback Elimination

When fetching data from one thread and passing it to another, traditional threading introduces tons of callbacks. These callbacks can significantly reduce the code’s readability and maintainability. Coroutines, on the other hand, eliminate the need for callbacks, resulting in cleaner and more comprehensible code.

6. Cost-Efficiency

Creating and managing threads can be an expensive operation due to the need for separate thread stacks. In contrast, creating coroutines is remarkably inexpensive, especially considering the performance gains they offer. Coroutines don’t have their own stacks, making them highly efficient in terms of resource utilization.

7. Suspendable vs. Blocking

Threads are inherently blocking, meaning that when a thread is paused (e.g., during a sleep operation), the entire thread is blocked, preventing it from executing any other tasks. Coroutines, however, are suspendable. This means that when a coroutine is delayed, it can yield control to other coroutines, allowing them to execute concurrently. This ability to suspend and resume tasks seamlessly enhances the overall responsiveness of an application.

8. Enhanced Concurrency

Coroutines provide a superior level of concurrency compared to traditional threads. Multiple threads often involve blocking and context switching, which can be slow and resource-intensive. In contrast, coroutines offer more efficient context switching, making them highly suitable for concurrent tasks. They can change context at any time, thanks to their suspendable nature, leading to improved performance.

9. Speed and Efficiency

Coroutines are not only lightweight but also incredibly fast. Threads are managed by the operating system, which introduces some overhead. In contrast, coroutines are managed by developers, allowing for fine-tuned control. Having thousands of coroutines working in harmony can outperform a smaller number of threads, demonstrating their speed and efficiency.

10. Understanding Coroutine Context

In Kotlin, every coroutine operates within a context represented by an instance of the CoroutineContext interface. This context defines the execution environment for the coroutine, including the thread it runs on. Here are some common coroutine contexts:

  • Dispatchers.Default: Suitable for CPU-intensive work, such as sorting a large list.
  • Dispatchers.Main: Used for the UI thread in Android applications, with specific configurations based on runtime dependencies.
  • Dispatchers.Unconfined: Allows coroutines to run without confinement to any specific thread.
  • Dispatchers.IO: Ideal for heavy I/O operations, such as long-running database queries.

Example:

CoroutineScope(Dispatchers.Main).launch {
  task1()
}
CoroutineScope(Dispatchers.Main).launch {
 task2()
}

Import following dependencies to build.gradle (app) level file

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:x.x.x"

Types of Coroutine Scopes

In Kotlin coroutines, scopes define the boundaries within which coroutines are executed. These scopes help determine the lifecycle of coroutines and provide a structured way to manage them. Coroutin is always call suspend method or other coroutine method.

There are three primary coroutine scopes:

1. Global Scope

Coroutines launched within the global scope exist for the duration of the application’s lifecycle. Once a coroutine completes its task, it is terminated. However, if a coroutine has unfinished work and the application is terminated, the coroutine will also be abruptly terminated. Let’s imagine a situation when the coroutines have some work or instruction left to do, and suddenly we end the application, then the coroutines will also die, as the maximum lifetime of the coroutine is equal to the lifetime of the application.

Example:

import ...

class MainActivity : AppCompatActivity() {
    val TAG = "Main Activity"
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        GlobalScope.launch {
            Log.d(TAG, Thread.currentThread().name.toString())
        }
        Log.d("Outside Global Scope",  Thread.currentThread().name.toString())
    }
}

2. Lifecycle Scope

The lifecycle scope is similar to the global scope, but with one crucial difference: coroutines launched within this scope are tied to the lifecycle of the activity. When the activity is destroyed, any coroutines associated with it are also terminated. This ensures that coroutines do not continue running unnecessarily after the activity’s demise.

Example:

import ...

const val TAG = "Main Activity"
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
           // launching the coroutine in the lifecycle scope
            lifecycleScope.launch {
                while (true) {
                    delay(1000L)
                    Log.d(TAG, "Still Running..")
                }
            }
 
            GlobalScope.launch {
                delay(5000L)
                val intent = Intent(this@MainActivity, SecondActivity::class.java)
                startActivity(intent)
                finish()
            }
    }
}

3. ViewModel Scope

The ViewMondel scope is akin to the lifecycle scope, but with a more extended lifespan. Coroutines lauched within this scope persist as long as the associated ViewModel is active. A ViewModel is a class that manages and stores UI-related data, making it a suitable scope for coroutines performing tasks tied to the ViewModel’s lifecycle.

Example:

import ...

class MyViewModel : ViewModel() {
  
    /**
     * Heavy operation that cannot be done in the Main Thread
     */
    fun launchDataLoad() {
        viewModelScope.launch {
            sortList()
            // Modify UI
        }
    }
  
    suspend fun sortList() = withContext(Dispatchers.Default) {
        // Heavy work
    }
}

Coroutines Functions

In Kotlin, two main functions are used to start coroutines:

  1. launch{ }
  2. async { }  

Using launch

The launch function is ideal when you need to perform an asynchronous task without blocking the main thread. It does not wait for the coroutine to complete and can be thought of as “fire and forget.”

Example:

private fun printSocialFollowers(){
        CoroutineScope(Dispatchers.IO).launch {
            var fb = getFBFollowers()
            var insta = getInstaFollowers()
            Log.i("Social count","FB - $fb, Insta - $insta")
        }
    }

    private suspend fun getFBFollowers(): Int{
        delay(1000)
        return 54
    }

    private suspend fun getInstaFollowers(): Int{
        delay(1000)
        return 113
    }

In the above example, getInstaFollowers() will wait for getFBFollowers() to finish executing, and after that, getFBFollowers() and getInstaFollowers() will be executed concurrently.

Using async

On the other hand, the async function is used when you require the result or output from your coroutine and are willing to wait for it. However, it’s important to note that async can block the main thread at the entry point of the await() function, so it should be used judiciously.

Here’s an example demonstrating the use of async:

private fun printSocialFollowers(){
        CoroutineScope(Dispatchers.IO).launch {
            var fb = async { getFBFollowers() }
            var insta = async { getInstaFollowers() }
            Log.i("Social count","FB - ${fb.await()}, Insta - ${insta.await()}")
        }
    }

    private suspend fun getFBFollowers(): Int{
        delay(1000)
        return 54
    }

    private suspend fun getInstaFollowers(): Int{
        delay(1000)
        return 113
    }

In the above example, both getFBFollowers() and getInstaFollowers() are called in parallel, reducing execution time compared to the launch function. However, it’s important to keep in mind that async should be used when you need the results and are prepared for potential blocking.

Conclusion

In conclusion, coroutines are a powerful tool for writing asynchronous code in a more readable and maintainable manner. By understanding the key concepts, how coroutines work, and how to use them effectively in practice, you can take advantage of the benefits they offer in modern programming. Whether you are a beginner or an experienced developer, this guide will provide you with the knowledge and resources to master coroutines and improve your programming skills.