So let’s not talk about the concept, let’s talk about the code, let’s see how coroutines work.

Retrofit request code

interface HttpInterface {
    @GET("/photos/random")
    suspend fun getImageRandom(@Query("count") count: Number): ArrayList<ImageBean>
}
Copy the code

Call code in the activity

override fun onCreate(savedInstanceState: Bundle?). {
    lifecycleScope.launch {
        val imageArray: ArrayList<ImageBean> = httpInterface.getImageRandom(10)// Send the request
        textView.text = "The number of pictures is" + imageArray.size/ / update the UI}}Copy the code

You can see that sending the request and updating the UI are in one block of code that looks like they’re both running in the main thread, but there are no errors. This is where coroutines are most attractive non-blocking hangs, which I’ll talk about later.

Create coroutines

There are three ways to create coroutings: launch, async, and runBlocking

launch

The launch method is signed as follows:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope. () - >Unit
): Job {
    / / to omit
    return coroutine
}
Copy the code

Launch is an extension of CoroutineScope and takes three parameters. The first argument, literally, is the coroutine context, which we’ll focus on later. The second parameter is the coroutine startup mode, which by default is executed immediately after creation. The third parameter, the official document says that the block is a coroutine block, so it is required. This Job can be interpreted as a background Job that ends after the block is executed, or it can be cancelled using the Job’s cancel method.

async

The async method is signed as follows:

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope. () - >T
): Deferred<T> {
    / / to omit
    return coroutine
}
Copy the code

The CoroutineScope extension takes exactly the same parameters as the launch, except that the return parameter is Deferred, which is inherited from the Job and is equivalent to a Job that returns results. The result returned can be obtained by calling its await method.

runBlocking

RunBlocking blocks the thread that called it until the block completes execution.

Log.i("zx"."Current thread 1-" + Thread.currentThread().name)
runBlocking(Dispatchers.IO) {
    delay(2000)
    Log.i("zx"."After sleep for 2000 milliseconds, the current thread" + Thread.currentThread().name)
}
Log.i("zx"."Current thread 2-" + Thread.currentThread().name)
Copy the code

output

The defaultDispatcher-worker-1 of the current thread is defaultDispatcher-worker-1 of the current thread is 2-mainCopy the code

As you can see, even if the coroutine specifies to run on the IO thread, it still blocks the main thread. RunBlocking is mainly used to write test code, and should not be used freely, so I won’t go into more detail about it.

CoroutineScope CoroutineScope

Launch and async are both extensions of CoroutineScope, which translates to CoroutineScope. CoroutineScope is similar to variable scope, which defines the scope of coroutine code. When the scope is cancelled, all coroutines in the scope are cancelled. For example:

MainScope().launch {
    var i = 0

    launch(Dispatchers.IO) {
        while (true) {
            Log.i("zx"."The subcoroutine is running$i")
            delay(1000)}}while (true) {
        i++
        Log.i("zx"."The parent coroutine is running$i")

        if (i>4) {
            cancel()
        }
        delay(1000)}}Copy the code

Output:

Parent coroutine is running 1 child coroutine is running 1 child coroutine is running 2 child coroutine is running 2 child coroutine is running 3 child coroutine is running 3 child coroutine is running 4 child coroutine is running 4 child coroutine is running 4 parent coroutine is running 5Copy the code

After 5 seconds, the parent coroutine calls cancel(), and the child coroutine ends without continuing to print out values.

CoroutineScope() can be used to create a CoroutineScope. This is not a constructor. CoroutineScope is an interface, so there is no constructor.

@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if(context[Job] ! =null) context else context + Job())
Copy the code

CoroutineContext is the only member variable in the CoroutineScope interface. CoroutineScope() is used to create two scopes, MainScope and GlobalScope. The source code is as follows:

public fun MainScope(a): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

public object GlobalScope : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}
Copy the code

