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:

  1. ANR occurs in the App because the main thread is blocked when a time-consuming task is executed in the main thread.
  2. 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

  1. Dispatchers.Main: Enables the Coroutine to run the Main thread for UI operation
  2. Dispatchers.IO: Enables Coroutine to run on IO threads for network or I/O operations
  3. 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

  1. Launch: Launches a new Coroutine, but does not return results
  2. 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!