Kotlin Coroutines for Android Development

If you want to get started with Kotlin Coroutines for Android Development and want to master it, then this article is for you. In this tutorial, we are going to master the Kotlin Coroutines in Android by covering the following topics:

  • What are kotlin coroutines?
  • Suspending functions
  • Coroutine Scope
  • Dispatchers
  • withContext
  • Coroutine Builders

Before getting started, check out my other post on kotlin.

Beginning Android Development with Kotlin

Kotlin Scope Functions Explained [Example]

Kotlin High Order Functions and Lambdas Explained

koin | Dependency Injection In Android

Ktor|Networking Client For Android

MVVM with Kotlin Coroutines and Retrofit [Example]


What are kotlin coroutines?

A coroutine is a function that can pause its execution to be resumed later. You can think of coroutines as lightweight threads.

Kotlin Coroutines

Coroutines = Co + Routines

Here, Co means cooperation and Routines means functions. It means that when functions cooperate, we call it Coroutines.

  • They are light-weight.
  • Built-in cancellation support.
  • Lower chances for memory leaks, through structured concurrency.
  • Jetpack libraries provide coroutines support.

Adding coroutines to a project

To use coroutines in Kotlin, you must include the coroutines-core library in the build.gradle (Module: app) file of your project.

Coroutines on Android are available as a core library, and Android-specific extensions:

  • kotlinx-coroutines-core — Main interface for using coroutines in Kotlin
  • kotlinx-coroutines-android — Support for the Android Main thread in coroutines
dependencies {
  ...
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}

Suspending Functions

Suspend is a keyword that indicates that a function can be paused and resumed. Any function that is marked as suspend can only be called from another suspend function or a coroutine scope.

suspend function
private suspend fun printAllNames() {
        for (i in 1..2000) {
            println("Name $i")
        }
    }

As mentioned above, suspend function can be called from another suspend function or coroutine scope. let’s see how to call the suspend function using the coroutine scope.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d("MainActivity","Start - Main")

        GlobalScope.launch {
            printAllNames()
            Log.d("MainActivity","End - Scope")
        }

        Log.d("MainActivity","End - Main")

    }

    private suspend fun printAllNames() {
        delay(1000)
    }

Output

D/MainActivity: Start - Main
D/MainActivity: End - Main
D/MainActivity: End - Scope

Coroutine Scope

A CoroutineScope keeps track of any coroutine it creates using the builder functions launch or async. It provides the ability to cancel a coroutine at any point in time.

A coroutine cannot be launched without scope. CoroutineScope gets notified whenever a failure happens. Assuming that our activity is the scope, the background task should get canceled as soon as the activity is destroyed.

There are 3 scopes in Kotlin coroutines:

  • Global Scope
  • LifeCycle Scope
  • ViewModel Scope

Global Scope

When Coroutines are launched within the global scope, they live long as the application does. If the coroutines finish their job, they will be destroyed and will not keep alive until the application dies.

Coroutines launched in the global scope will be launched in a separate thread.

Check below, the example for launching a coroutine in a global scope and printing the current thread names to verify the global scope launched in the separate thread,

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        GlobalScope.launch {
            Log.d("Inside Global Scope", Thread.currentThread().name.toString())
        }
        Log.d("Outside Global Scope", Thread.currentThread().name.toString())

    }

Output

D/Outside Global Scope: main
D/Inside Global Scope: DefaultDispatcher-worker-1

LifeCycle Scope

The lifecycle scope is the same as the global scope, but the only difference is that when we use the lifecycle scope, all the coroutines launched within the activity also die when the activity dies.

Coroutines launched in the LifeCycle scope will be launched in the main thread.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        lifecycleScope.launch {
            Log.d("Inside Global Scope", Thread.currentThread().name.toString())
        }
        Log.d("Outside Global Scope", Thread.currentThread().name.toString())

    }

Output

D/Inside Life Cycle Scope: main
D/Outside Life Cycle Scope: main

ViewModel Scope

It is also the same as the lifecycle scope, the only difference is that the coroutine in this scope will live as long the view model is alive.

We need to add lifecycle-viewmodel-ktx dependency to the module-level build.gradle to access these ViewModel scopes.

implementation  'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'

Check out the below example to launch the viewModelScope from the ViewModel class.

class MainViewModel : ViewModel() {
    
    val users = MutableLiveData<User>()

    fun getAllUsers() {
        viewModelScope.launch {
            val usersData = fetchUsersFromApi()
            withContext(Dispatchers.Main) {
                users.postValue(usersData)
            }
        }
    }
}

Dispatchers

Dispatchers specify on which thread the operation should be performed.

Types of Dispatchers

There are majorly 4 types of Dispatchers.

  • Main Dispatcher(Dispatchers.Main)
  • IO Dispatcher(Dispatchers.IO)
  • Default Dispatcher(Dispatchers.Default )

Main Dispatcher

It starts the coroutine in the main thread. It is mostly used when we need to perform the UI operations within the coroutine.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        GlobalScope.launch(Dispatchers.Main) {
            Log.d("Inside GlobalScope", Thread.currentThread().name.toString())
            // perform UI related Operations
        }
        Log.d("Outside GlobalScope", Thread.currentThread().name.toString())
    }

Output

