- Published on
Android Thread: Kotlin Coroutines Knowledge
- Authors
- Name
- Dan Tech
- @dan_0xff
In the series of articles on Threading in Android, I have covered many concepts: Android Thread, Looper, RxJava, Backpressure RxJava... Now it's time to explore a powerful new tool - Kotlin Coroutines.
Kotlin Coroutines?
Kotlin - Programming language; routine - schedule; co-routines - multiple schedules coordinating together
Kotlin Coroutines is a library written in Kotlin, which helps programmers use and manage asynchronous logic flows (schedules) effectively.
Concepts when using Kotlin Coroutines
CoroutineScope
Is the origin, the engine to trigger the launch of a Coroutine. Imagine CoroutineScope as a factory where Coroutines are created and their lifecycles are managed.
In a Kotlin application, you can create your own CoroutineScope or use GlobalScope - A CoroutineScope with application-wide scope. Be careful when using GlobalScope to avoid resource consumption and Memory Leaks.
In an Android application, you can create your own CoroutineScope for specific cases, use GlobalScope, or use the built-in CoroutineScopes that Android provides: lifecycleScope of Activity, Fragment; viewModelScope of ViewModel. These built-in CoroutineScopes have their own lifecycle, closely tied to their Components (Activity, Fragment, ViewModel), making it easier for you to manage memory and avoid wasting resources that can easily cause Memory Leaks.
val customCoroutineScope = CoroutineScope(EmptyCoroutineContext)
customCoroutineScope.launch {
Log.d("KtCoroutines", "Hello coroutines")
}
viewModelScope.launch {
Log.d("ViewModelScope", "Hello coroutines")
}
lifecycleScope.launch {
Log.d("LifecycleScope", "Hello coroutines")
}
Job
Job in Kotlin Coroutines represents a Coroutine created from CoroutineScope.
There are 2 types of Jobs commonly used by default in Kotlin Coroutines: Job and Deferred.
Similarities: Both are used to describe a Coroutine created by CoroutineScope. Contains data about the status of the running Coroutine. Can be cancelled when needed.
The biggest difference between Job and Deferred: Job describes a task that is executed but the return result is not important (fire and forget), Deferred is a task that is executed, and waits for the return result (fire and wait for result)
CoroutineContext, Dispatchers
CoroutineContext
Context: Background
CoroutineContext is understood as the context, the environment for executing a Coroutine.
CoroutineContext contains a lot of data: Dispatcher, Parent Job, Coroutine Name, etc. In the scope of this article, let's focus on Dispatchers.
Dispatchers
Is the one who decides which Thread Pool the Coroutine will be executed on. Note that this is a Thread Pool, not a Thread. Let's take the following example to demonstrate
viewModelScope.launch(Dispatchers.IO) {
while (true) {
Log.d("DispatchersTest", Thread.currentThread().name)
delay(1000)
}
}
You will observe that the Thread name printed on the screen will be changed, not kept the same as before. This proves that the resume of the Coroutine after suspending at the delay(1000) function has moved the logic of Log.d to a new Thread. However, this new Thread still belongs to the Thread Pool of Dispatchers.IO
Kotlin provides the application with some default Dispatchers that we can choose from.
- Dispatchers.Default: Is the Dispatcher used to create Coroutine Jobs that execute on the Background Thread. These tasks are recommended and regulated as CPU-bound operations: recursive calculations, bank interest, market volatility, etc.
- Dispatchers.IO: Is the Dispatcher used to create Coroutine Jobs that execute on the Background Thread environment. Tasks running on IO are recommended to be related to IO operations (file write/read execution, network, ...)
- Dispatchers.Main: Represents the main thread in the application. Coroutines executed by Dispatchers.Main always run on a single Main Thread.
- Dispatchers.Unconfined: In particular, Coroutine Jobs created by Unconfined will not be able to determine the environment in which they will be executed after resuming from a suspend. This means that at the time of resume, the Kotlin Coroutines management system will search for the nearest free Thread to continue execution, not limited to Dispatchers.IO, Dispatchers.Main, or Dispatchers.Default.
viewModelScope.launch(Dispatchers.Unconfined) {
while (true) {
Log.d("DispatchersTest", Thread.currentThread().name)
delay(1000)
}
}
Use the above example for the types of Dispatchers you want to use to better understand the Thread determination mechanism of Dispatchers.
One thing to note that no one tells you: Dispatchers.IO and Dispatchers.Default share some Threads in the ThreadPool. This helps to reduce the rate of janking when switching contexts and increases the ability to optimize performance. Source: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-i-o.html
Suspend Function
suspend function is a special type of function of Kotlin Coroutines. True to its name, a suspend function can be suspended (interrupted, stopped) and then resumed later to continue executing logic.
- suspend function can only be executed inside another suspend function.
fun normalFunction() {
delay(1000) // -> error
newSuspendFunction() // -> error
coroutineScope.launch {
newSuspendFunction() // -> not error
}
}
suspend fun newSuspendFunction() {
Log.d("Running new suspend function")
anotherSuspendFunction() // -> not error
}
suspend fun anotherSuspendFunction() {
delay(1000) // -> not error
}
val userApi: UserApi by lazy { get() }
// Real usecase của suspend function
suspend fun fetchFullUserProfile(userId: String): FullUserProfile {
val remoteUserProfile = async { userApi.getUser(userId) }.await()
val userSetting = async { userApi.getUserSetting(userId) }.await()
return FullUserProfile(remoteUserProfile, userSetting)
}
Compare Kotlin Coroutines and RxJava
The relationship between Kotlin Coroutines and RxJava (Interview question)