The most detailed Android version of Kotlin coroutine entry advanced actual combat in history (1)

The most detailed Android version of Kotlin coroutine entry advanced actual combat in history (1)

Preface

The author struggled for a long time when writing this article, and I don t know how to explain it.

kotlin
Knowledge of coroutines. When I was studying before, the author also used various knowledge about prostitution and read many articles, which can probably be summarized into three types:

  • The words were too shallow, so I took a few sentences with them, and after reading them, nothing was left.
  • The talk was too deep, and it was dizzy from beginning to end. The final summary is three sentences: Where am I, what am I doing, mobile phones are really fun.
  • The content is moderate, but when used in actual development, all kinds of sudden rollovers began, and then scratching my head: Why is the result different from what I imagined?

The learning process of knowledge is like falling in love. It is gradual and orderly. If you just want to know more about it, there is a high probability that it will be easy to overturn. But the chat is too shallow, the feelings are not in place, and it is difficult to follow up after thinking about it. Without systematic learning, it is difficult to talk about the knowledge points learned thoroughly, because the author's idea is:"

Allow readers to more easily absorb the knowledge of kotlin coroutines, while being able to seamlessly connect to actual application development
". So for the next explanation of each knowledge point, the author will explain it to different depths according to different stages. As for whether this actual depth can meet the needs of readers, you can only experience it by yourself.

Article overview

The object of this article is to have a certain kotlin foundation and the foundation of Android development. The author will start from the first point of view, create a project from scratch to explain. The article is mainly to explain the basic use of Kotlin coroutines, project applications and some knowledge of coroutine principles. It will also explain some knowledge of Kotlin, Android Jetpack components, the basic use of commonly used third-party frameworks, etc.

(Will not go into the principle, just basic use)
. The article is mainly divided into 5 levels:

  1. Basic usage of kotlin coroutine.
  2. A preliminary explanation of the key knowledge points of kotlin coroutines.
  3. Use kotlin coroutine to develop Android applications.
  4. kotlin coroutine combination
    Android Jetpack
    Component application.
  5. The use of kotlin coroutines combined with third-party frameworks, such as:
    Retrofit
    ,
    OkHttp
    ,
    coil
    Wait.
  6. In-depth kotlin coroutine principles (such as:
    CoroutineContext
    ,
    Dispatcher
    ,
    CoroutineExceptionHandler
    ,
    Continuation
    ,
    Scheduler
    and many more)

Since the article involves a lot of points and the content may be too long, you can jump through it in stages according to your ability and familiarity. If there is any incorrect description, please send your little hands to private messages to the author, thank you very much

Due to time, the author can only write during the day and work in the evening, so the update frequency should be one article a week. Of course, I will try my best to use the time and strive to publish in advance. In order to facilitate reading, split this article into multiple chapters, and select the corresponding chapters according to your needs. Now it is only a rough catalog in the author's mind at present, and the update shall prevail:

Extended series

Major 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

Let's start to get into the topic. 1. we use Android studio (hereinafter abbreviated as AS) to create an engineering project with Kotlin as the development language.

KotlinCoroutineDemo
And then we first
project
of
build.gradle
Refer to the following configuration

the CLASSPATH "org.jetbrains.kotlin: Kotlin-Gradle-plugin: 1.4.32" Copy Code

Then in the app

build.gradle
Reference related configuration

//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 code

Now we can happily start our next demo development. Remember to move the small bench and bring melon seeds and peanuts to watch

Basic introduction to Kotlin coroutines

For the convenience of this article, we refer to Kotlin coroutines as coroutines (

Coroutine
).

What is a coroutine

When many people hear about coroutine, the first thought is what coroutine is. I will not define it here. I recommend that you take the official website of Kotlin to find related explanations. But here we quote the official original sentence:

  • Coroutines simplify asynchronous programming by putting complexity into libraries. The logic of the program can be expressed sequentially in the coroutine, and the underlying library will solve its asynchrony for us. The library can package relevant parts of user code as callbacks, subscribe to related events, and schedule execution on different threads (or even different machines!), while the code remains as simple as sequential execution.

  • Coroutine is a concurrent design pattern, you can use it on the Android platform to simplify the asynchronous execution of the code

The simple summary is that we can write code that executes asynchronously in a synchronous manner. The coroutine is dependent on the thread, but the coroutine does not need to block the thread when it is suspended, which is almost costless. So the coroutine is like a user-mode thread, very lightweight, and N coroutines can be created in one thread. The creation of the coroutine was too late