D/Outside GlobalScope: main
D/Inside GlobalScope: main

IO Dispatcher

It starts the coroutine in the IO thread, it is used to perform all the data operations such as networking, reading, or writing from the database, reading, or writing to the files. It will run on a different thread.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        GlobalScope.launch(Dispatchers.IO) {
            Log.d("Inside GlobalScope", Thread.currentThread().name.toString())
            // perform IO related Operations
        }
        Log.d("Outside GlobalScope", Thread.currentThread().name.toString())
    }

Output

D/Outside GlobalScope: main
D/Inside GlobalScope: DefaultDispatcher-worker-1

Default Dispatcher

The default CoroutineDispatcher is used by all coroutine builders like launch, async, etc if no dispatcher nor any other ContinuationInterceptor is specified in their context.

It is Optimized for CPU-intensive work off the main thread.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        GlobalScope.launch(Dispatchers.Default) {
            Log.d("Inside GlobalScope", Thread.currentThread().name.toString())
            // perform CPU intensive work
        }
        Log.d("Outside GlobalScope", Thread.currentThread().name.toString())
    }

Output

D/Outside GlobalScope: main
D/Inside GlobalScope: DefaultDispatcher-worker-1

withContext

Inside a coroutine, we use withContext to switch between Dispatchers. It’s nothing but a suspended function. It calls the specified suspending block with a given coroutine context, suspends until it completes, and returns the result.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        GlobalScope.launch {
            Log.d("Inside GlobalScope", Thread.currentThread().name.toString())
            withContext(Dispatchers.Main) {
                Log.d("Inside GlobalScope  withContext", Thread.currentThread().name.toString())
            }
        }
        Log.d("Outside GlobalScope", Thread.currentThread().name.toString())
    }

Output

D/Outside GlobalScope: main
D/Inside GlobalScope: DefaultDispatcher-worker-1
D/Inside GlobalScope  withContext: main

Coroutine Builders

There are multiple possible ways to create coroutines based on different requirements. In this section, let’s take a look at a few of them.

  • launch
  • async
  • runBlocking

Launch

It Launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a Job. The coroutine is canceled when the resulting job is canceled. The launch doesn’t return any result.

It is used in scenarios of “fire and forget” where we launched the work on a new thread but were not bothered about the result.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        GlobalScope.launch {
            Log.d("Inside GlobalScope", Thread.currentThread().name.toString())
        }
        Log.d("Outside GlobalScope", Thread.currentThread().name.toString())
    }

Async

With the async builder, we can start a new coroutine and it returns a result with a suspend function call await. The return type of async is Deferred. We can’t use async in a normal function as it has to call suspend function await to get the result. So we generally use launch builder inside a normal function and then use async inside it.

We need to generally use async only when we need the parallel execution of different tasks as it waits for all the tasks to be completed and returns a deferred result.

class MainActivity : AppCompatActivity() {
    
    val TAG = MainActivity::class.java.name

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)


        lifecycleScope.launch(Dispatchers.IO) {
            Log.d(TAG," lifecycleScope Thread " + Thread.currentThread().name)
            val firstNameDeferred = async {
                Log.d(TAG," getFirstName Thread " + Thread.currentThread().name)
                getFirstName()
            }
            val lastNameDeferred = async {
                Log.d(TAG," getLastName Thread " + Thread.currentThread().name)
                getLastName() }
            val fullName = firstNameDeferred.await() + lastNameDeferred.await()
            Log.d(TAG," End lifecycleScope Thread " + Thread.currentThread().name)
            Log.d(TAG, " End $fullName")

        }

    }

    suspend fun getFirstName() : String {
        delay(3000)
        return "Hello"
    }

    suspend fun getLastName() : String {
        delay(2000)
        return "World !"
    }
    
}

RunBlocking

runBlocking starts a new coroutine and blocks the current thread until its completion. It is designed to bridge regular blocking code and libraries that are written in suspending/non-blocking style.

you can use runBlocking to block the main application. So that it will not terminate until all the coroutines launched within the application are completed either successfully or exceptionally.

class MainActivity : AppCompatActivity() {

    val TAG = MainActivity::class.java.name

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        Log.d(TAG,"Before runblocking")
        runBlocking {
            delay(2000)
            Log.d(TAG,"Inside runblocking")
        }
        Log.d(TAG,"After runblocking")
    }
}

Launch vs Async in Kotlin Coroutines

LaunchAsync
The launch is fire and forget.Async is performing a task and return a result.
launch{} does not return anything.async{ }, which has an await() function that returns the result of the coroutine.
launch{} cannot be used when you need the parallel execution of network calls.Use async only when you need the parallel execution network calls.
launch{} will not block your main thread.Async will block the main thread at the entry point of the await() function. 
Execution of other parts  of the code will not wait for the launch result since launch is not a suspend callExecution of the other parts of the code will have to wait for the result of the await() function.
The launch can’t work like async in any case or condition.If you use async and do not wait for the result, it will work the same as the launch.
Launch can be used at places if you don’t need the result from the method called. Use async when you need the results from the multiple tasks that run in parallel. 
launch vs async

Conclusion

I hope you learned about the kotlin coroutines and you can able to perform parallel and series tasks. Thanks for reading.

Leave a Reply

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