Introduction
There are two types of multitasking methods to manage multiple concurrent processes. In one type the OS controls the swich bewteen processes. The other type is called cooperative multitasking, in which processes control their behaviour by themselves.
Coroutines are software components that create subroutines for coporative multitasking. A coroutine can be introduced as a sequence of well managed sub tasks. To some extend, a coroutine can be considered as a light weight thread. You can execute many coroutines in a single thread. A coroutine can also be swiched between threads, and can be suspended from one thread and resumed from another thread.
In kotlin there’s a coroutine library since 1.3, which avoids us the need for RxJava, AsyncTask, Executors, HandlerThreads and IntentServices. The coroutines API allows us to write asynchronous code in a sequential manner.
Importance of coroutines
For a phone with a 90hz screen, our app has only 11ms (1000 ms / 90 refresh per second) to perform tasks in the Android main Thread, in between screen refreshes. By default android main thread has a set of regular responsabilities such as:
- always parse xml
- inflate view components
- draw them again and again for every refresh
- attend click events
If we add more tasks to the main thread, if its execution time exceeds this super small time gap between two refreshes, the app will show performance errors. The Screen might freeze. As a result of this, we should always implement long running tasks async in a separated thread.
Coroutines vs Threads
Couroutines are NOT the same as Threads. We have Main or UI Thread. We also have on demand back ground worker threads, but threads are not equal to coroutines. Any of those threads can have many coroutines running in it, at the same time.
Basic Implementation
We need to implement both core and Android dependencies.
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-RC2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0-RC2"
}
Let’s start with CoroutineScope
. This is an interface which provides the scope of the coroutine. We need to run the task in a background thread, so we provide Dispatchers.IO
. launch{}
is the coroutine builder.
btnDownload.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch {
downloadData()
}
}
The function downloadData()
will execute inside our new coroutine.
Coroutines Scope
In an App we can launch hundreds coroutines running at the same time. By default, coroutines don’t help us to keep track of them or any work that’s being done by them. If we don’t manage them carefully, there can be leaks in memory. To solve this, we must start all coroutines within a scope. Using the scope, we can easily keep track of coroutines, cancel them and handle errors or exceptions. We do this through the CoroutineScope
interface.
There’s another interface called GlobalScope
(very rarely used) which is used to launch top-level coroutines, which are operating on the whole application lifetime.
Coroutines Dispatchers
Both of these scopes also acts as a reference to the coroutine context. (Dispatchers.IO
). We can include the name of a Job instance, plus a Dispatcher
as the context of the job. This describes the kind of thread where the coroutine should be run. It’s recommended to start coroutines using Main Thread and then switch to Background Threads.
We have:
Dispatchers.Main
- coroutine will run in the Main Thread (also called UI Thread). There’s only one, so execute only lightweight tasks.Dispatchers.IO
- coroutine will run in a background thread from a shared pool of on-demand created threads. We use this to work with local database, networking or work with files.Dispatchers.Default
- for CPU intensive stasks such as sorting a large list.Dispatchers.Unconfined
- coroutine will run in the current thread, but if it’s suspended and resumed it will run on suspending function’s thread. This is used withGlobalScope
. It’s not recommended for Android Development.
We may as well create our own executors and dispatchers. Most of the time, we should use Main
and IO
.
Coroutines Builders
The following snippet .launch{}
is the coroutine builder. This are extension functions of coroutine scopes.
There are four main builders:
launch
- this launches a new coroutine without blocking the current thread. It returns an instance ofJob
, which can be used as a reference to the coroutine or to cancel it. We use this for coroutines that doesn’t have any result as the return value.async
- it’s used to get a result value. It allows us to launch coroutines in parallel without blocking the current thread. This returns an instance ofDeferred<T>
. We need to invokeawait()
to get the value. This are the ones we use most in Android Development.produce
- Is for coroutines which produces aStream
of elements. Returns an instance ofReceiveChannel
.runblocking
- Used mostly for testing. It will block the thread while the coroutine is executing. It returns a result of typeT
.
Switch the Thread of a Coroutine
In Android, we cannot directly call to a view
component running in the UI Thread from a Background Thread. This will crash with a CalledFromWrongThreadException
.
THIS WON’T WORK
btnDownload.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch {
downloadData()
}
}
// executed in a coroutine
private fun downloadData() {
for (i in 1..20000) {
// THIS WON'T WORK.
textView.text = "Downloading user $i in ${Thread.currentThread().name}"
}
}
Only the original thread that created a view hierarchy can touch its views, therefore we have to call views from the UI Thread. We add withContext()
and suspend
to the function’s args, as withContext
is a suspending function. We cannot call a suspending function from a normal function.
// executed in a coroutine
private suspend fun downloadData() {
withContext(Dispatchers.Main) {
for (i in 1..20000) {
textView.text = "Downloading user $i in ${Thread.currentThread().name}"
}
}
}
Suspending Functions
Whenever a coroutine is suspended, the current stack frame of ther function is copied and saved into memory. When the function resumes after completing its task, the stack frame is copied back from where it was saved and starts running again.
Other libraries such as room and retrofit, also provide suspending functions support.
These are some of those suspending functions: withContext
, withTimeout
, withTimeoutOrNull
, join
, delay
. If we’re going to call them from our own functions, we have to mark our own functions as suspend fun
. A suspending function can only be called from a coroutine block or from another suspending function. suspending means a function with a heavy long running task.
A suspending function doesn’t block a thread. It only suspends the coroutine itself. That thread is returned to the pool while the coroutine is waiting, and when the waiting is done the coroutine resumes on a free thread in the pool.
Async Builder and Await
Writing code to download data in parallel and combine them at the end is called parallel decomposition. To do it, we need to use async coroutine builder. This returns and instance of Deferred
. We can use it by invoking its await
function.
fun asyncTest() {
CoroutineScope(Main).launch {
val stock1 = async(IO) {getStock1()}
val stock2 = async(IO) {getStock2()}
val total = stock1.await() + stock2.await()
}
}
private suspend fun getStock1() {
// ...
}
Since this coroutine runs on the Main
Thread, we could add a Toast
message or interact with the view
.
Unstructured Concurrency
Unstructured concurrency does not guarantee to complete all the tasks of the suspending function, before it returns. There’re situations we need to launch multiple coroutines concurrently in a suspending function and get some result returned from the function.
This is the wrong way to do it!.
class UserDataManager {
suspend fun getTotalUserCount(): Int {
var count = 0
CoroutineScope(Dispatchers.IO).launch {
delay(1000)
count = 50
}
return count
}
}
This will return 0
instead of 50
, as it doesn’t wait for the async task. The child coroutines can be still running, even after the completion of the parent coroutine.
Structured Concurrency
Structured concurrency guarantees to complete all the work started by coroutines within the child scope before the return of the suspending function. It helps us to keep track of tasks we started and to cancel them when needed.
All previous problems can be solved using the suspending function coroutineScope
. It allows us to create a child scope, within a given coroutine scope. This guarantees the completion of the tasks when the suspending function returns.
class UserDataManager {
var count = 0
lateinit var deferred: Deferred<Int>
suspend fun getTotalUserCount(): Int {
coroutineScope {
launch {
delay(1000)
count = 50
}
}
deferred = async(IO) {
delay(3000)
return@async 70
}
return count + deferred.await()
}
}
Coroutines patterns with ViewModel and LiveData
For either ViewModelScope and LifeCycleScope we need to implement the following libraries.
def arch_version = '2.2.0-alpha04'
implementation 'androidx.lifecycle:lifecycle-extensions:$arch_version'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:$arch_version'
ViewModelScope
We have a ViewModel
class. We define the coroutine scope we want to use and the method where we call it from.
In Android everytime a ViewModel is cleared from memory, just before cleaning it invokes its onCleared()
method.
Some of the coroutines we launch in a ViewModel have the potential to run even after the ViewModel is cleared from memory. It might run until app termination. If this is not intended, we will end up leaking memory. To avoid this, we need to clear the coroutine.
class MainActivityViewModel: ViewModel() {
// allows us to control all coroutines launched in this scope
private val myJob = Job()
private val myScope = CoroutineScope(Dispatchers.IO + myJob)
fun getUserData() {
myScope.launch {
// write code.
}
}
override fun onCleared() {
super.onCleared()
// this is useful to cancel the coroutines for a couple of ViewModels
myJob.cancel()
}
}
If we have 20 ViewModels, there’s a better alternative with viewModelScope
as this may be a wasting of time. This is bounded to ViewModel’s lifecycle. It was created to automatically handle cancellation when the ViewModel’s onClear()
is called.
viewModelScope
is a CoroutineScope
tied to a ViewModel
. It needs the ktx dependency in gradle.
Now, for the previous ViewModel, we can simply use viewModelScope
instead of myScope
. Clearing will be done automatically if the ViewModel is cleared.
class MainActivityViewModel: ViewModel() {
fun getUserData() {
viewModelScope.launch {
// write code.
}
}
}
LifeCycleScope
A lifecycleScope
is defined for each Lifecycle Object. All coroutines launched in this scope are canceled when the Lifecycle
is destroyed. You can access the CoroutineScope
either via lifecycle.coroutineScope
or lifecycleOwner.lifecycleScope
. This scope is for objects with a lifecycle, such as Activities and Fragments.
If it’s in an Activity
or a Fragment
, all the coroutines will be canceled when the onDestroy()
method is called.
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// ...
lifecycleScope.launch {
// do whatever with coroutines.
}
}
}
Sometimes we might need to suspend execution of a code block, considering the current state of a lifecycle object. For that we have three additional builders.
lifecycleScope.launchWhenCreated
- if you have long running operations which should happen only once during the lifecycle of the activity or fragment. This will be called when it’s created for the first time.lifecycleScope.launchWhenStarted
- this will launch when the activity or fragment started. For some operations we need ourFragment
lifecycle to be at least started.lifecycleScope.launchWhenResumed
- this is the state in which the App interacts with the user.
LiveData Builder
Example Without LiveData Builder
We have the following data
class with two properties.
data class User(val id: Int, val name: String)
Then we create a repository class.
class UserRepository {
suspend fun getUsers(): List<User> {
// implement
}
}
We also need a ViewModel
class. with a viewModelScope
which launches a new coroutine. Then we have a withContext()
which switches the thread of the coroutine to a background thread.
class MainActivityViewModel: ViewModel() {
private var userRepository = UserRespository()
var users: MutableLiveData<List<User>> = MutableLiveData()
fun getUsers() {
viewModelScope.launch {
var result: List<User>? = null
withContext(Dispatchers.IO) {
result = userReppository.getUsers()
}
users.value = result
}
}
}
This is the MainActivity
class. We invoke the previous function and observe the list of users.
override fun onCreate(savedInstanceState: Bundle?) {
...
mainActivityViewModel = ViewModelProvider(this).get(MainActivityViewModel::class.java)
mainActivityViewModel.getUsers()
mainActivityViewModel.users.observe(this, Observer { myUsers ->
myUsers.forEach {
Log.i("MyTag", "name is ${it.name}")
}
})
}
With LiveData Builder
To use this we need to add the following dependency.
def arch_version = '2.2.0-alpha04'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$arch_version"
There’s a new block which will automatically execute when LiveData becomes active. It automatically decides when to stop executing and cancel the coroutines inside the building block considering the state of the lifecycle owner.
Inside the LiveData building block, you can use emit()
function to set a value to LiveData.
This is the previous LiveModel. Remember long running tasks need to be executed in a background thread.
class MainActivityViewModel: ViewModel() {
private var userRepository = UserRespository()
var users = liveData(Distpatchers.IO) {
val result = usersRepository.getUsers()
emit(result)
}
}
With this change, we don’t need to invoke the function in MainActivity
anymore.
override fun onCreate(savedInstanceState: Bundle?) {
...
mainActivityViewModel = ViewModelProvider(this).get(MainActivityViewModel::class.java)
mainActivityViewModel.users.observe(this, Observer { myUsers ->
myUsers.forEach {
Log.i("MyTag", "name is ${it.name}")
}
})
}