What Coroutine
Simply put, Coroutine is a concurrent design pattern that allows you to solve asynchronous problems with much cleaner code.
For example, on Android it helps you solve two main problems:
- ANR occurs in the App because the main thread is blocked when a time-consuming task is executed in the main thread.
- Provides security for the main thread, as well as network callback and disk operation from the main thread.
I will give examples of how to solve these problems in the following articles.
Callback
Speaking of asynchrony, let’s take a look at our normal asynchrony approach. The first is the basic callback method.
The advantage of callback is that it is easy to use, but you may encounter some of the following situations while using it
GatheringVoiceSettingRepository.getInstance().getGeneralSettings(RequestLanguage::class.java)
.observe(this, { language ->
convertResult(language, { enable ->
// todo something})})Copy the code
It is possible to call one callback within another callback, or even more callbacks. The problem with these situations is that the level of nesting between code is too deep, resulting in complex nesting of logic and higher maintenance costs later on, which is not what we want to see.
So what’s the solution? Of course there is, and one of the solutions is the second one that I’m going to talk about.
Rx series
For multi-nested callbacks, the Rx family already handles this very well, such as RxJava. Let’s take a look at the RxJava solution
disposable = createCall().map {
// return RequestType
}.subscribeWith(object : SMDefaultDisposableObserver<RequestType>{
override fun onNext(t: RequestType) {
// todo something}})Copy the code
RxJava’s rich operators, combined with Observable and Subscribe, solve the problem of asynchronous nested callbacks. However, it is relatively expensive to use, and you need to know its operators well enough to avoid abusing or overusing them, which increases the natural complexity.
The solution we aspire to is simpler, more comprehensive, and more robust, and today’s topic, Coroutine, does just that.
The basics of Coroutine in Kotlin
In Android, we all know that network requests should be placed in child threads, and the corresponding callback is usually handled in the main thread, the UI thread. I won’t go into the normal writing, but what about Coroutine? Take a look at the following code example:
private suspend fun get(url: String) = withContext(Dispatchers.IO) {
// to do network request
url
}
private suspend fun fetch(a) { // call in Main
val result = get("https://rousetime.com") // call in IO
showToast(result) // call in Main
}
Copy the code
If the fetch method is called on the main thread, you’ll find that using a Coroutine to handle asynchronous callbacks is just like handling synchronous callbacks, with no more nested logic.
Notice how Coroutine enables this simple operation by adding two operations suspend and resume to resolve time-consuming tasks
- Suspend: Suspends the currently executing coroutine and saves all local variables at this point
- Resume: Resumes from where it was suspended, and the data saved at the time of suspension is restored
Suspend suspends a task so that it is temporarily out of the calling thread so that the current thread can resume other tasks. Once the suspended task has finished, resume it back into the current thread.
So the example above shows that while GET is still being requested, the fetch method will be suspended until get ends, at which point it will be inserted into the main thread and return the result.
A picture is worth a thousand words, and I made one that I hope will help.
Also note that the suspend method can only be called by other suspend methods or by a coroutine, such as launch.
Dispatchers
Coroutine, on the other hand, uses Dispatchers to schedule threads for the coordinator’s execution. This is similar to RxJava’s schedules, but Coroutine must be executed within Dispatchers schedules. Because Dispatchers will be responsible for the task where resume is suspended.
Dispatchers provides three modes of switching, respectively
- Dispatchers.Main: Enables the Coroutine to run the Main thread for UI operation
- Dispatchers.IO: Enables Coroutine to run on IO threads for network or I/O operations
- Dispatchers.Default: Improve CPU utilization outside of the main thread, such as sorting lists or parsing JSON.
Look again at the example above
private suspend fun get(url: String) = withContext(Dispatchers.IO) {
// to do network request
url
}
private suspend fun fetch(a) { // call in Main
val result = get("https://rousetime.com") // call in IO
showToast(result) // call in Main
}
Copy the code
To make get run on the IO thread, we use the withContext method, which is passed dispatchers. IO so that tasks in its closure are in the IO thread. WitchContext is also a suspend function.
Create a Coroutine
As mentioned above, the suspend function can only be called from the corresponding suspend or Coroutine. So how do you create a Coroutine?
There are two ways, launch and async respectively
- Launch: Launches a new Coroutine, but does not return results
- Async: Starts a new Coroutine, but returns the result
Again, if we need to perform the FETCH method, we can use launch to create a Coroutine
private fun excute(a) {
CoroutineScope(Dispatchers.Main).launch {
fetch()
}
}
Copy the code
Another async, because it returns a result, can use await or awaitAll if you want to wait for all async to finish
private suspend fun fetchAll(a) {
coroutineScope {
val deferredFirst = async { get("first")}val deferredSecond = async { get("second") }
deferredFirst.await()
deferredSecond.await()
// val deferred = listOf(
// async { get("first") },
// async { get("second") }
/ /)
// deferred.awaitAll()}}Copy the code
So with await or awaitAll you can guarantee that all async is complete before resume is called.
Architecture Components
If you use an Architecture Component, you can also use Coroutine on top of it, because Kotlin Coroutine already provides an API and customizes the CoroutineScope.
If you are not familiar with Architecture Components, I highly recommend reading my Android Architecture Components series
Before use, you need to update the dependent version of the Architecture Component, as shown below
object Versions {
const val arch_version = "2.2.0 - alpha01"
const val arch_room_version = "2.1.0 - rc01"
}
object Dependencies {
val arch_lifecycle = "androidx.lifecycle:lifecycle-extensions:${Versions.arch_version}"
val arch_viewmodel = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.arch_version}"
val arch_livedata = "androidx.lifecycle:lifecycle-livedata-ktx:${Versions.arch_version}"
val arch_runtime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.arch_version}"
val arch_room_runtime = "androidx.room:room-runtime:${Versions.arch_room_version}"
val arch_room_compiler = "androidx.room:room-compiler:${Versions.arch_room_version}"
val arch_room = "androidx.room:room-ktx:${Versions.arch_room_version}"
}
Copy the code
ViewModelScope
In the ViewModel, viewModelScope.launch is provided to enable Coroutine to be used, and the Coroutine is automatically cancelled once the ViewModel is cleared.
fun getAll(a) {
viewModelScope.launch {
val articleList = withContext(Dispatchers.IO) {
articleDao.getAll()
}
adapter.clear()
adapter.addAllData(articleList)
}
}
Copy the code
The IO thread uses the articleDao to fetch data from the database. Once the data is returned, it is processed in the main thread. If the ViewModel is cleared in the process of fetching data, fetching data is also stopped, preventing waste of resources.
LifecycleScope
For Lifecycle, LifecycleScope is provided and we can create Coroutine directly through launch
private fun coroutine(a) {
lifecycleScope.launch {
delay(2000)
showToast("coroutine first")
delay(2000)
showToast("coroutine second")}}Copy the code
Because Lifecycle is aware of the Lifecycle of a component, once the component onDestroy is done, the call in the corresponding LifecycleScope. Launch closure will also be cancelled.
LifecycleScope essentially Lifecycle. CoroutineScope
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
lifecycle.removeObserver(this)
coroutineContext.cancel()
}
}
Copy the code
It listens for the DESTROYED state in onStateChanged and cancels the Coroutine with a call to Cancel.
LifecycleScope, on the other hand, can also conduct suspend processing based on Lifecycle different life states. For example, do something special to its STARTED
private fun coroutine(a) {
lifecycleScope.launchWhenStarted {
}
lifecycleScope.launch {
whenStarted { }
delay(2000)
showToast("coroutine first")
delay(2000)
showToast("coroutine second")}}Copy the code
The same effect can be achieved by calling the launchWhenStarted directly or in launch.
LiveData
LiveData can be used directly from LiveData, where a suspend function is called as an argument and a LiveData object is returned
fun <T> liveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT,
@BuilderInference block: suspend LiveDataScope<T>. () -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)
Copy the code
So we can use liveData directly to implement Coroutine effect, let’s look at the following code
// Room
@Query("SELECT * FROM article_model WHERE title = :title LIMIT 1")
fun findByTitle(title: String): ArticleModel?
// ViewModel
fun findByTitle(title: String)= liveData(Dispatchers.IO) { MyApp.db.articleDao().findByTitle(title)? .let { emit(it) } }// Activity
private fun checkArticle(a) {
vm.findByTitle("Android Architecture Components Part1:Room").observe(this, Observer {
})
}
Copy the code
Fetch data from the database via title. The fetch takes place in the IO thread. Once the data is returned, the emit method is used to send the returned data. So at the View layer, we can use the checkArticle method directly to listen for the state of the data.
LiveData, on the other hand, has its active and inactive states, and Coroutine will be activated and canceled accordingly. Activation is not automatically activated if it has already completed or if an abnormal cancellation, such as a CancelationException, is thrown.
For sending data, you can also use emitSource, which has something in common with EMIT in that it clears the original data before sending new data, but differs from emit in that emitSource returns a DisposableHandle object, so that it can call its Dispose method to cancel sending.
I ended up writing a simple Demo using the Architecture Component and Coroutine, which you can check out on Github
Source code address: github.com/idisfkj/and…
Recommended reading
Android Architecture Components Part1:Room
Android Architecture Components Part2:LiveData
Android Architecture Components Part3:Lifecycle
Android Architecture Components Part4:ViewModel
The public,
Scan the QR code, follow the wechat official account, get the exclusive latest IT technology!