CoroutineScope
There are three ways to create and start the coroutine:

  1. runBlocking: T
    Start a new coroutine and block the thread that calls it, until the code inside is executed, the return value is generic
    T
    , Which is the type of the last line in your coroutine body, and what type is finally returned
    T
    What type is it.
  2. launch: Job
    To start a coroutine without blocking the calling thread, it must be in the coroutine scope (
    CoroutineScope
    ) Can be called, and the return value is a Job.
  3. async:Deferred<T>
    To start a coroutine without blocking the calling thread, it must be in the coroutine scope (
    CoroutineScope
    ) Can be called. To
    Deferred
    The coroutine task is returned as an object. Return value generic
    T
    with
    runBlocking
    Similar is the type of the last line of the coroutine body.

Wait, there seems to be something wrong, the strange knowledge points suddenly increased. As mentioned above, what type is the last line in the coroutine body, and what type is finally returned

T
It's just what type, it seems to be different from what we think, shouldn't the return value use return? I have learned
kotlin
Will know that in the
kotlin
In higher-order functions,
lambda
If an expression does not explicitly return a value, it will implicitly return the value of the last expression.

that

Job
,
Deferred
with
Coroutine scope
What's that stuff again. No hurry, take your time, we will explain clearly one by one.

What is Job, Deferred, and coroutine scope

Job
We can think of him as a coroutine homework is passed
CoroutineScope.launch
Generated, and it runs a specified code block at the same time, and completes when the code block is completed. We can pass
isActive
,
isCompleted
,
isCancelled
To get
Job
The current status of.
Job
The status of is shown in the figure below, taken from the official document:

The life cycle of a coroutine

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 roughly understand the next coroutine job from creation to completion or cancellation,

Job
It is not expanded here, we will explain it in the actual use process later.

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

Deferred

Deferred
Inherited from
Job
, We can think of it as a return value
Job
,

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

We need to focus on

await()
Method, you can see
await()
The result of the method is
T
, Which means that we can pass
await()
The method gets the return value of the execution flow, of course, if an exception occurs or the execution is canceled, the corresponding exception will be thrown.

What is scope

Coroutine scope (

Coroutine Scope
) Is the scope of the coroutine operation.
launch
,
async
All
CoroutineScope
The extension function ,
CoroutineScope
Defines the scope of the newly launched coroutine, and will inherit his
coroutineContext
Automatically spread all of its
elements
And cancel the operation. In other words, if this scope is destroyed, the coroutine inside will also become invalid. Just like the scope of a variable, such as the following
test
In the method
money
variable

private fun test () { //scope start int money = 100 ; println(money) } //End scope //the println (Money) copying the code

at this time

money
Cannot be called, because AS will prompt
Unresolved reference: money
. The scope of the coroutine is also such a function, which can be used to ensure that the coroutines inside have a scope restriction. The most common scenario in our development process is memory leaks. Coroutines also have such problems. We will explain the scope of coroutines in detail later.
CoroutineScope
The relevant knowledge of this article is only explained here as a basic point, and will not continue to be extended.

Basic usage of Kotlin coroutine

Now we start to use coroutines, first we are

MainActivity
Create a new one in the xml layout
Button
Button and then set the click event, and then create a
start()
Method through
Button
The click event is executed. Now we start at
start
Coroutines are used in the method.

Just now we mentioned that there are three ways to start a coroutine. Next, let s take a look at how to pass

runBlocking
,
launch
with
async
To start the coroutine, we use it directly in the start method, but because of our
launch
with
async
Startup can only be started under the scope of the coroutine, so what should we do?

Run the first coroutine

In Android there is a file called

GlobalScope
The global top-level coroutine, this coroutine runs during the entire application life cycle. We use this coroutine
launch
with
async
Start, the code is 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 () { runBlocking { Log.d( "runBlocking" , "Start a coroutine" ) } GlobalScope.launch{ Log.d( "launch" , "Start a coroutine" ) } GlobalScope.async{ Log.d( "async" , "Start a coroutine" ) } } } Copy code

Then run the app, click the button to execute

start()
method. We can see the following output on the console:

D/runBlocking: start a coroutine D/launch: start a coroutine D/async: start a coroutine Copy code

