Coroutines | Pilove Notes

PiLove
The Startup
Published in
9 min readAug 14, 2020

Disclaimer: The goal of these notes is not to write an original commentary on this or any other topic, rather collect quotes and opinions from various sources and android experts in one place for better understanding!

On the JVM, you can expect each thread to be about 1MB in size. Each thread has it’s own stack. And lastly is the cost of thread scheduling, context switching, and CPU cache invalidation.

You can think of this like having multiple coroutines multiplexed on to a single thread. Coroutines have a small memory footprint — a few dozen bytes. That gives you a very high level of concurrency with very little overhead.

Android developers today have many async tools at hand. These include RxJava/Kotlin/Android, AsyncTasks, Jobs, Threads.

The documentation says Kotlin Coroutines are like lightweight threads. They are lightweight because creating coroutines doesn’t allocate new threads. Instead, they use predefined thread pools and smart scheduling.

Scheduling is the process of determining which piece of work you will execute next, just like a regular schedule.

Additionally, coroutines can be suspended and resumed mid-execution. This means you can have a long-running task, which you can execute little-by-little. You can pause it any number of times, and resume it when you’re ready again. Knowing this, creating a large number of Kotlin Coroutines won’t bring unnecessary memory overhead to your program. You’ll just suspend some of them until the thread pool frees up.

Also, you can easily change the context of the coroutine, and what that means is that if we think about threads as construction sites and coroutines as workers on the construction site, changing context means shifting a worker from one site to an another.

How To Use It (source: AndroidDevs)

First, let’s add dependencies:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5'

The simplest way to start a coroutine:

GlobalScope.launch { ... }

Every coroutine needs to start in a scope, and GlobalScope is a scope that will live as long as our app lives. Of course, if coroutine finishes its job it will be destroyed, but also it will be destroyed if the app is destroyed even though coroutine didn’t finish a task.

The code that is in GlobalScope will be launched in a different thread. Here is an example:

GlobalScope.launch {
Log.d(TAG, "Coroutine says hello from thread ${Thread.currentThread().name}")
}
Log.d(TAG, "Coroutine says hello from thread ${Thread.currentThread().name}")-----------Output---------
Coroutine says hello from thread main
Coroutine says hello from thread DefaultDispather-worker-2

Now, similar to Thread.sleep(2000L) for coroutines to be delayed we have delay(2000L) :

GlobalScope.launch {
delay(2000L)
Log.d(TAG, "Coroutine says hello from thread ${Thread.currentThread().name}")
}

Oppose to Thread.sleep() , delay() will not stop the whole thread that it belongs to, the same way that if one worker on construction site stops working the other workers will continue, but if we want for the whole construction site to stop working we would use Thread.sleep()

If we increase the delay in the previous example to 5000L and after we launch our app we immediately closed the app, the printing of the log that is from coroutine will not be executed. Why? Because if the main thread is finished, then all other thread launched inside the app will be also finished.

Suspend Functions

If you inspect delay() function you will see that her definitions if actually

public suspend fun delay(
timeMillis: Long
): Unit

Suspend function can only be executed inside a coroutine or inside another suspend function.

If we call two suspend functions one after the other in the same coroutine, the one will influence the other by delaying all executions by sums of both time delays, like in this example:

Coroutines Contexts

Coroutines are always started in a specific context and the context will describe in which thread our coroutine will be started in.

As for now, we only used GlobalScope.launch {} but what we can do is to pass a Dispatcher to gain more control over coroutines. So we can write the following:

GlobalScope.launch(Dispatchers.Main){} 

This will start coroutine in the main thread which is used for changes on UI that can only be changed from the main thread.

GlobalScope.launch(Dispatchers.IO){}

This is used for all kinds of data operations such as networking, writing to databases, reading or writing to files.

GlobalScope.launch(Dispatchers.Default){}

This is used for complex and long-running calculations that will block the main thread like for example sorting a huge number of elements.

GlobalScope.launch(Dispatchers.Unconfiend){}

So this coroutine is not confined to a specific thread so if you start a coroutine that calls a suspend function it will stay in a thread that suspend function resumed.

And we can also start a coroutine in our own thread:

GlobalScope.launch(newSingleThreadContext("MyThread")){}

But the really useful thing about coroutine context is that you can easily switch them within a coroutine:

