preface

The author struggled for a long time when writing this article, and did not know what form to explain kotlin coroutine knowledge. I used to study in the time, also whoring a variety of knowledge, read a lot of articles, can probably be summarized as three:

  • Speak too shallow, three or two words with the past, after watching only dull.
  • Talk too deep, from beginning to end are dizzy, the final summary is three sentences: where am I, what am I doing, the phone is really fun.
  • It’s fine, but when it comes to actual development you start rolling over and scratching your head: Why didn’t it turn out the way I expected

The learning of knowledge is like falling in love. If you want to get to the bottom of it, it’s very easy to roll over. But the conversation is too shallow, the feelings are not in place, the follow-up to in-depth after it is difficult. Without systematic learning, it is difficult to integrate the knowledge points learned in lectures, because the author’s idea is: “Let readers more easily absorb kotlin coroutine knowledge, and at the same time, can seamlessly connect to the practical application development.” Therefore, the author will explain each knowledge point to different depths according to different stages. As for whether the actual depth can meet the needs of readers, you can only experience it yourself.

The article gives an overview of

This article is aimed at objects that have a certain kotlin foundation and Android development foundation. I will explain this from a first point of view, creating projects from scratch. This paper mainly explains the basic use of Kotlin coroutine, project application and part of the coroutine principle knowledge. It will cover some of the basics of Kotlin, Android Jetpack components, common third-party frameworks, etc. (I won’t get into the fundamentals, just the basics). The paper is mainly divided into five levels:

  1. Basic usage of the Kotlin coroutine.
  2. Kotlin coroutine key points preliminary explanation.
  3. Develop Android applications using Kotlin coroutine.
  4. Kotlin coroutine combinationAndroid JetpackComponent applications.
  5. Use of Kotlin coroutines in conjunction with third-party frameworks such as:Retrofit.OkHttp.coilAnd so on.
  6. Dive into the Kotlin coroutine principle (e.g.CoroutineContext,Dispatcher,CoroutineExceptionHandler,Continuation,SchedulerEtc.)

Because the article involves only a lot of points, the content may be too long, according to their own ability level and familiarity with the stage jump read. If there is an incorrect place about the trouble you move small hand private letter to the author, very grateful

Due to time reasons, the author works during the day and can only write in the free time at night, so the update frequency should be once a week. Of course, I will try my best to make use of time and try to publish in advance. In order to facilitate reading, this article is divided into multiple chapters, and the corresponding chapters are selected according to their own needs. Now it is only a general catalog in the author’s mind, and the final update shall prevail:

  • Basic usage of Kotlin coroutines
  • Kotlin coroutine introduction to Android version in detail (ii) -> Kotlin coroutine key knowledge points preliminary explanation
  • Kotlin coroutine exception handling
  • Use Kotlin coroutine to develop Android applications
  • Network request encapsulation for Kotlin coroutines
  • [Kotlin Coroutine for Android (6) -> Kotlin coroutine combined with Jetpack and the use of third-party frameworks]
  • [Kotlin coroutine introduction for Android (7) -> In-depth Kotlin Coroutine principle (1)]
  • [Kotlin coroutine introduction for Android version in detail (8) -> In-depth Kotlin coroutine principle (2)]
  • [Kotlin coroutine introduction for Android version in detail (9) -> In-depth Kotlin coroutine principle (3)]
  • [Kotlin coroutine introduction for Android (10) -> In-depth Kotlin coroutine principle (4)]

Extend the series

  • Encapsulating DataBinding saves you thousands of lines of code
  • A ViewModel wrapper for everyday use

The main version number used in the article

  • Android studio 4.1.3
  • kotlin: 1.4.32
  • kotlinx-coroutines-android: 1.4.3
  • Retrofit: 2.9.0
  • okhttp: 4.9.0
  • coil: 1.2.0
  • room: 2.2.5

Project creation and configuration

First, we will use Android Studio (short for AS) to create a kotlin coroutinedemo. Then we will reference the following configuration in the build.gradle of the project

  classpath "Org. Jetbrains. Kotlin: kotlin - gradle - plugin: 1.4.32"
Copy the code

