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:
- Basic usage of the Kotlin coroutine.
- Kotlin coroutine key points preliminary explanation.
- Develop Android applications using Kotlin coroutine.
- Kotlin coroutine combination
Android Jetpack
Component applications. - Use of Kotlin coroutines in conjunction with third-party frameworks such as:
Retrofit
.OkHttp
.coil
And so on. - Dive into the Kotlin coroutine principle (e.g.
CoroutineContext
,Dispatcher
,CoroutineExceptionHandler
,Continuation
,Scheduler
Etc.)
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.3kotlin
: 1.4.32kotlinx-coroutines-android
: 1.4.3Retrofit
: 2.9.0okhttp
: 4.9.0coil
: 1.2.0room
: 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:
RunBlocking: T
Start a new coroutine and block the calling thread until the code inside completes, returning a generic valueT
What is the type of the last line in your coroutine body, and what type does it returnT
It’s what type.Launch: Job
To start a coroutine without blocking the calling thread, the coroutine scope (CoroutineScope
), the return value is a Job.async:Deferred<T>
To start a coroutine without blocking the calling thread, the coroutine scope (CoroutineScope
). In order toDeferred
Object in the form of a coroutine task. Return value genericT
withrunBlocking
Similar 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 returnedT
What type is it? It’s not what we think it is. Shouldn’t it be returnkotlin
Will know, in thekotlin
In higher-order functions,lambda
Expression If you do not explicitly return a value, it implicitly returns the value of the last expression.
theJob
,Deferred
andCoroutine scope
What 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? 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 throughrunBlocking
Method as you can see, its return value is calledjoinBlocking
Method, while injoinBlocking
In 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 thingasync
Function, andlaunch
It has the same three parameterscontext
,start
andblock
, the default value is the same, and the final return is also onecoroutine
Object. justasync
The returnedDeferredCoroutine
Object.
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 in
Dispatchers.Main
Case 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.Main
The 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 Main
This 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.
trailer
As mentioned at the end of the next chapter, the following knowledge points will be preliminarily explained in this chapter, including the functions of three parameters in launch and async functions mentioned above. Here’s the list:
- Coroutine scheduler
CoroutineDispatcher
- Coroutines below
CoroutineContext
role - Coroutine start mode
CoroutineStart
- Coroutine scope
CoroutineScope
- Suspend functions as well
suspend
The role of keywords
Originality is not easy. If you like this article, you can click “like”.
- 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