Detailed explanation of Kotlin coroutine
1. Foreword
1.1 Asynchronous
In development, we often encounter operations that need to be completed asynchronously, such as network requests. Since network requests are time-consuming, they need to be placed in sub-threads to work, while the main thread can continue to interact with users. Kotlin’s coroutine is essentially a thread framework, which can easily switch the context of threads.
1.2 Callback
After the child thread completes the time-consuming operation, it usually updates the interface through callback. The implementation of callback is relatively simple and suitable for simple scenarios. If it is a more complex scenario, such as multiple requests to the network, and the next request depends on the result of the previous request, the code of this structure is extremely difficult to read and maintain. bad.
//The client makes three network asynchronous requests in sequence, and updates the UI with the final result
request1(parameter) { value1 ->
request2(value1) { value2 ->
request3(value2) { value3 ->
updateUI(value3)
}
}
}
1.3 Coroutine role
- Coroutines can make asynchronous code synchronous.
- The coroutine canReduce the design complexity of asynchronous programs.
2. Getting to know coroutines
2.1 Add dependencies
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
2.2 Give an example
fun main(args: Array) {
println("Start")
GlobalScope. launch(Dispatchers. Main) {
delay(1000L)
println("Hello World")
}
println("End")
}
/*
Running result: ("Start" will be printed immediately, followed by "End", after 1000 milliseconds, "Hello, World!" will be printed)
start
end
Hello, World!
*/
The above code uses the launch method to start a coroutine. The curly braces after launch are the coroutines, and the code inside the curly braces is the code running in the coroutine.
If you don’t use the coroutine, it is the following style:
fun main(args: Array) {
println("Start")
Thread {
Thread. sleep(1000L)
println("Hello World")
}.start()
println("End")
}
The two pieces of code look almost exactly the same, and the running resultsAlso exactly. So where is the magic of coroutines?
If we add the operation of outputting the current thread name to all the output positions of the above two pieces of code:
fun main(args: Array) {
//coroutine code
println("Start ${Thread.currentThread().name}")
GlobalScope. launch(Dispatchers. Main) {
delay(1000L)
println("Hello World ${Thread.currentThread().name}")
}
println("End ${Thread.currentThread().name}")
fun main(args: Array) {
// thread code
println("Start ${Thread.currentThread().name}")
Thread {
Thread. sleep(1000L)
println("Hello World ${Thread.currentThread().name}")
}.start()
println("End ${Thread.currentThread().name}")
}
The thread code output is: “Start main” -> “End main” -> “Hello World Thread-2”. This result is also easy to understand, first output “Start” in the main thread, then create a new thread and block for one second after starting, At this time, the main thread continues to execute downwards and output “End”. At this time, the blocking time of the started thread ends, and the currently created thread outputs “Hello World”.
The coroutine code output is: “Start main” -> “End main” -> “Hello World main”. It is easy to understand that the first two outputs are consistent with the above, but after waiting for one second, the output in the coroutine shows that the current output thread is the main thread!
This is a very magical thing. Immediately after outputting “Start”, “End” is output, indicating that our main thread is not blocked, and it must be other threads that are blocked during the second of waiting. But the output after the blocking ends occurs in the main thread, which shows one thing: the code in the coroutine automatically switches to other threads and then automatically switches back to the main thread! Isn’t this exactly what we’ve always wanted?
2.3 launch method
Let’s take a closer look at the declaration of the launch method:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart. DEFAULT,
block: suspend CoroutineScope.() -> Unit): Job {...}
Parameters of the launch method:
context
: Coroutine context, which can specify the thread on which the coroutine runs. By default, it is consistent with the coroutineContext in the specified CoroutineScope. For example, GlobalScope runs in a background worker thread by default. can also passTo change the thread that the coroutine runs by displaying the specified parameters, Dispatchers provides several values that can be specified:Dispatchers.Default
,Dispatchers.Main
,Dispatchers.IO
,Dispatchers. Unconfined
.start
: The startup mode of the coroutine. The default (and most commonly used)CoroutineStart.DEFAULT
means that the coroutine is executed immediately, in addition toCoroutineStart.LAZY
,CoroutineStart.ATOMIC
code>,CoroutineStart.UNDISPATCHED
.- block: Coroutine body. That is to say, the code to be run inside the coroutine can be conveniently written in the way of lamda expression to run the code inside the coroutine.
- CoroutineExceptionHandler: In addition to handling exceptions inside the coroutine, CoroutineExceptionHandler can also be specified to handle exceptions inside the coroutine.
Return value Job: A reference to the currently created coroutine. You can control the start and cancel of the coroutine through the start
, cancel
, join
and other methods of the Job.
2.4 suspend keyword
Then why is the delay function in the coroutine not executed in the main thread? And why does it automatically switch back to the main thread after execution? How is this possible? Let’s take a look at the definition of the delay function:
public suspend fun delay(timeMillis: Long) {...}
You can find this letterCompared with the normal function, there is one more suspend
keyword in the front, this keyword translates to “suspend”, and the function modified by the suspend
keyword is also Called “suspend function”.
There is a rule about the suspending function: the suspending function must be called in a coroutine or other suspending functions, in other words, the suspending function must be executed directly or indirectly in a coroutine.
It is not the thread that is suspended but the coroutine. When the suspend function is encountered, the thread where the coroutine is located will not be suspended or blocked, but the coroutine is suspended, that is to say, when the coroutine is suspended, the current coroutine is decoupled from the thread it is running on. The thread continues to execute other codes, while the coroutine is suspended and waits, waiting for the thread to continue to execute its own code in the future. That is, the code in the coroutine is non-blocking for the thread, but it is blocking for the coroutine itself.
Add the supend
keyword before the function that needs to be suspended.
2.5 runBlocking
runBlocking is not a suspending function; that is, runBlocking runs a new coroutine and blocks the currently interruptible thread, and the thread calling it will remain in this function until the coroutine is executed.
fun main() {
runBlocking {
launch {
// start a new coroutine in the background and continue
delay(3000L)
println("Hello, World!")
}
println("End")
}
println("Start")
}
/*
Running result: ("End" will be printed immediately, after 3000 milliseconds, "Hello, World!" will be printed, print "Start" next)
end
Hello, World!
start
*/
2.6 CoroutineScope
coroutineScope is a suspending function; that is, if the coroutine in it is suspended, the coroutineScope function will also be suspended. coroutineScope will not block the current thread while waiting for all child coroutines to complete their tasks
CoroutineScope
is an interface. If you look at the source code of this interface, you will find that only one attribute CoroutineContext
is defined in this interface
public interface CoroutineScope {
/**
* Context of this scope.
*/
public val coroutineContext: CoroutineContext
}
So CoroutineScope
just defines a new Coroutine
execution coroutine scope. Each coroutine builder
is an extension function of CoroutineScope
and automatically inherits the coroutineContext
and cancellation operations of the current coroutine scope.
Actually, we do not need to implement the CoroutineScope
interface by ourselves, since all Coroutine
need a CoroutineScope
, so for the convenience of creating Coroutine
, there are many extension functions on CoroutineScope
, such as launch
, async
, actor
, cancel
, etc.
2.6.1 GlobalScope
GlobalScope
is a singleton implementation of CoroutineScope
. Since the GlobalScope
object is not associated with the application life cycle components, we need to manage the Coroutine
created by GlobalScope
by ourselves, so generally speaking we will not directly Use GlobalScope
to create Coroutine
.
launch
fun main(args: Array) {
println("Start")
GlobalScope. launch(Dispatchers. Main) {
delay(1000L)
println("Hello World")
}
println("End")
}
/*
Running result: ("Start" will be printed immediately, followed by "End", after 1000 milliseconds, "Hello, World!" will be printed)
start
end
Hello, World!
*/
async
//concurrent requests
GlobalScope. launch(Dispatchers. Main) {
//Three requests are performed concurrently
val value1 = async { request1(parameter1) }
val value2 = async { request2(parameter2) }
val value3 = async { request3(parameter3) }
//Update the UI after all the results are returned
updateUI(value1. await(), value2. await(), value3. await())
}
//requestAPI is adapted to the Kotlin coroutine
suspend fun request1(parameter : Parameter){...}
suspend fun request2(parameter : Parameter){...}
suspend fun request3(parameter : Parameter){...}
In the above code, we use the async method to wrap and execute the suspend
method, and then use the await
method to obtain the request result when the result is used, so three requests It is performed concurrently, and after the results of the three requests are returned, it will switch back to the main thread to update the UI.
Summary:
launch
and async
are similar, they are both used in a CoroutineScope
to start a new sub-coroutine. The difference can also be seen from the function name. launch
is more used to initiate a time-consuming task that does not require results (such as batch file deletion and creation), and this work does not need to return results. The async
function goes a step further and is used to execute time-consuming tasks asynchronously and needs to return values (such as network requests, database reads and writes, file reads and writes). After the execution is completed, pass await()
function to get the return value.
2.6.2 Lifecycle-aware coroutine scope
GlobalScope
object is not associated with application lifecycle componentsLink, you need to manage the Coroutine
created by GlobalScope
by yourself. For our convenience, Google’s Jetpack also provides some lifecycle-aware coroutine scopes. In actual development, we can easily select the appropriate range of coroutines to specify the timing of automatic cancellation for time-consuming operations (network requests, etc.). For details, see: https://developer.android.google.cn/topic/libraries/architecture /coroutines
Example:
viewModelScope.launch{...}
viewLifecycleOwner.lifecycleScope.launch{...}
Third, use the correct posture
Borrow the big guy’s Demo, project github address
You can see that the function is actually very simple, the interface consists of a button and three pictures. Every time the refresh button is pressed, three pictures will be obtained from the network and displayed on the interface. The refresh button becomes unavailable when the image is obtained from the network, and the button becomes available after the refresh is complete.
1. Add dependencies
//Add the dependencies of Retrofit network library and gsonConverter, pay attention to version 2.6.0 or above
implementation 'com.squareup.retrofit2:retrofit:2.7.0'
implementation 'com.squareup.retrofit2:converter-gson:2.7.0'
//Add the dependencies of the architecture components in Jetpack, pay attention to the viewmodel to add the dependencies of viewmodel-ktx
implementation "androidx.lifecycle:lifecycle-livedata:2.1.0"
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0'
implementation "androidx.lifecycle:lifecycle-extensions:2.1.0"
//Add Glide dependency for image loading
implementation 'com.github.bumptech.glide:glide:4.10.0'
2. Write UI interface
2. Write network layer interface
The data format is very simple, we can easily create the corresponding entity class:
data class ImageDataResponseBody(
val code: String,
val imgurl: String
)
ApiService is the access singleton class of our network interface
import com.njp.coroutinesdemo.bean.ImageDataResponseBody
import retrofit2.http.GET
import retrofit2.http.Query
//Network Interface
interface ApiService {
//declare as suspend method
@GET("image/sogou/api.php")
suspend fun getImage(@Query("type") type: String = "json"): ImageDataResponseBody
}
NetworkService is the defined network interface:
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.createimport java.util.concurrent.TimeUnit
//Network layer access unified entrance
object NetworkService {
//retorfit instance, do some unified network configuration here, such as adding converters, setting timeouts, etc.
private val retrofit = Retrofit. Builder()
.client(OkHttpClient.Builder().callTimeout(5, TimeUnit.SECONDS).build())
.baseUrl("https://api.ooopn.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
//Network layer access service
val apiService = retrofit. create()
}
It is worth noting that when we define our interface, we must declare it as a suspend method, which completes the perfect support for Kotlin coroutines
3. Write ViewModel code
First of all, because our project needs to monitor the status of network loading, we can set whether the refresh button can be clicked and display error messages. So we can write a LoadState class as the bearer of network loading state information:
enum class LoadState {
/**
* Loading
*/
Loading,
/**
* success
*/
Success,
/**
* fail
*/
Fail
}
Then let’s create our ViewModel:
class MainViewModel : ViewModel() {
val imageData = MutableLiveData<List>()
val loadState = MutableLiveData()
fun getData() {
viewModelScope. launch {
try {
loadState.value = LoadState.Loading
val data1 = async { NetworkService.apiService.getImage() }
val data2 = async { NetworkService.apiService.getImage() }
val data3 = async { NetworkService.apiService.getImage() }
imageData.value = listOf(data1.await(), data2.await(), data3.await()).map {
it.imgurl
}
loadState.value = LoadState.Success
} catch (e: Throwable) {
loadState.value = LoadState.Fail
// Unified error handling here
ExceptionUtil.catchException(e)
}}
}
}
object ExceptionUtil {
/**
* Handle exceptions, toast prompts error messages
*/
fun catchException(e: Throwable) {
e. printStackTrace()
when (e) {
is HttpException -> {
catchHttpException(e. code())
return
}
is SocketTimeoutException -> ToastUtil.showShort(R.string.common_error_net_time_out)
is UnknownHostException, is NetworkErrorException -> ToastUtil.showShort(R.string.common_error_net)
is MalformedJsonException, is JsonSyntaxException -> ToastUtil.showShort(R.string.common_error_server_json)
// interface exception
is Throwable -> ToastUtil.showShort("$e.errorCode: $e.msg")
else -> ToastUtil.showShort("${CoolWeatherApplication.context.getString(R.string.common_error_do_something_fail)}: ${e::class.java.name}")
}
}
/**
* Handle network exceptions
*/
fun catchHttpException(errorCode: Int) {
if (errorCode in 200 until 300) return// success code is not processed
ToastUtil. showShort(catchHttpExceptionCode(errorCode))
}
/**
* Handle network exceptions
*/
private fun catchHttpExceptionCode(errorCode: Int): Int = when (errorCode) {
in 500..600 -> R.string.common_error_server
in 400 until 500 -> R.string.common_error_request
else -> R.string.common_error_request
}
}
4. Write View layer code
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.bumptech.glide.Glide
import com.xxx.coroutinesdemo.R
import com.xxx.coroutinesdemo.bean.LoadState
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity(){
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R. layout. activity_main)
//Get the ViewModel
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
//Dynamic observation of loading status
viewModel.loadState.observe(this, Observer {
when (it) {
LoadState.Success -> button.isEnabled = true
LoadState.Fail -> {
button.isEnabled = true
Toast.makeText(this, it.msg, Toast.LENGTH_SHORT).show()
}
LoadState.Loading -> {
button.isEnabled = false
}
}
})
//Observe the picture Url data
viewModel.imageData.observe(this, Observer {
//Load three pictures with Glide
Glide.with(this)
.load(it[0])
.into(imageView1)
Glide.with(this)
.load(it[1])
.into(imageView2)
Glide.with(this)
.load(it[2])
.into(imageView3)
})
//Click the refresh button to load from the network
button.setOnClickListener {
viewModel. getData()
}
}
}