Then reference the configuration in your app’s build.gradle

    // Kotlin
    implementation "Org. Jetbrains. Kotlin: kotlin - stdlib: 1.4.32"
    // Coroutine core library
    implementation "Org. Jetbrains. Kotlinx: kotlinx coroutines -- core: 1.4.3"
    // Coroutine Android support library
    implementation "Org. Jetbrains. Kotlinx: kotlinx coroutines - android: 1.4.3"
}
Copy the code

Now we can happily begin the next step of our demo development. Remember to move a good small bench with melon seeds peanuts heart look

Basic introduction to Kotlin coroutines

For convenience, we refer to kotlin coroutines as coroutines in this paper.

What is a coroutine

Most people’s first thought when they hear coroutines is what coroutines are, and I’m not going to define them here. I recommend you go to kotlin’s official website for an explanation. But here’s the official quote:

  • Coroutines simplify asynchronous programming by putting complexity into libraries. The program logic can be expressed sequentially in coroutines, and the underlying library takes care of the asynchrony for us. The library can wrap relevant parts of user code as callbacks, subscribing to related events, and on different threads (or even different machines!) Schedule execution, and the code is kept as simple as sequential execution.

  • Coroutines are a concurrent design pattern that you can use on the Android platform to simplify code that executes asynchronously

The simple generalization is that we can write code that executes asynchronously in a synchronous manner. Coroutines are thread-dependent, but do not block threads when they are suspended, almost cost-free. So coroutines are like user-mode threads, very lightweight, you can create N coroutines in one thread. Coroutines are created using CoroutineScope. There are three ways to start coroutines:

  1. RunBlocking: TStart a new coroutine and block the calling thread until the code inside completes, returning a generic valueTWhat is the type of the last line in your coroutine body, and what type does it returnTIt’s what type.
  2. Launch: JobTo start a coroutine without blocking the calling thread, the coroutine scope (CoroutineScope), the return value is a Job.
  3. async:Deferred<T>To start a coroutine without blocking the calling thread, the coroutine scope (CoroutineScope). In order toDeferredObject in the form of a coroutine task. Return value genericTwithrunBlockingSimilar to the type of the last line of the coroutine body.

Wait, there’s something wrong, there’s a sudden increase in weird knowledge. What type is the last line in the coroutine body mentioned above, and what type is ultimately returnedTWhat type is it? It’s not what we think it is. Shouldn’t it be returnkotlinWill know, in thekotlinIn higher-order functions,lambdaExpression If you do not explicitly return a value, it implicitly returns the value of the last expression.

theJob,DeferredandCoroutine scopeWhat the hell is that again. Take your time. We’ll explain it one by one.

What are Job, Deferred, coroutine scopes

A Job can be thought of as a coroutine Job that is generated by coroutinescope.launch and runs a specific code block and completes when that block is complete. The current status of the Job can be obtained by isActive, isCompleted, or isCancelled. The following figure shows the status of the Job, which is extracted from the official document:

The life cycle of coroutines

State [isActive] [isCompleted] [isCancelled]
New (optional initial state) false false false
Active (default initial state) true false false
Completing (transient state) true false false
Cancelling (transient state) false false true
Cancelled (final state) false true true
Completed (final state) false true false

We can use the following figure to get an idea of the next coroutine Job from creation to completion or cancellation. Job is not extended here, and we will explain how to use it later.

                                      wait children
+-----+ start  +--------+ complete   +-------------+  finish  +-----------+
| New | -----> | Active | ---------> | Completing  | -------> | Completed |
+-----+        +--------+            +-------------+          +-----------+
                 |  cancel / fail       |
                 |     +----------------+
                 |     |
                 V     V
             +------------+                           finish  +-----------+
             | Cancelling | --------------------------------> | Cancelled |
             +------------+                                   +-----------+
Copy the code

Deferred

Deferred inherits from Job, which we can think of as a Job with a return value,

public interface Deferred<out T> : Job {
    // Returns the result value, or throws an appropriate exception if the delay is cancelled
    public suspend fun await(a): T  
    public val onAwait: SelectClause1<T>
    public fun getCompleted(a): T
    public fun getCompletionExceptionOrNull(a): Throwable?
}
Copy the code

We need to focus on the await() method. We can see that the await() method returns T, indicating that we can get the return value of the execution flow with the await() method. Of course, if an exception occurs or execution is cancelled, the corresponding exception will be thrown.

What is scope

