Posts Android - Coroutines
Post
Cancel

Android - Coroutines

What is a coroutine

Coroutines are essentially jobs that execute within a thread. Coroutines are suspendable and can switch their context. They are important for thing like network calls without blocking the UI and generally for any parallel jobs.

To start with coroutines, first include them in the dependencies:

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

Note that version numbers may differ. The simplest way (not the best way) to start a coroutine is now:

1
2
3
GlobalScope.launch{
    // coroutine code
}

This will mean that our coroutine will live as long as the application does or until it is completed. Coroutines started from global scope will be started in a separate thread, so the code will be executed asynchronously:

1
2
3
GlobalScope.launch{
    Log.d("Main Activity", "Coroutine from thread: ${Thread.currentThread().name}")
}

We can not fully predict in which thread the coroutine will be launched, but it will be launched in another thread.

Similarly to a sleep function in threads, with coroutines we have delay:

1
2
3
4
GlobalScope.launch{
    delay(3000L)
    Log.d("MainActivity", "Coroutine from thread: ${Thread.currentThread().name}")
}

However, delay is very different to sleep in that it will only pause the current coroutine and will not block the whole thread. Another important thing to note is that all coroutines on all threads will be killed if the main thread finishes its work.

Suspend function calls

If we take a closer look at the implementation of delay, we can see that it is a suspend function. A special thing about suspend functions is that they can only be executed inside another suspend function or inside a coroutine. We can also of course right our own suspend function:

1
2
3
4
suspend fun mockNetworkCall(): String {
    delay(3000L)
    return "Answer of mock network call"
}

Let’s add another mock function:

1
2
3
4
suspend fun mockNetworkCall2(): String {
    delay(3000L)
    return "Answer of mock network call 2"
}

And execute these from a coroutine:

1
2
3
4
5
6
GlobalScope.launch{
    val networkAnswer1 = mockNetworkCall()
    val networkAnswer2 = mockNetworkCall2()
    Log.d("MainActivity", networkAnswer1)
    Log.d("MainActivity", networkAnswer2)
}

The log statements will now execute after 6 seconds, as both suspend functions affect the coroutine.

Coroutine context

Coroutines are always started in a specific context, which describes in which thread the coroutine will be started in. For now, we only used global scope, which didn’t give much control over this. We can actually pass a dispatcher to the function as parameter and specify a dispatcher:

  • Main
  • IO
  • Default
  • Unconfined

The main thread is very useful for changing the UI (as this can only be done from the main thread). IO is used for all kinds of data operations - files, network requests, databases etc. Default is good for long calculations to not block the main thread and the UI. Unconfined is not confined to a specific thread. We can slo just use our own thread by using newSingleThreadContext() and passing a name for the thread.

A really useful thing is that we can switch a coroutine context from the coroutine. For example, if we want to make a network call and display it on the UI, we can use switching context:

1
2
3
4
5
6
7
GlobalScope.launch(Dispatchers.IO){
    val networkAnswer1 = mockNetworkCall()
    // switch to main thread and update a text view
    withContext(Dispatchers.Main) {
        tv_mock.text = networkAnswer1
    }
}

Run Blocking

So, even if we call a delay in a coroutine, it will not actually block the thread it is running in. There is however a function that will start a coroutine in the main thread and block it, which is called runBlocking. The difference between this and a global scope launch with a main dispatcher, is that runBlocking will block the thread. For example, delay within runBlocking will block UI updates.

Why would we need to block the main thread? Well, if we have a suspend function and we want to use it, but not utilise a coroutine behavior, we can use runBlocking and mimic the function simple running on the main thread.

Another use is in testing, where we can use this to access suspend functions. We can also start a new coroutine inside a runBlocking scope:

1
2
3
4
5
6
runBlocking {
    launch(Dispatchers.IO){
        // IO code
    }
    // run blocking code
}

The launched coroutine will actually run asynchronously to the runBlocking scope. The IO thread here is not blocked.

Jobs

Whenever we launch a new coroutine it actually returns a job, which can be saved in a variable:

1
2
3
val job = GlobalScope.launch(Dispatchers.Default) {
    // job code
}

We can now, for example, wait for it to finish by using join:

1
2
3
4
5
6
val job = GlobalScope.launch(Dispatchers.Default) {
    // job code
}
runBlocking{
    job.join()
}