MainScope is a method that returns a scope that runs on the main thread and needs to be manually cancelled. GlobalScope is a GlobalScope that runs for the entire application life cycle and cannot be cancelled in advance, so it is generally not used. In Android, the KTX library provides some common scopes for us to use, such as lifecycleScope and viewModelScope. The lifecycleScope can be used directly in all of the LifecycleOwner implementation classes, such as activities and fragments. The lifecycleScope will follow the lifecycle of the Activity or Fragment. When an Activity or Fragment is destroyed, all coroutines in the scope of the coroutine are automatically removed, without manual management, and there is no risk of memory leakage. A similar viewModelScope will also be cancelled with the destruction of the viewModel.

CoroutineContext has appeared in several places: CoroutineContext is required when launching a coroutine or async method. CoroutineContext is required when creating a coroutine scope. CoroutineContext is required when there is only one member variable in the coroutine scope.

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}
Copy the code

So CoroutineContext must be important.

CoroutineContext CoroutineContext

CoroutineContext holds the context of the coroutine. It is a Set of elements (not actually a Set), each of which has a unique key. In general terms, CoroutineContext holds the various Settings that coroutines depend on, such as schedulers, names, exception handlers, and so on.

CoroutineContext source code:

public interface CoroutineContext {

    public operator fun <E : Element> get(key: Key<E>): E?

    public fun <R> fold(initial: R, operation: (R.Element) - >R): R

    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else
            context.fold(this) { acc, element ->
                / / to omit
            }

    public fun minusKey(key: Key< * >): CoroutineContext

    public interface Key<E : Element>

    public interface Element : CoroutineContext {
        / / to omit}}Copy the code

CoroutineContext has an interface Element, which is the Element that makes up CoroutineContext, and the most important is the plus operator function, This function combines several elements into a single CoroutineContext, which can be called directly with + because it is an operator function. Such as:

var ctx = Dispatchers.IO + Job()  + CoroutineName("Test name")
Log.i("zx", ctx.toString())
Copy the code

The output

[JobImpl{Active} @31226A0, CoroutineName, Dispatchers.IO]Copy the code

What are the elements? Take a look at the subclasses of Element. There are so few Element subclass (a interface) : Job, CoroutineDispatcher, CoroutineName, CoroutineExceptionHandler.

Job

A Job can be simply interpreted as a reference to a coroutine. When a coroutine is created, an instance of a Job is returned. The life cycle of a coroutine can be managed by a Job. Job is one of the CoroutineContext elements that can be passed in a CoroutineScope to give coroutine different characteristics. SupervisorJob() and the Deferred Job subinterface are the two main functions that create the Job.

Job()

Create an active Job object that can be passed to the parent Job so that when the parent Job is cancelled, the Job and its children can be cancelled. Any failure of a child of the Job immediately causes the Job to fail and cancellations of its remaining children. This makes sense. For example:

CoroutineScope(Dispatchers.IO + Job()+MyExceptionHandler()).launch {
    var index = 0
    launch {
        while (true) {
            index++
            if (index > 3) {
                throw Exception("Subcoroutine 1 is abnormal.")
            }
            Log.i("zx"."Subcoroutine 1 is running")
        }
    }

    launch {
        while (true) {
            Log.i("zx"."Subcoroutine 2 is running")}}}Copy the code

If subcoroutine 1 is abnormal, the entire Job fails, and subcoroutine 2 does not continue running.

SupervisorJob()

Create an active Job object. The children of this Job are independent of each other. The failure or cancellation of a child does not cause the failure of the main Job or affect other children.

CoroutineScope(Dispatchers.IO + SupervisorJob() + MyExceptionHandler()).launch {
    launch {
        while (true) {
            index++
            if (index > 3) {
                throw Exception("Subcoroutine 1 is abnormal.")
            }
            Log.i("zx"."Subcoroutine 1 is running")
        }
    }

    launch {
        while (true) {
            Log.i("zx"."Subcoroutine 2 is running")}}}Copy the code

SupervisorJob() is the sub – coroutine 2 SupervisorJob(). It is designed to run on its SupervisorJob 2, and it is not cancelled when the sub – coroutine 1 is abnormal.