Coroutine Scope is the Scope within which a Coroutine runs. Launch and async are extensions of CoroutineScope, which defines the scope of a newly launched coroutine and inherits its coroutineContext to automatically propagate all its elements and cancel operations. In other words, if the scope is destroyed, so is the coroutine inside it. Consider the scope of a variable, such as the money variable in the test method below

private fun test(){ // scope start
    int money = 100;
    println(money)
} // scope end
// println(money)  
Copy the code

In this case, money cannot be invoked, because THE AS prompt Unresolved Reference: money. Coroutine scope is also used to ensure that all coroutines have a scope limit. We develop the process of the most common scenario on memory leaks, coroutine with such problems, and then we will elaborate on CoroutineScope CoroutineScope related knowledge, here is just a basic point to explain, do not continue to extend.

Basic usage of the Kotlin coroutine

To start using coroutines, first we create a new Button in the XML layout of the MainActivity and set up the click event. Then we create a start() method that executes through the Button’s click event. Now we start using coroutines in the start method.

We can use runBlocking, launch, and async to start a coroutine, but we can only launch a coroutine in the scope of a coroutine.

Run the first coroutine

In Android, there is a global top-level coroutine called GlobalScope that runs throughout the application life cycle. We use this coroutine to launch async and launch launch as follows:

import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.Group
import androidx.viewpager.widget.ViewPager
import kotlinx.coroutines.*
import java.lang.NullPointerException

class MainActivity : AppCompatActivity() {
    private lateinit var btn:Button
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) btn = findViewById(R.id.btn) btn.setOnClickListener  { start() } }private fun start(a){
        runBlocking {
            Log.d("runBlocking"."Start a coroutine")
        }
        GlobalScope.launch{
            Log.d("launch"."Start a coroutine")
        }
        GlobalScope.async{
            Log.d("async"."Start a coroutine")}}}Copy the code

Then run the app and click the button to execute the Start () method. We can see the following output on the console:

D/runBlocking: launch a coroutine D/launch: launch a coroutine D/async: launch a coroutineCopy the code

Wow!, so easy. The coroutine was that simple, so let’s keep going. As mentioned above, each of the three startup methods will get its own return message. We now add three variables and assign each with a coroutine while printing:

private fun start(a){
    val runBlockingJob = runBlocking {
        Log.d("runBlocking"."Start a coroutine")
    }
    Log.d("runBlockingJob"."$runBlockingJob")
    val launchJob = GlobalScope.launch{
        Log.d("launch"."Start a coroutine")
    }
    Log.d("launchJob"."$launchJob")
    val asyncJob = GlobalScope.async{
        Log.d("async"."Start a coroutine")
        "I'm the return value."
    }
    Log.d("asyncJob"."$asyncJob")}Copy the code

Then run it and we can see the following output on the console:

D/runBlocking: Start a coroutine D/runBlockingJob:41D/launchJob: StandaloneCoroutine{Active}@3b8b871 D/launch: launch a coroutine D/async: launch a coroutine D/asyncJob: DeferredCoroutine{Active}@63f2656Copy the code

It could be

D/runBlocking: Start a coroutine D/runBlockingJob:41
D/launchJob: StandaloneCoroutine{Active}@1344515D/asyncJob: DeferredCoroutine{Active}@38c002a D/async: start a coroutine D/launch: Start a coroutineCopy the code

And it could be

D/runBlocking: Start a coroutine D/runBlockingJob:41D/launchJob: StandaloneCoroutine{Active}@b94e973D/ Async: start a coroutine D/ asyncJob: DeferredCoroutine{Active}@f7aa030
Copy the code

Mm-hmm. What’s going onThe order of the last four logs is still random. Do not understand the children’s shoes, that you did not carefully read the text. Knowledge points come, quickly take out your little book to write down, we one by one to analyze.

As mentioned above, runBlocking starts a new coroutine and blocks the calling thread. Comparing the output log, we can see that the position order of the previous two runBlocking output logs does not change, which shows that runBlocking blocks the calling thread. Do not continue executing until the runBlocking run ends.

Moving on, we see that the next four logs are unordered, but launchJob always precedes asyncJob. The log output in launch and Async coroutines is unordered. Each execution may see the order differently from the previous one. As mentioned earlier, launch and async both start a coroutine but do not block the calling thread, so launchJob always precedes asyncJob (it is not obvious between 2 coroutines, you can start 5 or more coroutines simultaneously to see the log output if you try it yourself).