This will now actually block the main thread until the job is finished. We can also cancel a job using job.cancel:

1
2
3
4
5
6
val job = GlobalScope.launch(Dispatchers.Default) {
    // job code
}
runBlocking{
    job.cancel()
}

However, cancellation is cooperative, so the coroutine needs to be set up to be correctly canceled. There needs to be enough time to tell the coroutine that it has been canceled.

Async and Await

If we have several suspend functions and execute them both in the same coroutine, they are sequential by default. The first function will be executed first, second one second, and so on. However, if, for example, if we want to make two network calls then we actually don’t want to make the sequential, but rather we would like them to be asynchronous.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GlobalScope.launch(Dispatchers.IO) {
    val networkAnswer1 = mockNetworkCall()
    val networkAnswer2 = mockNetworkCall2()
    Log.d("MainActivity", networkAnswer1)
    Log.d("MainActivity", networkAnswer2)

}

suspend fun mockNetworkCall(): String {
    delay(3000L)
    return "Answer of mock network call"
}

suspend fun mockNetworkCall2(): String {
    delay(3000L)
    return "Answer of mock network call 2"
}

Currently, both functions delay the coroutine for 3 seconds and they will be executed in 6 seconds. However, we would like them to take 3 seconds. We could start a new coroutine for every call:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
GlobalScope.launch(Dispatchers.IO) {
    var networkAnswer1: String? = null
    var networkAnswer2: String? = null
    val job1 = launch{networkAnswer1 = mockNetworkCall()}
    val job2 = launch{networkAnswer2 = mockNetworkCall2()}
    job1.join()
    job2.join()
    Log.d("MainActivity", networkAnswer1)
    Log.d("MainActivity", networkAnswer2)

}

suspend fun mockNetworkCall(): String {
    delay(3000L)
    return "Answer of mock network call"
}

suspend fun mockNetworkCall2(): String {
    delay(3000L)
    return "Answer of mock network call 2"
}

This will actually now take 3 seconds, however the approach is very ugly and hacky. Instead, we can use async:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GlobalScope.launch(Dispatchers.IO) {
    val networkAnswer1 = async{mockNetworkCall()}
    val networkAnswer2 = async{mockNetworkCall2()}
    Log.d("MainActivity", ${networkAnswer1.await()})
    Log.d("MainActivity", ${networkAnswer2.await()})

}

suspend fun mockNetworkCall(): String {
    delay(3000L)
    return "Answer of mock network call"
}

suspend fun mockNetworkCall2(): String {
    delay(3000L)
    return "Answer of mock network call 2"
}

If we re-run this, this takes the same time, however the approach is much better.

lifecycleScope and viewModelScope

So far we have been using GlobalScope, which will stay alive as long as the application does. This in most cases is bad practice, as we rarely need such a long living coroutine. In android, there are two very useful scopes we can use, lifecycleScope and viewModelScope.

To add these we need some life cycle dependencies:

1
2
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$arch_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$arch_version"

Due to the fact that GlobalScope is tied to the application rather than an activity, it is quite prone to creating memory leaks. If the coroutine is using some resources in GlobalScope and the activity is destroyed, the coroutine will still be running, and the resources will not be garbage collected, causing a memory leak. To solve this problem, we can use lifecycleScope instead. This binds the coroutine to the life cycle of the activity, rather than the application.

Example of using firestore with coroutines

Let’s say we need to get data from users and some messages they wrote to each other to create a chat between them. In the past, this would be implemented using callbacks. The functions would run asynchronously and notify when the data is available:

1
2
3
4
5
6
7
getUser1{ user1 ->
    getUser2{ user2 ->
        getMessages{ messages ->
            // finally construct chat object
        }
    }
}

This is a super ugly callback maze. Luckily we have coroutines:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data class User(
    val name: String = "",
    val age: Int = -1
)

val mockDocument = Firebase.firestore.collection("data").document("mock")
val bob = User("Bob", 25)
LifecycleScope.launch(Dispatchers.IO){
    mockDocument.set(bob).await()
    val userBob = mockDocument.get().await().toObject(User)
    withContext(Dispatches.Main) {
        tvData.text = userBob.toString()
    }
}
This post is licensed under CC BY 4.0 by the author.

Recent Update

    Contents