preface
Hello everyone, I’m Xiao Yi! As we all know, At Google I/O 2017, Google announced Kotlin as the first Android development language, and in recent years, many large and mid-sized companies have been recruiting Android developers to use Kotlin, so if you haven’t started using Kotlin yet, Just get on board!
recommended
This article will be first published on the public account “Code Man” and personal blog “Li Yi’s Small station”, come and pay attention to it!
Understanding coroutines
There are many benefits to using Kotlin development that I won’t go over here. And there’s something in Kotlin called a coroutine, which some of you may have heard of or even used, but many of you may not know much about, so let me tell you a little bit about what a coroutine is!
Coroutines profile
Coroutines actually is a kind of programming ideas, is present in many languages, roughly the effect also is same, all is in order to help us in dealing with a multithreaded code to provide a more friendly and simple encoding, but each language is not the same as the concrete implementation, in Kotlin coroutines we can understand simple framework for a thread.
The inconvenience of ordinary threads
Let’s use a small example to compare the difference between using normal threads and using coroutines:
Assume that four interfaces request A, B, C, and D. Interface C can be executed only after obtaining the execution results of interface A and interface B. Interface D can be executed only after obtaining the execution results of interface C.
The above scenario is the most common scenario in Android development. If it is implemented by ordinary thread, the result of interface request is usually obtained by callback, and interface A and interface B should be executed concurrently. We must check whether interface B is completed in callback after interface A is executed. It is also necessary to check if interface A is complete in the completed callback of interface B, so that the results of both interfaces can be sent to interface C in the first place, and interface C needs to call interface D in the completed callback to make it execute.
fun apiA(object: callback(){get -- > result A calls -- > interface AB completes execution detection method ()})fun apiB(object: callback()Fun interface AB execute execute execute execute execute execute execute execute execute execute execute execute (){if(the results A! =null&& result B! =nullApiC (result A, result B, object: callback(){call -- > apiD(result C)}}}// Start executing
fun start(a){
apiA();
apiB();
}
Copy the code
As you can see, the entire execution process is full of callbacks, and the mutual monitoring between interfaces A and B to see if the execution is complete also leads to much more code. If the logic were a little more complicated, it would be callback hell, and the code would be worse to read. Of course, we can use message notification methods such as Handler for unified processing, but the reading experience is also limited.
The above inconvenience, in the final analysis, is because the thread is scheduled by the system, the system controls the end of the execution of the thread, we developers in the main thread is unable to know when the end of the execution of the thread, can only wait for the thread itself to inform us.
Advantages of coroutines
The above scenario is implemented by coroutine, and its implementation logic is roughly as follows:
Result A = Coroutine A executes () result B = coroutine B executes () result C = coroutine C executes (result A, result B) Coroutine D executes (result C)Copy the code
PS: Here is A special reminder that the logic of coroutine A and B above seems to be executed by coroutine A first and then executed by coroutine B, but in fact, A and B are also executed concurrently
As you can see from the above flow, the code is written directly from asynchronous code to synchronous code, the logic is much clearer, there are many fewer callbacks, and the reading experience of the code is also improved. Thanks to the flexibility of the coroutine to switch between threads, the developer can explicitly control the end of the coroutine and no longer have to wait passively for notifications. At this point, some students may think of RxJava, both can achieve the effect we want, but the design concept of the two is not the same, syntax is also very different, interested students can understand both.
To sum up, we can conclude that the coroutine can simplify asynchronous coding, write asynchronous in a synchronous way, and the developer can flexibly control the execution and termination of the coroutine as well as the switch of threads. While this is one of the features we care about most in Android development, there are other benefits of coroutines that we don’t need to know about for now.
Import coroutines
The Kotlin coroutine support is already automatically added to higher versions of Android Studio. If coroutines are not available, you can add the following dependencies
implementation "Org. Jetbrains. Kotlinx: kotlinx coroutines -- core: 1.3.5." "
implementation "Org. Jetbrains. Kotlinx: kotlinx coroutines - android: 1.3.5." "
Copy the code
3. Several ways of using coroutines
1. runBlocking
runBlocking {
getUserInfo(userId)
}
Copy the code
This method is generally not recommended because it blocks the thread
2. GlobalScope.launch
GlobalScope.launch {
getUserInfo(userId)
}
Copy the code
This method needs to be used with caution. Although it does not block threads, it has the same life cycle as the app and cannot be cancelled
3. Create your own CoroutineScope
// The context here is CoroutineContext, which is not the same thing as the Activity's inherited context
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
getUserInfo(userId)
}
Copy the code
This method is recommended because we can control the life cycle of coroutine flexibly. Therefore, we mainly introduce the use of coroutine in this way, and the other two ways can be understood by students who are interested.
4. Simple use of coroutines
Several methods commonly used by coroutines include launch, withContext and async/await. We will introduce launch and withContext and leave async/await to be discussed with suspend in the next chapter.
launch
First of all, let’s look at the source of launch and have a brief understanding of what launch is!
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
Copy the code
As you can see, launch is an extension function of CoroutineScope (that is, launch is an internal function of CoroutineScope) and finally returns a new Coroutine (that is, a new Coroutine is created), implementation of which we won’t care. Its use method is as follows:
// Switch to main thread
coroutineScope.launch(Dispatchers.Main) {
...
}
// Switch to IO thread execution
coroutineScope.launch(Dispatchers.IO) {
...
}
/ / small example
coroutineScope.launch(Dispatchers.IO) {
val userInfo = getUserInfo()
}
tv_name = "xxx"
Copy the code
We can switch to a different thread by specifying Dispatchers, otherwise the coroutine defaults to performing tasks in the thread of its method. The code in {} is the block of code that the coroutine needs to execute. In the last small example, tv_name = “XXX” does not wait for getUserInfo() to finish executing, Since the execution of coroutinescope.launch (dispatchers.io) {} does not block the execution of the UI thread, coroutinescope.launch (dispatchers.io) {} just opens another thread, Just like new Thread().start(). So it’s important to clarify the relationship between coroutines and their external threads. Let’s look inside the coroutine again with a concrete example:
coroutineScope.launch(Dispatchers.Main) {
tv_name.text = "xxx"
// Switch to the IO thread
launch(Dispatchers.IO){
// Get user information
val userInfo = getUserInfo()
// Switch to the UI thread
launch(Dispatchers.Main){
// Modify the user name display
tv_name.text = userInfo.username
// Switch to the IO thread
launch(Dispatchers.IO) {
// Get the list of messages
val msgList = getMessageList(userInfo.token)
// Switch to the UI thread
launch(Dispatchers.Main){
// Displays the message list
}
}
}
}
}
Copy the code
In the above example, we get the user information in the IO thread, then update the user name in the main thread, then get the message list in the IO thread, and finally display the message list in the main thread. The whole process is completed by launching to create subcoroutines that work in different threads and is a completely synchronous coding experience. In addition, if both launches are at the same level, as follows:
coroutineScope.launch(Dispatchers.Main) {
// Switch to the IO thread
launch(Dispatchers.IO){
// Get user information
val userInfo = getUserInfo()
}
// Switch to the IO thread
launch(Dispatchers.IO){
// Get the home page information
val userInfo = getHomeInfo()
}
}
Copy the code
GetUserInfo () and getHomeInfo() execute concurrently, meaning that the two launches are in parallel. This approach is suitable for scenarios where the launch is concurrent and no results are returned, such as multiple network requests that are concurrent but unrelated to each other.
withContext
Although the use of launch has changed the whole coding mode from asynchronous to synchronous, it is not beautiful to nest multiple launches. We can optimize it and use withContext to rewrite it:
coroutineScope.launch(Dispatchers.Main) {
tv_name.text = "xxx"
// Get user information
val userInfo = withContext(Dispatchers.Main) {
getUserInfo()
}
tv_name.text = userInfo.username
// Get the list of messagesval msgList = withContext(Dispatchers.IO) { getMessageList(userInfo!! .token) }// Displays the message list
}
Copy the code
In the code above, withContext is executed sequentially, unlike multiple sibling launches that are executed concurrently. Using withContext eliminates the nesting of launches for a better reading experience. However, withContext can only be used within CoroutineScope, not in other methods, such as:
class BaseActivity : Activity(a){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) withContext(Dispatchers.IO){ ... }}}Copy the code
This doesn’t work because withContext can only be used inside the CoroutineScope.
Five, the summary
This chapter briefly introduces what coroutines are and how to use them. In the next chapter, we will introduce the async/await and suspend keywords in writing coroutines, which are used frequently in our daily development, especially suspend. Finally, I recommend a series of Kotlin coroutine cognition articles for those who do not understand coroutines very well.
- “Take a hard look at Kotlin’s Coroutines – Can’t Learn coroutines? Probably because all the tutorials you read are wrong.”
- “Kotlin coroutine hanging so weird and confusing? I skinned it today.”
- What is a “non-blocking” hang, and Are Coroutines Really Lighter?