GlobalScope.launch(Dispatchers.IO){
val answer = doNetworkCall()
withContext(Dispatchers.Main){
textView.text = answer //update UI

So withContext is used to switch context of a coroutine, and the thread that doNetworkCall() was executed is different than the thread that updated UI.

runBlocking()

It was previously elaborated that using a delay() won’t actually block the thread it was running in. However, runBlocking() will actually start a coroutine in the main thread and also block the main thread.

runBlocking { this: CoroutineScope
delay(100L) //this will block our UI updates
}

This can be useful if you don’t need whole coroutine behavior but need to call suspend functions in the main thread.

Because we are already in a CoroutineScope we can create a new coroutine with launch{} :

runBlocking { this: CoroutineScope
launch { this: CoroutineScope
}

Also, you can as well here use Dispatchers like:

launch(Discpatchers.IO) {}

Because this launch{} will actually run asynchronously to coroutine launched in the main thread (by runBlocking) this want be blocked:

Even though we have two delays time want be add up, so two logs will be printed at the same time after 3 seconds, and the situation where the first log will be printed after 3 seconds and the next log after 6 seconds will happen if the IO thread is blocked which in the example above is not the case.

Coroutine jobs and how to cancel it

Whenever we launch a new coroutine it returns a Job, which we can save in a variable.

val job = GlobalScope.launch(Dispatchers.Default) {}

With this job, we can for an example wait for it until it is finished, by using job.join() and this join() function is actually suspend function so we have to use coroutine scope to execute it.

val job = GlobalScope.launch(Dispatchers.Default) {}runBlocking { job.join() }

And this will actually block all threads until this job is finished.

We can also use job.cancel() to cancel our job.

So in the beginning, we launched a coroutine that takes 5 seconds to finish printing logs, and after 2 seconds we cancel this job.

Cancelation is not always as easy as in the previous example, because cancelation is cooperative which means that our coroutine needs to be set up to be correctly canceled. We have to have enough time to “tell” coroutine that it is canceled, and previously we did that with the help of delay() which is suspend function.

Here’s an example of another coroutine that cannot be canceled that easily:

If we run this code we will see that even though this job was canceled after 2 seconds, it will still continue to calculate fibonacci numbers between 30 and 40. The reason is that our coroutine is so busy with this calculation that is doesn’t have time to check for cancellations. If we did use suspend functions in coroutine then it would have enough time. So we have to do it manually:

for(i in 30..40) {
if(isActive) {
Log.d(TAG, "Result...")
...

For other long-running operations, coroutines have a function withTimeOut()

withTimeOut(3000L) {
for(i in 30..40)

So withTimeOut will execute the task and if the task takes longer than 3 seconds it will cancel it automatically.

Async and Await

If we have several suspend functions and execute them in a coroutine, they will be executed sequentially, but if we want to do it at the same time like for example two network calls we have to use Async.

Let’s look at the following example:

Output:

Answer1 is Answer 1
Answer2 is Answer 2
Time is 6000 ms

So we can see that because of the sequential execution of functions, line by line, the total time is summed-up which for network calls is not a good solution.

So how to solve this problem. Well, we could start a new coroutine for each function we execute, so let's do that:

Output:

Answer1 is Answer 1
Answer2 is Answer 2
Time is 3039 ms

We can see know that the request took only 3 seconds, but this approach was terrible. So how to do it properly? Instead of launch that returns job we can use async {} .

Another way of starting a coroutine is async {}. It is like launch {}, but returns an instance of Deferred<T>, which has an await() function that returns the result of the coroutine. Deferred<T> is a very basic future (fully-fledged JDK futures are also supported, but here we'll confine ourselves to Deferred for now). -kotlinlang.org

Function await() will block the current coroutine until the deferred value answer is available.

Output:

Answer1 is Answer 1
Answer2 is Answer 2
Time is 3039 ms

So we should always use async{} if we want to return some kind of value from coroutine. We could also write GlobalScope.async {} but in this example, it doesn’t make any sense because we don’t return anything from the main coroutine.

lifecycleScope & viewModelScope

These scopes are very useful ones. Previously mentioned, GlobalScope is used to start a coroutine that lives as long as the app does, however, most of the time it will be a bad practice to use GlobalScope because we rarely need a coroutine to be alive as long as our app.

For Android, there are two very useful predefined scopes which are lifecycleScope and viewModelScope. To add there we need to add some dependencies:

def arch_version = '2.2.0-alpha01' 
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$arch_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$arch_version"

Let’s examine another example:

So on the click of the button, we first start an infinite loop in a coroutine, and in a second coroutine, we start another activity after 5 seconds. If we run this code we will see that even though the second activity did start after 5 seconds, the infinite loop did not end after the main activity was destroyed. This is the main problem with GlobalScope. The infinite loop will stop only when the whole app is destroyed. This can cause memory leaks because if the coroutine started in the main activity, a uses the resources of the main activity, which is now destroyed, the resources won't be garbage collected because the coroutine still uses does resource. To solve this problem, we should change GlobalScope to lifecycleScope. This scope will bind a coroutine to a lifecycle of the activity, so if the activity is destroyed, the coroutine is also destroyed.

The same thing goes to viewModelScope, only that it will keep your coroutine alive as long as your viewModel is alive.

A ViewModelScope is defined for each ViewModel in your app. Any coroutine launched in this scope is automatically canceled if the ViewModel is cleared. Coroutines are useful here for when you have work that needs to be done only if the ViewModel is active. For example, if you are computing some data for a layout, you should scope the work to the ViewModel so that if the ViewModel is cleared, the work is canceled automatically to avoid consuming resources. -docs

Additional:

When using LiveData, you might need to calculate values asynchronously. For example, you might want to retrieve a user's preferences and serve them to your UI. In these cases, you can use the liveData builder function to call a suspend function, serving the result as a LiveData object.-docs

Coroutines with Firebase Firestore

To avoid “callback hell” we should consider using coroutines with Firebase.

implementation 'com.google.firebase:firebase-firestore-ktx:21.4.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.1.1'

Here’s an example:

So as we can see, by selecting a set() or get() function, they return a Task<DocumentSnapshot> and in that case, we can use suspend function await() to avoid having listeners and callbacks. This does not apply to snapshot listeners for real-time updates, only for tasks.

--

--