MainScope, viewModelScope, and lifecycleScope are all created with SupervisorJob(), so subcoroutines in these scopes do not cause the root coroutine to exit. Kotlin provides a quick function to create a coroutine that uses the SupervisorJob, and that is supervisorScope. Such as:

CoroutineScope(Dispatchers.IO).launch {
    supervisorScope {
        // This subcoroutine code exception does not cause the parent coroutine to exit.}}Copy the code

Is equivalent to

CoroutineScope(Dispatchers.IO).launch {
    launch(SupervisorJob()) {

    }
}
Copy the code

Deferred

Is a subinterface of Job, which is a Job with a return result. The coroutine created by the async function returns a Deferred, and you can get the actual return value from the Deferred await(). Async and await are similar to async and await in other languages, such as JavaScript, and are usually used to execute two coroutines in parallel. For example, the following code

suspend fun testAsync1(a): String = withContext(Dispatchers.Default)
{
    delay(2000)
    "123"
}

suspend fun testAsync2(a): String = withContext(Dispatchers.Default)
{
    delay(2000)
    "456"
}

lifecycleScope.launch {
    val time1 = Date()
    val result1 = testAsync1()
    val result2 = testAsync2()
    Log.i("zx"."The results for${result1 + result2}")
    Log.i("zx"."Take${Date().time - time1.time}")}Copy the code

Will output:

The result is 123456. It takes 5034Copy the code

If async is used instead, parallel the two coroutines. The code is as follows:

lifecycleScope.launch {
    val time1 = Date()
    val result1 = async { testAsync1() }
    val result2 = async { testAsync2() }
    Log.i("zx"."The results for${result1.await() + result2.await()}")
    Log.i("zx"."Take${Date().time - time1.time}")}Copy the code

The output

The result is 123456. It takes 3023Copy the code

The total time is the longer of the two parallel coroutines.

CoroutineDispatcher scheduler

Specifies the thread or pool of threads on which the coroutine will run.

  • Dispatchers.Main runs on the Main thread. The Android platform is the UI thread, which is single threaded.
  • Dispatchers.Default The Default scheduler, or Default if no scheduler is specified in the context. Suitable for performing computation-intensive tasks that consume CPU resources. It is supported by a shared thread pool on the JVM. By default, the maximum number of parallel threads used by this scheduler is equal to the number of CPU cores, but at least two.
  • IO Dispatchers, using a shared pool of threads created on demand, are suitable for performing IO intensive blocking operations such as HTTP requests. This scheduler defaults to the larger of the two values, kernel and 64.
  • Dispatchers.Unconfined coroutine scheduler not limited to any particular thread and not commonly used.

Note that both Default and IO run in a thread pool. The two subcoroutines may or may not be in the same thread. For example:

CoroutineScope(Dispatchers.IO).launch {
    launch {
        delay(3000)
        Log.i("zx"."Current thread 1-" + Thread.currentThread().name)
    }

    launch {
        Log.i("zx"."Current thread 2-" + Thread.currentThread().name)
    }
}
Copy the code

The output

Current thread 2- defaultDispatcher-worker-2 Current thread 1-DefaultDispatcher-worker-5Copy the code

So, if ThreadLocal data for a thread is involved, do it.

What happens if you make an IO request with the wrong Dispatchers.Default? The Default scheduler has a much smaller number of parallel threads than the IO scheduler. One of the characteristics of IO requests is that the wait time is very long, while the actual processing time is very short. Therefore, a large number of requests are in the state of waiting for the allocated thread, resulting in inefficiency. You can actually write a program to test it, but I won’t try it here.

CoroutineName Name of a coroutine

Pass in a String as the name of the coroutine, typically used for debug log output to distinguish between different schedulers.

CoroutineExceptionHandler exception handler

Used to handle all uncaught exceptions in the coroutine scope. Implement CoroutineExceptionHandler interface, the code is as follows:

class MyExceptionHandler : CoroutineExceptionHandler {
    override val key: CoroutineContext.Key<*>
        get() = CoroutineExceptionHandler

    override fun handleException(context: CoroutineContext, exception: Throwable) {
        Log.i("zx"."${context[CoroutineName]}Abnormal occurs in,${exception.message}")}}Copy the code

Then concatenate with + and set to scope.

CoroutineScope(Dispatchers.IO + CoroutineName("Parent coroutine") + MyExceptionHandler()).launch {
    launch(CoroutineName("Subcoroutine 1") + MyExceptionHandler()) {
        throw Exception("It's over. It's abnormal.")}}Copy the code

The output content is

An exception has occurred in CoroutineName. That's it. That's an exceptionCopy the code

Why is the exception thrown by the parent coroutine? It turns out that the exception rule is that the subcoroutine will throw the exception up one level until it reaches the root coroutine. So what is a root coroutine? It’s just the outer coroutine, and the special rule is that coroutines created with the SupervisorJob are also treated as root coroutines. For example:

CoroutineScope(Dispatchers.IO + CoroutineName("Parent coroutine") + MyExceptionHandler()).launch {
    launch(CoroutineName("Subcoroutine 1") + MyExceptionHandler() + SupervisorJob()) {
        throw Exception("It's over. It's abnormal.")}}Copy the code

The output content is

Exception in CoroutineName(subcorout1), die, exceptionCopy the code

Speaking of handle exceptions, we must think try/catch, why have a try/catch, also have a CoroutineExceptionHandler in coroutines? Or actually does CoroutineExceptionHandler, when using CoroutineExceptionHandler when to use try/catch? The official document is so describe CoroutineExceptionHandler used to handle an uncaught exception, is used for global catch-all behavior last a mechanism. You can’t recover from CoroutineExceptionHandler exception. When the handler is called, the coroutine is finished. This is the last line of defense. When the coroutine is finished, you can only handle exceptions and nothing else. Let me give you an example

CoroutineScope(Dispatchers.IO + CoroutineName("Parent coroutine") + MyExceptionHandler()).launch {
    val test = 5 / 0
    Log.i("zx"."I want to continue executing my coroutine code even if there is an exception. For example, I want to notify the user to refresh the interface.")}Copy the code

Coroutines body in the first line 5/0 throws an exception, in CoroutineExceptionHandler for processing, but coroutines will end directly, the subsequent won’t execute code, if you want to continue to perform coroutines, such as pop-up Toast to inform users, there won’t do it. Try/catch would be fine.

CoroutineScope(Dispatchers.IO + CoroutineName("Parent coroutine") + MyExceptionHandler()).launch {
    try {
        val test = 5 / 0
    } catch (e: Exception) {
        Log.i("zx"."I'm abnormal.")
    }
    Log.i("zx"."Continue to execute other code of the coroutine.")}Copy the code

That being the case, I put all the code in the coroutines directly try/catch, not CoroutineExceptionHandler not line? That sounds fine, so let’s give it a try

inline fun AppCompatActivity.myLaunch(
    crossinline block: suspend CoroutineScope. () - >Unit
) {
    CoroutineScope(Dispatchers.IO).launch {
        try {
            block()
        } catch (e: Exception) {
            Log.e("zx"."Abnormal," + e.message)
        }
    }
}
Copy the code

As long as the wrapped myLaunch function is called, all the coroutine code is wrapped in try/catch, which must be ok. Let’s say I call it like this

myLaunch {
    val test = 5 / 0
}
Copy the code

It’s not broken. Good. Change the code and continue calling

myLaunch {
    launch {
        val test = 5 / 0}}Copy the code

There is a try/catch layer in the outermost layer of the APP. Think about the previous rule for coroutines throwing exceptions: subcoroutines throw an exception up one level until they reach the root coroutine. The exception code runs in the subcoroutine. The subcoroutine throws the exception directly to the parent coroutine, so the try/catch is not caught. Here the parent coroutine doesn’t specify an exception handler, so it crashes. Someone might want to raise the bar, so why don’t I just put the try/catch in the subcoroutine? If you remember to add try/catch here, you might forget to add it elsewhere. So CoroutineExceptionHandler the advantage of the full scope to catch exceptions came out. So let’s briefly summarize the differences and use scenarios.

  • CoroutineExceptionHandler scope for coroutines global catch exceptions untreated, can capture of abnormal coroutines, capture the exception, coroutines ended. Used for final exception handling to ensure no crash, such as logging.
  • Try/catch can catch exceptions more finely, down to a line of code or an operation, fail to catch subcoroutines, and do not prematurely end the coroutine. Suitable for catching predictable exceptions.

The following is a personal experience, not necessarily correct, for reference only.

CoroutineExceptionHandler applies to catch exceptions unpredictable. Try/catch applies to predictable exceptions. What are predictable and unpredictable exceptions? For example, if you want to write a file to a disk, you may have no permissions, or the disk may be full. These exceptions are predictable, so try/catch should be used. Unpredictable exceptions is refers to, there is nothing wrong with code looks, but I don’t know where will go wrong, don’t know where the try/catch, try/catch any less, this time should give CoroutineExceptionHandler, CoroutineExceptionHandler, after all, is the last line of defense.

CoroutineContext summary

CoroutineContext by Job, CoroutineDispatcher, CoroutineName, CoroutineExceptionHandler composition. A Job controls the life cycle of a coroutine and determines whether a parent Job is cancelled when a subitem is abnormal. The CoroutineDispatcher determines which thread the coroutine runs on. CoroutineName Gives a name to a coroutine, used for debugging purposes. CoroutineExceptionHandler used in full scope to capture and handle exceptions. Subcoroutines automatically inherit the CoroutineContext of their parent and can be overwritten. CoroutineContext elements can be combined by the + operator, or the elements in CoroutineContext can be retrieved by the corresponding key.

CoroutineStart Startup mode

The second argument to launch and async is CoroutineStart, which is the launch mode of the coroutine.

  • DEFAULT- The DEFAULT mode for scheduling coroutines immediately;

  • LAZY- starts coroutines lazily only when needed, using start();

  • ATOMIC- to schedule a coroutine atomically (in a non-cancellable way) and to cancel after the hang point;

  • UNDISPATCHED- Also executes the coroutine atomically (in an unrescinable manner) to the first limit point. The limit is different from ATOMIC. UNDISPATCHED does not require dispatch and is executed directly. UNDISPATCHED is executed in the thread specified by the parent coroutine. Once the limit is reached, it is dispatched to the thread specified in its own context. ATOMIC is executed in the thread specified in its own coroutine.

    Note that schedules and executes are not the same and do not necessarily execute immediately after scheduling.

Give examples.

LAZY mode:

val job = lifecycleScope.launch(start = CoroutineStart.LAZY) {
    Log.i("zx"."Coroutine runs 1")}Copy the code

The above code does not print out the content. You need to manually call job.start() to start the coroutine and print out the content.

ATOMIC model:

val job = lifecycleScope.launch(start = CoroutineStart.ATOMIC) {
    Log.i("zx"."Coroutine runs 1")
    delay(2000)
    Log.i("zx"."Coroutine runs 2")
}
job.cancel()
Copy the code

Because of the ATOMIC start mode used, execution can’t be cancelled until after the suspend start (delay is the suspend function), so “coroutine ran 1” is printed anyway. Execution can be cancelled after the start of the hang, so the second line is not printed.

UNDISPATCHED mode:

lifecycleScope.launch {
    Log.i("zx"."Parent coroutine, current thread" + Thread.currentThread().name)

    val job = launch(Dispatchers.IO, CoroutineStart.UNDISPATCHED) {
        Log.i("zx"."Subcoroutine, current thread" + Thread.currentThread().name)
        delay(1000)
        Log.i("zx"."Subcoroutine delay after current thread" + Thread.currentThread().name)
    }
}
Copy the code

Above code output

Parent coroutine, the current thread main subcoroutine, the current thread main subcoroutine delay, the current thread defaultDispatcher-worker-1Copy the code

The result verifies that the parent coroutine is used to execute the coroutine until the first point of suspension, and the thread set in coroutineContext is used after the point of suspension. Similar to ATOMIC, it is also non-cancellable until the first hanging point is reached.

Suspend and withContext

Before repeatedly mentioned hanging starting point, then what is hanging starting point? What is hanging? The suspend point is actually the point at which the coroutine code reaches the suspend function. At this point, the coroutine is suspended and the code after the suspend function is no longer executed. Until the suspend function is finished, the coroutine code automatically resumes execution. The delay function above is a suspend function. It suspends the current coroutine block, executes the delay function first, and resumes executing the original code when the delay finishes. The feature of pausing and then automatically resuming after the code has finished executing is ideal for handling asynchronous tasks. For example:

private suspend fun getBitmapByHttp(a): Bitmap {
    Log.i("zx"."Current thread" + Thread.currentThread().name)
    val url = URL("https://www.baidu.com/img/flexible/logo/pc/result.png");
    val imageConnection = url.openConnection() as HttpURLConnection
    imageConnection.requestMethod = "GET"
    imageConnection.connect()
    val inputStream: InputStream = imageConnection.inputStream
    return BitmapFactory.decodeStream(inputStream)
}

lifecycleScope.launch {
    val bitmap = getBitmapByHttp()// The first row
    viewBinding.imageView.setImageBitmap(bitmap)/ / the second line
}
Copy the code

The suspend function is defined to fetch the Bitmap from the network loading image. You then launch a coroutine to lifecycleScope, calling the suspend function inside. As we would expect, the first line is a suspend function, a suspend starting point, which will wait until getBitmapByHttp completes before continuing with the second line, setImageBitmap. First to run after, however, the output current thread “main” and then collapsed, throwing the NetworkOnMainThreadException abnormalities, why the suspend function will run in the main thread here? Because suspend does not know exactly which thread to cut to, it still runs on the main thread. With the above code, Android Studio prompts Redundant ‘suspend’ modifier (more than the suspend modifier). How do you get the suspend function to switch to a specific thread? And that’s where we use withContext.

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope. () - >T
): T
Copy the code

This is the signature of withContext, and you can see that withContext has to pass in the coroutine context and a coroutine block. The coroutine context includes Dispatchers, which specify which thread withContext will be cut into to execute. WithContext is also a suspend function, so when withContext executes, the coroutine that called it will pause, wait until it has cut to the specified thread and finished executing, and then automatically cut back to the calling coroutine and continue executing the coroutine code. This is essentially hanging, automatically cutting away, and then automatically cutting back when it’s done. It’s the same code that I had before, but if I add withContext it’s fine.

private suspend fun getBitmapByHttp(a): Bitmap = withContext(Dispatchers.IO) {
    Log.i("zx"."Current thread" + Thread.currentThread().name)
    val url = URL("https://www.baidu.com/img/flexible/logo/pc/result.png");
    val imageConnection = url.openConnection() as HttpURLConnection
    imageConnection.requestMethod = "GET"
    imageConnection.connect()
    val inputStream: InputStream = imageConnection.inputStream
    BitmapFactory.decodeStream(inputStream)
}

lifecycleScope.launch {
    val bitmap = getBitmapByHttp()
    viewBinding.imageView.setImageBitmap(bitmap)
}
Copy the code

Since withContext can be cut away and back, do not call the outermost lifecyclescope.launch {}, do not start coroutine. Try and find AS error, compilation can not pass, Suspend function ‘getBitmapByHttp’ should be called only from a coroutine or another Suspend function. Mean another hang hang function can only be function calls, or coroutines that another hung function can only another hung in another function calls, or coroutines so dolls, would eventually hangs function calls, can only be in one of the collaborators cheng go so limit because of suspension, cut, cut the back and resume execution of these operations are performed by coroutines framework, You can’t do this if you don’t run it in a coroutine.

If a function is time-consuming, we can define it as a suspended function and use withContext to switch to a non-UI thread so that it does not block the UI thread. The above example also shows the process of customizing a suspend function by adding the suspend keyword to the function and then wrapping the content of the function with a built-in suspend function such as withContext.

Consider that instead of suspend and withContext, we would have to start threads ourselves and use handlers ourselves to communicate between threads. With coroutines, we don’t need to think about any of this, it’s much easier, and more importantly, it doesn’t break the logical structure of the code, so it looks like normal blocking code between two lines of code, but it’s asynchronous and non-blocking, which is what non-blocking means

Summary:

  • hangA thread scheduling operation that cuts off and automatically cuts back to continue execution. This operation is provided by the coroutine so that suspend methods can only be called inside the coroutine.
  • withContext A suspend function provided by a coroutine that cuts to the specified thread executing a block of code and then cuts back.
  • suspend It is only a restriction that restricts the suspended function to be called only in a coroutine, and there is no actual tangent thread
  • Non-blocking typeIt is written like normal blocking code, but it is non-blocking, with no callbacks, no nesting, and no breaking of the logical structure of the code
  • Custom suspend functionAdd the suspend keyword to the function and wrap the content of the function with system suspend functions such as withContext

Simple to use

As in the beginning of this article, we can simply use coroutine +Retrofit to send asynchronous network requests, but without exception handling, we can simply encapsulate it with exception handling and loading display.

Global coroutine exception handling

class GlobalCoroutineExceptionHandler(
    val block: (context: CoroutineContext, exception: Throwable) -> Unit
) :
    CoroutineExceptionHandler {
    override val key: CoroutineContext.Key<*>
        get() = CoroutineExceptionHandler

    override fun handleException(context: CoroutineContext, exception: Throwable) {
        block(context, exception)
    }
}
Copy the code

HandleException here did not actually handle exceptions, exception handling practical approach is to initialize the outside CoroutineExceptionHandler incoming block.

Http request Activity base class

open class HttpActivity : AppCompatActivity() {
    val httpInterface: HttpInterface = RetrofitFactory.httpInterface
    private var progress: ProgressDialog? = null
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
    }

    inline fun launchMain(
        crossinline block: suspend CoroutineScope. () - >Unit
    ) {
        val job = lifecycleScope.launch(GlobalCoroutineExceptionHandler(::handException)) {
            showProgress()
            block()
        }

        job.invokeOnCompletion {
            hideProgress()
        }
    }

    fun showProgress(a) {
        if (progress == null) {
            progress = ProgressDialog.show(
                this.""."Loading, please wait...".false.true)}}fun hideProgress(a) {
        if(progress ! =null&& progress!! .isShowing) { progress!! .dismiss() progress =null}}open fun handException(context: CoroutineContext, exception: Throwable) {
        var msg = ""
        if (exception is HttpException) {
            msg = when (exception.code()) {
                404- > {"$localClassName- Abnormal, 404 requested, requested resource does not exist"
                }

                500- > {"$localClassName- Abnormal, request 500, internal server error."
                }
                500- > {"$localClassName- Abnormal, request 401, identity authentication failed."
                }
                else- > {"$localClassName- The HTTP request is abnormal.${exception.response()}"}}}else {
            msg = "$localClassName- Abnormal.${exception.stackTraceToString()}"

        }
        Log.e("zx", msg)

        hideProgress()
        Snackbar.make(
            window.decorView,
            msg,
            Snackbar.LENGTH_LONG
        )
            .show()
    }
}
Copy the code

Defines a launchMain function, launchMain unified open coroutines, unified set CoroutineExceptionHandler, and to display circular progress bar in the request, the request after the hidden progress bar.

Defines a handException function, which is the actual exception handling function that handles some common exceptions and is displayed via Snackbar. The inheritance used here, you can also use extension functions to implement.

If you want to handle exceptions yourself, implement handException.

override fun onCreate(savedInstanceState: Bundle?). {
    super.onCreate(savedInstanceState)
    launchMain {
        val result = httpInterface.searchPhotos("china")
        / / update the UI}}Copy the code

Of course, this is a simple use that does not combine ViewModel and LiveData. If you do not need MVVM and only need to send requests in your Activity, you might consider using this method.