Wow, so easy. The coroutine turned out to be so simple, so let's move on. The three startup methods mentioned above will get their own return information respectively. We now add three variables and then use the coroutine to assign values, and output at the same time:

private fun start () { 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 am the return value" } Log.d( "asyncJob" , " $asyncJob " ) } Copy code

Then run, we can see the following output on the console:

D/runBlocking: start a coroutine D/runBlockingJob: 41 D/launchJob: StandaloneCoroutine{Active}@3b8b871 D/launch: start a coroutine D/async: start a coroutine D/asyncJob: DeferredCoroutine{Active}@63f2656 Copy code

It may also be

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

It may also be

D/runBlocking: start a coroutine D/runBlockingJob: 41 D/launch: start a coroutine D/launchJob: StandaloneCoroutine{Active} @b94e973 D/async: start a coroutine D/asyncJob: DeferredCoroutine the Active} { @ f7aa030 copy the code

Huh, the order of the next 4 logs is still random under what circumstances. If you don t understand the children s shoes, it means you didn t read the text carefully. The knowledge point is here, quickly take out your little book and write it down, and we will analyze them one by one.

We mentioned above

runBlocking
A new coroutine is started and the thread that calls it is blocked. We can see the first two by comparing the output log
runBlocking
The position order of the related output logs will not change, which proves what we said before
runBlocking
Will block the thread that calls it until
runBlocking
The execution will continue until the end of the run.

Next, we continue to look down, we see that the next four logs are out of order, but

launchJob
Always at
asyncJob
front. and
launch
with
async
The log output in the coroutine body is disordered. The order of seeing each time may be different from the previous one. We mentioned earlier
launch
with
async
Both start a coroutine but will not block the calling thread, so
launchJob
Always at
asyncJob
Before (it is not obvious between the two coroutines, you can start 5 or more coroutines at the same time to see the log output when you try it yourself)

and

launch
with
async
The log in the coroutine is disordered, because the coroutine uses a concurrent design pattern, so
launch
with
async
The log output in the coroutine body is out of order, which explains
launch
with
async
Both start a coroutine but do not block the calling thread, and also explain the relationship between the output order of the log (
The description here is not rigorous and will be added later
).

Is it over like this? Just now we mentioned that the coroutine adopts a concurrent design pattern, and multiple coroutines execute concurrently. So if at this time, we put the start coroutine under the same coroutine scope, what is the order in which it is started? You can think about this issue first, and we will come back to this issue later.

Return value of runBlocking

Now we return to the previous topic, we see the output log information

runBlockingJob
The output result of is 41. Why is it such a value? In fact, the default return is the current state of the coroutine job

We passed

runBlocking
The method can be seen, its return value is called
joinBlocking
Method, while in
joinBlocking
Method

We saw

joinBlocking
The method returns a state forced to become generic
T
Types of. We probably know now
runBlocking
What was returned. If in
runBlocking
Add a return value to the last line of the coroutine:

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

We will see the following output:

D/Coroutine: runBlocking starts a coroutine D/runBlockingJob: I am the return value of runBlockingJob coroutine Copy code

runBlocking
It is designed to connect conventional blocking code together, mainly for
main
Function and testing. According to the goal of this article, we will not expand further in the future.

Going down, we see

launchJob
The output is a
StandaloneCoroutine
Object, why is it a
StandaloneCoroutine
For the object, isn't it supposed to return a Job?

Don't panic, hold on! Continue to look down

launch function

We saw

launch
There are 3 parameters in the function
context
,
start
with
block
, With default values at the same time, although we don t know what these three parameters are for, but we can know the meaning by the name, we might as well make a bold guess. Do some basic explanations. We saw
launch
The method finally returns a
coroutine
Object, because we did not pass in a value, the last one returned is a
StandaloneCoroutine
Object, consistent with the log result we output. Then why did the author say
launch
What is returned is a Job. Let's keep looking
StandaloneCoroutine
What the hell is it? By looking up the inheritance relationship, we can see,
StandaloneCoroutine
It's just a Job, now it's clear at a glance.

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

async function

For the same reason, let's take a look

async
Function, and
launch
Have the same 3 parameters
context
,
start
with
block
, The default value is the same, and the final return is also a
coroutine
Object. just
async
return
DeferredCoroutine
Object.

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

The same is inheritance

AbstractCoroutine<Unit>
Class, but
DeferredCoroutine
Also inherited
Deferred<T>
interface. So it seems
DeferredCoroutine
Just one
Deferred<T>
, One carries the return value
Job
. So the question is, how do we get this
Deferred<T>
Carrying the return value
T
It.

We mentioned at the beginning that we need to focus on

Deferred
of
await()
Method, we can return
Deferred
Object, call
await()
Method to get the return value, we see
await()
There is a
suspend
Keywords, this is another thing.

public Suspend Fun the await () : T Copy Code

Suspend function

suspend
Is the key word of the coroutine, which means that each function is suspended
suspend
The way to decorate can only be in
suspend
Method or call in the coroutine. Now we modify the previous code and add a few more output logs at the same time:

private fun start () { 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 am async return value" } Log.d( "asyncJob.await" , ": ${asyncJob.await()} " ) Log.d( "asyncJob" , " $asyncJob " ) } } Copy code

Now we pass

GlobalScope.launch
Start a coroutine and pass in the coroutine body at the same time
launch
Two more coroutines were started directly. Why did we not use it in the coroutine body
GlobalScope.launch
Start, but use
launch
Start directly. Earlier we mentioned calling
launch
Must be in the coroutine scope (
Coroutine Scope
) Can only be called, because through
runBlocking
,
launch
with
async
The started coroutine body is equivalent to the coroutine scope, so here we can use it directly
launch
Start a coroutine. Let's run it, and then look at the log output:

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

It may also be like this

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

Now we see

asyncJob.await
It also outputs the return value we defined before, and at the same time
DeferredCoroutine
The status becomes
{Completed}
,This is because
await()
Is to wait for the completion of the value and continue execution without blocking the thread, when
deferred
Return the result value after the calculation is complete, or if
deferred
Is cancelled, the corresponding exception is thrown
CancellationException
. But because of
await()
Is the suspend function, he will suspend the call to his coroutine. So what we see
DeferredCoroutine
The status is
{Completed}
, And output at the same time
await
The log is also at the end.

Okay, so far. We

runBlocking
,
launch
,
async
That's it for the related introduction.

Concurrency and synchronization of coroutines in Android

Now we look back, we mentioned above: "Because the coroutine uses a concurrent design pattern, it leads to

launch
with
async
The log output in the coroutine body is out of order (
This is not rigorous
)".

Because the coroutine adopts the concurrency design pattern, there is no problem in most environments of this sentence. But, but, here comes the small details that need attention. If a coroutine satisfies the following points, the sub-coroutines in it will be executed synchronously:

  • The coroutine scheduler of the parent coroutine is in
    Dispatchers.Main
    Case start.
  • At the same time, the sub-coroutine is started without modifying the coroutine scheduler.
private fun start () { GlobalScope.launch(Dispatchers.Main) { for (index in 1 until 10 ) { //synchronous execution launch { Log.d( "launch $index " , "Start a coroutine" ) } } } } Copy 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/launch8: start a coroutine D/launch9: start a coroutine Copy code
private fun start () { GlobalScope.launch { for (index in 1 until 10 ) { //concurrent execution launch { Log.d( "launch $index " , "Start a coroutine" ) } } } } Copy 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: start a coroutine D/launch7: start a coroutine D/launch8: start a coroutine Copy code

Then the sub-coroutine will be executed synchronously, which is on the Android platform if the coroutine is in

Dispatchers.Main
Scheduler, it will schedule the coroutine to execute in the UI event loop, which is usually executed on the main thread, so you can understand why it is executed synchronously. If it is out of sync, then various problems will occur when I refresh the UI.

If one of the sub-coroutines changes his coroutine scheduler to

Non-Dispatchers.Main
, Then this sub-coroutine will execute concurrently with other sub-coroutines. We are not demonstrating here. You can try it yourself. After all, it is difficult to absorb knowledge in place without knowing it.

Notice

At the end of the next chapter, we mentioned that we will make a preliminary explanation of the following knowledge points in this chapter, including the above-mentioned

launch
with
async
The function of the 3 parameters in the function. The list is as follows:

  1. Coroutine scheduler
    CoroutineDispatcher
  2. Under the coroutine above
    CoroutineContext
    effect
  3. Coroutine start mode
    CoroutineStart
  4. Coroutine scope
    CoroutineScope
  5. Suspend function and
    suspend
    The role of keywords

Originality is not easy. If you like this article, you can use your little hand to like and collect it.

Extended series