The log in the coroutine body of launch and Async is disordered, because the coroutine adopts the concurrent design mode, so the log output in the coroutine body of launch and async is disordered, which explains that launch and async both start a coroutine without blocking the calling thread. It also explains the relationship between the output order of logs (described here loosely and will be added later).

Is this the end of it? Well, then you’re thinking too easy. We mentioned that coroutines are designed in a concurrent fashion, where multiple coroutines are executed concurrently. So what if at this point in time, we start the coroutine in the same coroutine scope and we start it in the same order? You can think about that for a moment, and we’ll come back to that.

Return value of runBlocking

The runBlockingJob output is 41. By default, it returns the current state of the coroutine job

We’re throughrunBlockingMethod as you can see, its return value is calledjoinBlockingMethod, while injoinBlockingIn the method

We see that the joinBlocking method returns a state strongly cast to the generic T type. We now have a rough idea of what runBlocking returns. If you add a return value to the last line of the runBlocking coroutine:

 val runBlockingJob = runBlocking {
        Log.d("Coroutine"."RunBlocking starts a coroutine")
        "I'm the return value of the runBlockingJob coroutine."
 }
Copy the code

We should see the following output:

D/runBlockingJob: I am the return value of the runBlockingJob CoroutineCopy the code

RunBlocking is designed to connect regular blocking code together and is used primarily in main functions and tests. Based on the goals of this article we will not expand further.

Moving on, we see that launchJob outputs a StandaloneCoroutine object. Why is it a StandaloneCoroutine object?

Don’t panic, hold on! Keep reading

Launch function

The launch function has three parameters: context, start, and block, all with default values. Although we don’t know what the three parameters are for, we can make a bold guess. We’ll skip them here and do some basic explanations for them later. We see that the launch method eventually returns a Coroutine object, and since we passed no value it returns a StandaloneCoroutine object, which is consistent with the log output. Why do I say launch returns a Job? If we look at StandaloneCoroutine, we can see that StandaloneCoroutine is a Job.

private open class StandaloneCoroutine(...). : AbstractCoroutine<Unit>(parentContext, active){
    //..... is omitted
}
Copy the code
public abstract class AbstractCoroutine<in T>(...). : JobSupport(active), Job, Continuation<T>, CoroutineScope {//..... is omitted
}
Copy the code

Async function

Let’s do the same thingasyncFunction, andlaunchIt has the same three parameterscontext,startandblock, the default value is the same, and the final return is also onecoroutineObject. justasyncThe returnedDeferredCoroutineObject.

private open class DeferredCoroutine<T>(...). : AbstractCoroutine<T>(parentContext, active), Deferred<T>, SelectClause1<T> {//..... is omitted
}
Copy the code

AbstractCoroutine

class, but DeferredCoroutine also inherits the Deferred

interface. DeferredCoroutine is a Deferred

, a Job with a return value. So how do we get the return value T of the Deferred

?



We mentioned at the beginning that we need to focus on the Deferred await() method. We can get the return value by returning the Deferred object and calling the await() method. We see the suspend keyword in front of await().

  public suspend fun await(a): T
Copy the code

Hang up function

Suspend is the keyword of the coroutine. It represents a suspended function. Each method invoked by suspend can only be called in the suspend method or in the coroutine. Now let’s modify the previous code and add a few more output logs:

private fun start(a){
    GlobalScope.launch{
        val launchJob = launch{
            Log.d("launch"."Start a coroutine")
        }
        Log.d("launchJob"."$launchJob")
        val asyncJob = async{
            Log.d("async"."Start a coroutine")
            "I'm async return value"
        }
        Log.d("asyncJob.await".":${asyncJob.await()}")
        Log.d("asyncJob"."$asyncJob")}}Copy the code

We are now launching one coroutine via GlobalScope.launch and launching two more coroutines directly within the coroutine body via launch. Why didn’t we launch globalScope. launch in the coroutine body instead of launching directly with launch? As mentioned earlier, launch must be called within the Coroutine Scope. Since the body of a Coroutine launched via runBlocking, launch, and async is the same as the Coroutine Scope, we can launch a Coroutine directly using launch. Let’s run it and look at the log output:

