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 to CoroutineStart.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()
        }


    }
}


Leave a Reply

Your email address will not be published. Required fields are marked *