Database calls, network calls, heavy-computing calls, all are such functions that pretty much every app requires, all of which must be run asynchronously, usually in a background thread as these functions can take entire seconds to complete and you don’t want to block your main thread and freeze your app for that long.
The go-to way for handling asynchronous code in Android has changed plenty over the years, from the early days of async tasks, to the days of RxJava, but now we’re transitioning into the era of coroutines.
So what exactly are coroutines? Why are they becoming the new standard? And are they here to stay?
What are Coroutines?
Simply put, coroutines are a way to write asynchronous code as if it were synchronous code…
Well almost as if.
fun fetchQuote() { viewModelScope.launch { val quote = kanyeRepository.getQuote() quoteLiveData.postValue(quote) } }
You start them with Coroutine Builders such as launch
and once you do, you open up the asynchronous world. What does this mean exactly? You can call suspend functions.
Adding Coroutines to your Project
Before we proceed, if you want to follow along, you will need the dependency for it in your app/build.gradle
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
Suspend Functions
suspend fun getQuote() = kanyeApi .getQuote("https://api.kanye.rest/")
By adding the suspend
keyword, you can transform any regular function into a suspend function. Suspend function can work asynchronously as they have the ability to carry long running operations without blocking the current thread.
Suspend functions can only be called within other suspend functions, or inside coroutine builders like launch
.
Coroutine Builders
We mentioned these quite a few times already, but Coroutine Builders are what you use to start a coroutine. There are a few of them.
launch
viewModelScope.launch { val quote = kanyeRepository.getQuote() quoteLiveData.postValue(quote) }
launch
is the most basic of the coroutine builders. It works in a sort of fire-and-forget type thing. It executes whatever is contained in its lambda and leaves it at that. No return value and such.
runBlocking
runBlocking { val quote = kanyeRepository.getQuote() quoteLiveData.postValue(quote) }
runBlocking
starts a coroutine, but like the name implies, it runs the coroutine whilst blocking the current thread. It doesn’t need to be executed on a coroutine scope (which we’ll get into), and is used in tests more than anything, because running it in production code is like trying to ride a bike with no wheels.
async
suspend fun postQuote() { val result = viewModelScope.async { kanyeRepository.getQuote() } val quote = result.await() quoteLiveData.postValue(quote) }
async
is a coroutine builder that can be used when you want to get the result of a coroutine. async
itself returns a Deferred<T>
with the generic type being whatever’s found at the end of the coroutine. In the above example, kanyeRepository.getQuote()
returns a String
, so the async
above returns a Deferred<String>
.
Where this really has its use is that the Deferred
class has the await()
function, which is a suspend function itself (so it can only be called within builders or other suspend functions) which waits for the value without blocking the current thread.
In terms of usage, this differs from launch
in that you can separate the execution and result of a coroutine. launch
is a fire-and-forget type of builder, so everything needs to be executed within itself, while async
can, well, defer its result to other areas of code away from itself.
Coroutine Scopes
Apart from runBlocking
, coroutine builders are executed within a CoroutineScope
.
Why is this needed? The scope of a coroutine delimits its lifetime. This is the coroutine-way of being able to dispose of coroutines and avoiding memory leaks.
private val myScope = CoroutineScope(Dispatchers.IO) fun dispose() = myScope.cancel()
Not only does a scope allow you to cancel coroutines when required, but it also holds the context a coroutine is run on, which contains more technical information about a coroutine including the thread it gets dispatched on.
Coroutine Scopes follow a hierarchical parent-child sort of structure known as structured concurrency.
“An outer scope cannot complete until all its children coroutines complete”
Not only does this make it easier for a coroutine scope and its children to be marked as completed or cancelled, but it also ensures errors and exceptions in the code are properly reported.
Like the above example, you can create your own scope by passing in a CoroutineContext
, but in most user-facing apps, you’ll be using one of these special scopes that work well with Android Architecture Components:
- viewModelScope – a scope that can be used inside
ViewModel
classes which ties its lifecycle to the view model. In an MVVM structure, most data-driven coroutines are best launched from the view model after all. You’ll need the below dependency.implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
- lifecycleScope – a scope that can be used inside
Lifecycle
classes, which also include components such as Activities and Fragments, and gets cancelled when the lifecycle reaches its destroyed state.This is useful if you need to execute memory-intensive UI functions that wouldn’t fit well in your view model. You’ll need the below dependency.implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
Lifecycle scope also has the added benefit of being able to schedule coroutines to launch when specific lifecycle events are called.
lifecycleScope.launchWhenStarted
Coroutine Context
I mentioned earlier that a Coroutine Scope holds a CoroutineContext
, which defines technical information about a coroutine.
The most common use for defining this is to set the thread a coroutine is dispatched on.
private val ioContext: CoroutineContext = Dispatchers.IO
There are a number of dispatchers:
Dispatchers.Main – Runs on the Main/UI thread and thus, is the only thread where UI changes can be made without exception. Overpopulating this thread may decrease the app’s front-end performance and freeze the app, resulting in bad UX.
Dispatchers.Default – Appropriate for CPU intensive tasks. Backed by a shared pool of threads and limited by the number of CPU cores.
Dispatchers.IO – Best used for non-CPU intensive background tasks like network and database calls. Also backed by a shared pool of threads.
Dispatchers.Unconfined – Runs coroutines unconfined to any specific thread. Unconfined coroutines start in the current thread and resumes in any thread switched to in the coroutine function. (Ex. Calling delay will switch to the Default scheduler, thus the coroutine will resume there, even if it was started in the Main thread).
The dispatcher tied to a coroutine context defines the thread the coroutine is dispatched to by default, but you can also set a coroutine context at the point where the coroutine builder is used:
viewModelScope.launch(Dispatchers.IO) { deferQuote() }
Or you can use withContext
inside of a coroutine to switch contexts:
viewModelScope.launch(Dispatchers.IO) { doSomethingOnIo() withContext(Dispatchers.Main) { doSomethingOnMain() } }
By default, both viewModelScope
and lifecycleScope
dispatch on Dispatchers.Main.immediate
, so if you need to launch a coroutine on them on a thread that isn’t the main thread, you’ll need to explicitly set it.
Conclusion
That covers the basics of coroutines and everything you need to know to get started. By now, you should be able to comfortably start coroutines and dispose of them properly.
The world of coroutines is vast, and there’s not enough space on one article to cover even a quarter of it.
But I wish you the best and good luck in the beginning of your coroutine journey. Happy coding ༼ つ ◕_◕ ༽つ