D/launchJob: StandaloneCoroutine{Active}@f3d8da3D/async: start a coroutine D/async: start a coroutine D/await: : I am async return value D/asyncJob: DeferredCoroutine{Completed}@d6f28a0
Copy the code

It could be

D/launchJob: StandaloneCoroutine{Active}@f3d8da3D/async: start a coroutine D/launch: Start a coroutine D/asyncJob. Await: : I am async return value D/asyncJob: DeferredCoroutine{Completed}@d6f28a0
Copy the code

Now we see that asyncJob. Await also outputs the return value we defined earlier, and the state of DeferredCoroutine changes to {Completed}, This is because await() waits for the value to complete without blocking the thread and continues execution, returning the result value when the deferred calculation is complete, or throwing a corresponding CancellationException if the Deferred is cancelled. But since await() is a pending function, it will suspend calling its coroutine. So we see that the status of the DeferredCoroutine is {Completed}, and the output of the await log is also at the end.

All right, so far. That concludes our introduction to runBlocking, launch, and async.

Coroutine concurrency and synchronization in Android

Now let’s go back to what we said above: “Because of the concurrent design pattern of coroutines, the log output in the coroutine body that causes launch and Async is out of order.”

Since coroutines use the concurrent design pattern, this statement is fine in most situations. But, but, but, here’s the little detail. The subcoroutines in a coroutine are executed synchronously if they:

  • The coroutine scheduler of the parent coroutine is inDispatchers.MainCase start.
  • At the same time, the subcoroutine starts without modifying the coroutine scheduler.
private fun start(a) {
    GlobalScope.launch(Dispatchers.Main) {
        for (index in 1 until  10) {
            // Synchronize execution
            launch {
                Log.d("launch$index"."Start a coroutine")}}}}Copy the code
D/ Launch1: Start a coroutine D/ Launch2: Start a coroutine D/ Launch3: Start a coroutine D/ Launch4: Start a coroutine D/ Launch5: Start a coroutine D/ Launch6: Start a coroutine D/ Launch7: start a coroutine D/ Launch7: Launch a coroutine D/ Launch8: Launch a coroutine D/ Launch9: Launch a coroutineCopy the code
private fun start(a) {
    GlobalScope.launch {
        for (index in 1 until  10) {
            // Execute concurrently
            launch {
                Log.d("launch$index"."Start a coroutine")}}}}Copy the code
D/ Launch1: Start a coroutine D/ Launch2: Start a coroutine D/ Launch3: Start a coroutine D/ Launch4: Start a coroutine D/ Launch5: Start a coroutine D/ Launch6: Start a coroutine D/ Launch9: Launch a coroutine D/ Launch7: Launch a coroutine D/ Launch8: Launch a coroutineCopy the code

Then the subcoroutine will be executed synchronously, which is on the Android platform if the coroutine is inDispatchers.MainThe scheduler, which schedules the coroutine to be executed in a UI event loop, usually on the main thread, so you can see why it’s synchronous. If it is out of sync, then I will have all kinds of problems when I perform UI refresh.

If one of the child coroutines changes its coroutine scheduler toThe Dispatchers. The MainThis subcoroutine will be executed concurrently with other subcoroutines. We won’t demonstrate it here, but you can try it out for yourself. After all, it is difficult to absorb knowledge in place without doing anything.

In the next chapter, we will give a preliminary explanation of the following points, including the functions of the three parameters in the launch and async functions mentioned above. Here’s the list:

  1. Coroutine schedulerCoroutineDispatcher
  2. Coroutines belowCoroutineContextrole
  3. Coroutine start modeCoroutineStart
  4. Coroutine scopeCoroutineScope
  5. Suspend functions as wellsuspendThe role of keywords

Need source code to see here: Demo source

Originality is not easy. If you like this article, you can click “like”.

Related articles

  • Basic usage of Kotlin coroutines
  • Kotlin coroutine introduction to Android version in detail (ii) -> Kotlin coroutine key knowledge points preliminary explanation
  • Kotlin coroutine exception handling
  • Use Kotlin coroutine to develop Android applications
  • Network request encapsulation for Kotlin coroutines

Extend the series

  • Encapsulating DataBinding saves you thousands of lines of code
  • A ViewModel wrapper for everyday use