The use of Kotlin coroutines

Foreword

This article is the learning record of the Kotlin coroutine on the Android official website. Record the features, applications, etc. of Kotlin Coroutines on Android

Overview of coroutines

1. What is a coroutine?

Coroutine is a concurrent design pattern that can be used to simplify asynchronously executed code, and it can help manage some time-consuming tasks to prevent time-consuming tasks from blocking the main thread. Coroutines can write asynchronous code in a synchronous manner, instead of the traditional callback method, making the code more readable.

About coroutine scope: coroutines must run in CoroutineScope (coroutine scope), a CoroutineScope manages one or more related coroutines. For example, there is viewModelScope under the viewmodel-ktx package, viewModelScope manages the coroutines started through it, if viewModel is destroyed , then viewModelScope will be canceled automatically, and the running coroutine started by viewModelScope will also be cancelled.

Suspend and resume

The coroutine has suspend and resume:

  • suspend (Suspend): Suspend the execution of the current coroutine and save all local variables.
  • resume (resume): Used to allow the suspended coroutine to continue execution from where it was suspended.

There is a suspend keyword in the coroutine, which needs to be distinguished from the suspend concept just mentioned. The suspend (suspend) just mentioned is a concept, and the suspend keyword can modify a function, but only this keyword has no effect on suspending the coroutine. Generally, the suspend keyword is Remind the caller that the function needs to run directly or indirectly under the coroutine, which acts as a mark and reminder.

What is the function of

suspend keyword mark and reminder? In the past, it was difficult for developers to judge whether a method was time-consuming. If a time-consuming method was called on the main thread by mistake, it would cause the main thread to freeze. With the suspend keyword, time-consuming The creator of the time-consuming function can modify the time-consuming method with the suspend keyword, and put the time-consuming code inside the method using withContext{Dispatchers.IO}, etc. into the IO thread, etc. are running, developers only need to call it directly or indirectly under the coroutine, so as to avoid time-consuming tasks running in the main thread and causing the main thread to freeze.

The following is an official example to illustrate the two concepts of coroutine suspend and resume:

suspend fun fetchDocs() { // Dispatchers. Main
    val result = get("https://developer.android.com") // Dispatchers.IO for `get`
    show(result) // Dispatchers. Main
}

suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }

We assume that the fetchDocs method is called in the coroutine, which provides a main thread environment (such as specified by Dispatchers.Main when starting the coroutine), and , the get method executes time-consuming tasks, it uses the suspending function withContext{Dispatchers.IO} to put the time-consuming tasks in the IO thread for execution .

In the fetchDocs method, when the get method starts to make network requests, it will hang (suspend ), when the network request is completed, get will resume (resume) the suspended coroutine instead of using a callback to notify the main thread .

Kotlin uses the stack frame (stack frame) to manage the running function and its local variables. When suspending a coroutine, the system will Copy and save the current stack frame for later use. When the coroutine resumes, the stack frame is copied back from where it was saved, and the function starts running again.

Scheduler

Kotlin coroutines must run in dispatcher, coroutines can suspend themselves, and dispatcher is responsible forresume them.

There are three kinds of Dispatcher:

  • Dispatchers.Main: Run coroutines on the main thread.
  • Dispatchers.IO: This dispatcher is optimized for performing disk or network I/O.
  • Dispatchers.Default: The dispatcher is suitable for performing tasks that consume a lot of CPU resources (sorting lists and parsing JSON), and is optimized.

Start a coroutine

There are two ways to start a coroutine:

  • launch: Start a new coroutine, the return value of launch is Job, the execution result of the coroutine will not be returned to caller.
  • async: Start a new coroutine, the return value of async is Deferred, Deferred Inherited from Job, the execution result of the coroutine can be obtained by calling Deferred::await, where await is the suspending function.

Initiate a coroutine in a regular function, usually using launch, because regular functions cannot call Deferred::await, you can use async to start a coroutine inside a coroutine or suspend function.

launch (return Job) and async (return Deferred) difference:

  1. launch starts the coroutine without returning a result; async starts the coroutine with a return result, which can be obtained through the await method of Deferred.
  2. launch The abnormality of the coroutine started will be thrown immediately; the exception of the coroutine started by async will not be thrown immediately, it will wait until the callDeferred::await will throw an exception.
  3. async is suitable for the execution of some concurrent tasks. For example, there is such a business: make two network requests, and display the request results together after both requests are completed. Using async can be achieved like this
interface IUser {
    @GET("/users/{nickname}")
    suspend fun getUser(@Path("nickname") nickname: String): User

    @GET("/users/{nickname}")
    fun getUserRx(@Path("nickname") nickname: String): Observable
}
val iUser = ServiceCreator.create(IUser::class.java)
GlobalScope.launch(Dispatchers.Main) {
    val one = async {
        Log.d(TAG, "one: ${threadName()}")
        iUser. getUser("giagor")
    }
    val two = async {
        Log.d(TAG, "two: ${threadName()}")
        iUser. getUser("google")
    }
    Log.d(TAG, "giagor:${one.await()} , google:${two.await()} ")
}

Coroutine concept

CoroutineScope

CoroutineScope will track all the coroutines it creates using launch or async, you can call scope.cancel()Cancel all running coroutines under this scope. In ktx, we provide some well-defined CoroutineScope, such as viewModelScope of ViewModel, <code lifecycleScope of >Lifecycle, for details, please refer to Android KTX | Android Developers.

viewModelScope will be canceled in ViewModel’s onCleared() method

You can create your own CoroutineScope, as follows:

class MainActivity : AppCompatActivity() {
    val scope = CoroutineScope(Job() + Dispatchers. Main)
    
    override fun onCreate(savedInstanceState: Bundle?) {
    ...
        scope. launch {
            Log.d(TAG, "onCreate: ${threadName()}") // main
            fetchDoc1()
        }
        
        scope. launch {
            ...
        }
    }
    
    suspend fun fetchDoc1() = withContext(Dispatchers.IO) {...}
    
    override fun onDestroy() {
        scope. cancel()
        super. onDestroy()
    }
}

When creating scope, combine Job and Dispatcher as a CoroutineContext, as CoroutineScope. When scope.cancel, all coroutines opened by scope will be automatically canceled, and scope cannot be used to open coroutines afterwards ( No error will be reported but the coroutine opening is invalid).

You can also cancel the coroutine by passing in Job of CoroutineScope:

 val job = Job()
    val scope = CoroutineScope(job + Dispatchers. Main)

    scope. launch {...}
...
job. cancel()

Use Job to cancel the coroutine, and then you cannot start the coroutine through scope.

In fact, looking at the source code, you can find that the CoroutineScope.cancel method is cancel through Job:

public fun CoroutineScope. cancel(cause: CancellationException? = null) {
    val job = coroutineContext[Job] ?: error("Scope cannot be canceled because it does not have a job: $this")
    job. cancel(cause)
}

The cancellation of the coroutine will be introduced later.

Job

When we use launch or async to create a coroutine, we will get a Job instance, this Job instance uniquely identifies the coroutine and manages the life cycle of the coroutine. Job is somewhat similar to the Thread class in Java.

JavaPart of the method of the Thread class:

It can manage the created threads.

Job class also has some extension functions as follows:

Job life cycle: New, Active, Completing, Completed, Canceling, Canceled. Although we can't access the state itself, we can call the isActive, isCancelled, isCompleted methods of Job

When the coroutine is Active, the coroutine failure or calling the Job.cancel method will make Job enter Cancelling Status (isActive = false, isCancelled = true). Once all child coroutines have finished their work, the outer coroutine will enter the Cancelled state with isCompleted = true.

CoroutineContext

CoroutineContext uses the following elements to define the behavior of coroutines:

  • Job: Controls the lifecycle of the coroutine.

  • CoroutineDispatcher: Dispatches work to the appropriate thread.

    The default is Dispatchers.Default

  • CoroutineName: The name of the coroutine, which can be used for debugging.

    The default is "coroutine"

  • CoroutineExceptionHandler: Handle uncaught exceptions.

For new coroutines created within the scope, the system will allocate a new Job instance for the new coroutine, and inherit other CoroutineContext from the scope containing the coroutine > element. Inherited elements can be replaced by passing a new CoroutineContext to the launch or async function. Note that passing a Job to launch or async has no effect as the system always assigns a Job to the new coroutine A new instance of .

Example:

val scope = CoroutineScope(Job() + Dispatchers. Main + CoroutineName("Top Scope"))

scope.launch(Dispatchers.IO) {
    Log.d(TAG, "onCreate: ${coroutineContext[CoroutineName]}")
}
D/abcde: onCreate: CoroutineName(Top Scope)

The newly created coroutine inherits elements such as CoroutineName from the external scope, but note that the CoroutineDispatcher element is rewritten, in In the newly created coroutine, the CoroutineDispatcher element is specified as Dispatchers.IO.

Access elements in coroutines

A coroutine can be started by launch or async of CoroutineScope:

public fun CoroutineScope.launch(...)
public fun  CoroutineScope. async(...)

There are coroutine context elements in CoroutineScope:

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

So you can directly access the coroutine context element in the coroutine:

scope.launch {
    coroutineContext
...
}

There are also some convenient extension functions, such as:

public val CoroutineScope.isAactive: Boolean
    get() = coroutineContext[Job]?.isActive?: true

In this way, the status of Job can be obtained directly in the coroutine:

scope.launch {
    isActive
...
}

Hierarchical relationship of coroutines

Each coroutine has a parent, this parent can be CoroutineScope, or another coroutine.

CoroutineContext calculation formula for creating a new coroutine:

Defaults + inherited CoroutineContext + arguments + Job()

If there are repeated coroutine context elements, the elements to the right of the + sign will overwrite the elements to the left of the + sign. The meaning of each part of the formula is as follows:

  • Defaults: such as the default Dispatchers.Default (CoroutineDispatcher) and "coroutine" (CoroutineName)
  • inherited CoroutineContext: CoroutineContext elements inherited from CoroutineScope or Coroutine
  • arguments: Elements passed in through the coroutine constructor launch or async
  • Job(): The new coroutine can always get a new Job instance

Avoid GlobalScope

In the official document, the use of GlobalScope, three reasons are given:

  • (1) Promotes hard-coding values. If you hardcode GlobalScope, you might be hard-coding Dispatchers as well.
  • (2) Makes testing very hard as your code is executed in an uncontrolled scope, you won't be able to control its execution.
  • (3) You can't have a common CoroutineContext to execute for all coroutines built into the scope itself.

The explanation of the second and third points is as follows: CoroutineScope created by ourselves can perform structured concurrent operations, for example, we can call CoroutineScope .cancel to cancel all running coroutines under this scope, the method of cancel is as follows:

public fun CoroutineScope. cancel(cause: CancellationException? = null) {
    val job = coroutineContext[Job] ?: error("Scope cannot be canceled because it does not have a job: $this")
    job. cancel(cause)
}

It first gets CoroutineContext internally's Job, and then there is the cancel method of Job to realize the cancellation of the coroutine. The CoroutineContext of CoroutineScope that we manually created all has Job, for example:

val scope = CoroutineScope(Job() + Dispatchers. Main + CoroutineName("Top Scope"))

Its construction method is:

public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

In the construction method, if the incoming CoroutineContext does not have a Job, a Job will be created and added to the CoroutineContext > in. But GlobalScope is global (singleton), its CoroutineContext is an EmptyCoroutineContext, there is no Job member

public object GlobalScope : CoroutineScope {
    /**
     * Returns [EmptyCoroutineContext].
     */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

When we call GlobalScope.launch, we can specify the CoroutineContext of the coroutine launched this time. When we call GlobalScope.cancel(), the following error will be reported:

java.lang.IllegalStateException: Scope cannot be canceled because it does not have a job: kotlinx.coroutines.GlobalScope@11b671b

It can be seen that the reason for the error is because GlobalScope does not have Job.

Coroutine cancellation

Cancel a single coroutine through Job, but does not affect other coroutines under this scope:

// assume we have a scope defined for this layer of the app
val job1 = scope. launch { … }
val job2 = scope. launch { … }
// First coroutine will be canceled and the other one won't be affected
job1. cancel()

Through the coroutine scope, all coroutines under this scope can be canceled (once the coroutine scope is cancelled, this scope cannot be used to start new coroutines):

// assume we have a scope defined for this layer of the app
val job1 = scope. launch { … }
val job2 = scope. launch { … }
scope. cancel()

You can use the cancel method to cancel a coroutine:

# Job.kt
public fun cancel(): Unit = cancel(null)

If you want to provide more information or reasons for the cancellation, you can call the following method and pass in a CancellationException yourself:

# Job.kt
public fun cancel(cause: CancellationException? = null)

The cancel method without parameters actually calls the cancel method with the CancellationException parameter. If you use the cancel method without parameters, the default CancellationException provided by the system will be used:

# Job.kt
public fun cancel(): Unit = cancel(null)

public fun cancel(cause: CancellationException? = null)

#JobSupport.kt
public override fun cancel(cause: CancellationException?) {
    cancelInternal(cause?: defaultCausencellationException())
}

internal inline fun defaultCancellationException(message: String? = null, cause: Throwable? = null) =
JobCancellationException(message?: cancellationExceptionMessage(), cause, this)

The original words of the official document:

Cancellation in coroutines is cooperative, which means that when a coroutine's Job is canceled, the coroutine isn't canceled until it suspends or checks for cancellation. If you do blocking operations in a coroutine, make sure that the coroutine is cancelable.

It can be concluded that:

  1. The cancellation of the coroutine is cooperative
  2. External cancellation of the currently running coroutine, the coroutine will not be canceled immediately, when the following two situations The coroutine will only be canceled when one of
    • The cooperation check of the coroutine is canceled in cooperation, which is similar to stopping the execution of a thread (the cooperation check of the thread is required).
    • When the coroutine suspend, the coroutine will also be canceled.

Coroutines can handle cancellation by throwing CancellationException

Active inspection

For example:

val scope = CoroutineScope(Job() + Dispatchers.IO + CoroutineName("Top Scope"))

bn1.setOnClickListener {
scope. launch {
        Thread. sleep(2000)
        Log.d(TAG, "onCreate: $isActive")
        Log.d(TAG, "onCreate: ${threadName()},${coroutineContext[CoroutineName]?.name}")
}
}

bn2.setOnClickListener {
scope. cancel()
}

If we only click bn1 to start the coroutine, but do not click bn2 to cancel the coroutine, then the output is

D/abcde: onCreate: true
D/abcde: onCreate: DefaultDispatcher-worker-1, Top Scope

Suppose we click bn1 to start the coroutine, and immediately click bn2 to cancel the coroutine (the coroutine is still in the period of Thread.sleep ), then the output is

D/abcde: onCreate: false
D/abcde: onCreate: DefaultDispatcher-worker-2, Top Scope

You can see that the value of isActive of the coroutine becomes false, but the coroutine will still be executed (although scope cannot be passed afterwards Then start a new coroutine).

In the above code, when calling scope.cancel (internally calling job.cancel), the coroutine will enter Cancelling state, when all the work in the coroutine is completed, the coroutine will enter the Cancelled state.

In the above example, scope.cancel has been called, but the current coroutine is still running, which means that the actual cancellation of the coroutine needs the internal cooperation of the coroutine, and one of the methods is to call ensureActive() function, the function of ensureActive is roughly equivalent to:

if (!isActive) {
    throw CancellationException()
}

Let's modify the example above:

val scope = CoroutineScope(Job() + Dispatchers.IO + CoroutineName("Top Scope"))

bn1.setOnClickListener {
scope. launch {
        Thread. sleep(2000)
        Log.d(TAG, "onCreate: $isActive")
        // Check if the coroutine is canceled
        ensureActive()
        Log.d(TAG, "onCreate: ${threadName()},${coroutineContext[CoroutineName]?.name}")
}
}

bn2.setOnClickListener {
scope. cancel()
}

After we click bn1 to start the coroutine, we immediately click bn2 to cancel the coroutine (the coroutine is still in the Thread.sleep period) , then the output is

D/abcde: onCreate: false

It can be seen that the ensureActive() function inside the current coroutine cooperates with the external cancel operation to successfully cancel the coroutine.

Of course, other methods can also be used to perform cooperative cancellation within the coroutine.

The coroutine is suspended

After the external coroutine cancel, when the running coroutine is suspend, the coroutine will also be cancelled.

Reform the above example:

val scope = CoroutineScope(Job() + Dispatchers.IO + CoroutineName("Top Scope"))

bn1.setOnClickListener {
    scope. launch {
        Thread. sleep(2000)
        Log.d(TAG, "onCreate: $isActive")
        withContext(Dispatchers. Main) {
            Log.d(TAG,
                  "onCreate: ${threadName()},${coroutineContext[CoroutineName]?.name}")
        }
    }
}

bn2.setOnClickListener {
scope. cancel()
}

If we only click bn1 to start the coroutine, but do not click bn2 to cancel the coroutine, then the output is

D/abcde: onCreate: true
D/abcde: onCreate: main, Top Scope

Suppose we click bn1 to start the coroutine, and immediately click bn2 to cancel the coroutine (the coroutine is still in the period of Thread.sleep ), then the output is

D/abcde: onCreate: false

It can be seen that when withContext suspend the current coroutine, the coroutine is canceled.

All suspend functions in

kotlinx.coroutines are cancelable (cancellable), such as withContext and delay (in the above example, instead of using withContext, the delay function can also be used to cancel the coroutine).

This is because the coroutine dispatcher CoroutineDispatcher will check the status of the job corresponding to the coroutine before continuing the normal execution. If the job is canceled, the CoroutineDispatcher will terminate the normal execution and call the corresponding cancellation handlers. The following is a Example:

var job: Job? = null

// start coroutine
binding.start.setOnClickListener {
    job = scope. launch {
        withContext(Dispatchers.IO){
            Thread. sleep(1000)
            Log.d(TAG, "1")
        }
        Log.d(TAG, "2")
    }
}

// cancel coroutine
binding.cancel.setOnClickListener {
    job?. cancel()
}

Click the button to start the coroutine first, and then click the button to cancel the coroutine during the Thread.sleep execution of the coroutine, then the output is:

D/abcde: 1

In addition, using the yield() function can also cancel the coroutine response. The reason will be introduced in the "Other suspending functions" section later.

Job.join vs Deferred.await

Job.join

Job.join can suspend the current coroutine until another coroutine is executed.

Job.join and Job.cancel:

  • If Job.cancel is called and then Job.join is called, the current coroutine will be suspended until the coroutine corresponding to the Job is executed.
  • If Job.join is called and then Job.cancel is called, the call of Job.cancel will not have any effect, because when Job.join is executed, the coroutine corresponding to the Job has been completed. At this time, the execution of Job.cancel will not have any impact.

Example 1:

var job :Job? = null

// Click the start button first
binding.start.setOnClickListener {
    job = scope. launch {
        Thread. sleep(1000)
        Log.d(TAG, "coroutine 1")
    }
}

// Then click the cancel button
binding.cancel.setOnClickListener {
    scope. launch {
        job?. cancel()
        job?. join()
        Thread. sleep(1000)
        Log.d(TAG, "coroutine 2")
    }
}
D/abcde: Coroutine 1
D/abcde: Coroutine 2

Example 2 (exchange the execution order of cancel and join):

var job : Job? = null
binding.start.setOnClickListener {
    job = scope. launch {
        Thread. sleep(1000)
        Log.d(TAG, "coroutine 1")
    }
}

binding.cancel.setOnClickListener {
    scope. launch {
        job?. join()
        job?. cancel()
        Thread. sleep(1000)
        Log.d(TAG, "coroutine 2")
    }
}
D/abcde: Coroutine 1
D/abcde: Coroutine 2

Deferred. await

by aWhen sync starts a coroutine, it will return a Deferred, and the execution result of the coroutine can be obtained through the await function of the Deferred.

If you call deferred.await after calling deferred.cancel, then the call of deferred.await will throw a JobCancellationException exception, as follows:

var deferred : Deferred? = null

binding.start.setOnClickListener {
    // Coroutine 1
deferred = scope. async {
        Thread. sleep(1000)
        Log.d(TAG, "print 1")
        1    
}
}

binding.cancel.setOnClickListener {
    // Coroutine 2
    scope. launch {
        deferred?. cancel()
        deferred?. await()
        Log.d(TAG, "after cancel")
    }
}
  • If you start coroutine 2 before the execution of coroutine 1 is completed, cancel and then await the coroutine, then the await call will throw a JobCancellationException Exception, which will end the operation of coroutine 2.
Output:
D/abcde: print 1

The reason for throwing an exception: the function of await is to wait for the calculation result of coroutine 1, and the coroutine 1 is canceled due to calling deferred.cancel, so call deferred.awaCoroutine 1 cannot calculate the result during it, so an exception will be thrown.

  • If coroutine 2 is started after coroutine 1 is executed, deferred.cancel has no effect because coroutine 1 has been executed at this time, and calling deferred.await afterwards will not throw an exception , everything works fine.
Output:
D/abcde: print 1
D/abcde: after cancel

Reverse the call order of cancel and await in the above code, namely:

var deferred : Deferred? = null

binding.start.setOnClickListener {
    // Coroutine 1
deferred = scope. async {
        Thread. sleep(1000)
        Log.d(TAG, "print 1")
        1    
}
}

binding.cancel.setOnClickListener {
    // Coroutine 2
    scope. launch {
        deferred?. await()
        deferred?. cancel()
        Log.d(TAG, "after cancel")
    }
}
Output:
D/abcde: print 1
D/abcde: after cancel

Because of calling await, coroutine 2 will wait for coroutine 1 to finish executing. When cancel is called, since coroutine 1 has already finished executing, the cancel function will not produce any effect at this time.

Clean up resources

When the coroutine is canceledAt this time, if you want the coroutine to respond to cancellation and clean up resources, there are two ways:

  • Manually check whether the coroutine is Active, so as to control the execution of the coroutine
  • Use the Try Catch Finally statement block

1. Manually check whether the coroutine is Active, so as to control the execution of the coroutine

while (i < 5 && isActive) {
    // print a message twice a second
    if (…) {
        println("Hello ${i++}")
        nextPrintTime += 500L
    }
}
// the coroutine work is completed so we can cleanup
println("Clean up!")

When the coroutine is not in the Active state, exit the while loop, do some resource cleaning actions, and then end the coroutine.

Second, use the Try Catch Finally statement block

suspend fun work(){
    val startTime = System. currentTimeMillis()
    var nextPrintTime = startTime
    var i = 0
    while (i < 5) {
        yield()
        // print a message twice a second
        if (System. currentTimeMillis() >= nextPrintTime) {
            println("Hello ${i++}")
            nextPrintTime += 500L
        }
    }
}
fun main(args: Array) = runBlocking {
   val job = launch (Dispatchers. Default) {
        try {
        work()
        } catch (e: CancellationException){
            println("Work canceled!")
        } finally {
            println("Clean up!")
        }
    }
    delay(1000L)
    println("Cancel!")
    job. cancel()
    println("Done!")
}

When the work function checks that the coroutine is not Active, it will throw a CancellationException. You can use the Try Catch statement block to catch the exception, and then in Finally block to do resource cleanup actions.

Note: When the coroutine is in the Cancelling state, the coroutine cannot be suspended. If you call the suspend function to try to suspend the coroutine at this time, the execution of the coroutine will be delayed. Finish. This means that if the coroutine is canceled, the coroutine's resource cleanup code cannot call the suspend function to suspend the coroutine. as follows

fun main(args: Array) = runBlocking {
    val job = launch {
        try {
            work()
        } catch (e: CancellationException) {
            println("Work canceled!")
        } finally {
            delay(2000L) // or some other suspend fun
            println("Cleanup done!") // no output
        }
    }
    delay(1000L)
    println("Cancel!")
    job. cancel()
    println("Done!")
}
Hello 0
Hello 1
Cancel!
Done!
Work canceled!

But there is a special method that can still execute the code that suspends the coroutine when the coroutine is canceled. We need to put the "code that suspends the coroutine" in NonCancellable CoroutineContext Execute below, the specific method is to modify the statement of the Finally block to:

 withContext(NonCancellable) {
        delay(2000L) // or some other suspend fun
        println("Cleanup done!") // successful output
    }
Hello 0
Hello 1
Cancel!
Done!
Work canceled!
Cleanup done!

Exception handling

For the exception in the coroutine, you can use try...catch... to catch it, or you can use CoroutineExceptionHandler.

CoroutineExceptionHandler is one of CoroutineContext

Use try...catch... in the coroutine to catch exceptions:

class LoginViewModel(
    private val loginRepository: LoginRepository
) : ViewModel() {

    fun login(username: String, token: String) {
        viewModelScope. launch {
            try {
                loginRepository. login(username, token)
                // Notify view user logged in successfully
            } catch (error: Throwable) {
                // Notify view login attempt failed
            }
        }
    }
}

Other suspending functions

coroutineScope

Suspend function coroutineScope: create a CoroutineScope, and call a specific suspend block in this scope>, the created CoroutineScope inherits the CoroutineContext of the external scope (Job in CoroutineContext will be overwritten).

This function is designed for parallel decomposition, when any child coroutine fail of this scope, this scopeOther sub-coroutines in it will also fail, and this scope will also fail (it feels a bit structured and concurrent).

When using coroutineScope, the external coroutine will be suspended until the code in coroutineScope and the coroutine in scope At the end of the execution, the external coroutine of the suspended function coroutineScope will resume execution.

An example:

 GlobalScope. launch(Dispatchers. Main) {
        fetchTwoDocs()
        Log.d(TAG, "Under fetchTwoDocs()")
    }

    suspend fun fetchTwoDocs() {
        coroutineScope {
            Log.d(TAG, "fetchTwoDocs: ${threadName()}")
            val deferredOne = async {
                Log.d(TAG,"async1 start: ${threadName()}")
                fetchDoc1()
                Log.d(TAG, "async1 end: ${threadName()}")
            }
            val deferredTwo = async {
                Log.d(TAG, "async2: start:${threadName()}")
                fetchDoc2()
                Log.d(TAG, "async2 end: ${threadName()}")
            }
            deferredOne. await()
            deferredTwo. await()
        }
    }

    suspend fun fetchDoc1() = withContext(Dispatchers.IO) {
        Thread. sleep(2000L)
    }

    suspend fun fetchDoc2() = withContext(Dispatchers.IO) {
        Thread. sleep(1000L)
    }
D/abcde: fetchTwoDocs: main
D/abcde: async1 start: main
D/abcde:async2:start:main
D/abcde: async2 end: main
D/abcde: async1 end: main
D/abcde: Under fetchTwoDocs()

Several points of interest:

  1. Under fetchTwoDocs() is executed after fetchTwoDocsAfter finishing, the code in
  2. coroutineScope runs in the main thread
  3. async runs in main thread, because the scope created by coroutineScope will inherit the CoroutineContext of the external GlobalScope.launch.

Even if the above code does not call deferredOne.await(), deferredTwo.await(), the execution and output results are the same.

suspendCoroutine

/**
 * Obtains the current continuation instance inside suspend functions and suspends
 * the currently running coroutine.
 *
 * In this function both [Continuation.resume] and [Continuation.resumeWithException] can be used either synchronously in
 * the same stack-frame where the suspension function is run or asynchronously later in the same thread or
 * from a different thread of execution. Subsequent invocation of any resume function will produce an [IllegalStateException].
 */
@SinceKotlin("1.3")
@InlineOnly
public suspend inline fun  suspend Coroutine(crossinline block: (Continuation) -> Unit): T {
    contract { callsInPlace(block, InvocationKind. EXACTLY_ONCE) }
    return suspendCoroutineUninterceptedOrReturn { c: Continuation ->
        val safe = SafeContinuation(c. intercepted())
        block (safe)
        safe. getOrThrow()
    }
}

suspendCoroutine is a behavior that actively suspends the coroutine, it will give you a Continuation, allowing you to decide when to resume the execution of the coroutine.

suspendCancellableCoroutine

Same as suspendCoroutine, suspendCancellableCoroutine can also suspend the coroutine. The difference between them is that suspendCancellableCoroutine does some processing and support for the cancellation of the coroutine.

When the coroutine is suspended, If the job of the coroutine is canceled, the coroutine will not be able to resume successfully, and the suspendCancellableCoroutine function will throw a CancellationException > .

The suspendCancellableCoroutine function is defined as follows:

public suspend inline fun  suspendCancellableCoroutine(
    crossinline block: (CancellableContinuation) -> Unit
): T =
    suspendCoroutineUninterceptedOrReturn { uCont ->
        val cancellable = CancellableContinuationImpl(uCont. intercepted(), resumeMode = MODE_CANCELLABLE)
        /*
         * For non-atomic cancellation we setup parent-child relationship immediately
         * in case when `block` blocks the current thread (e.g. Rx2 with trampoline scheduler), but
         * properly supports cancellation.
         */
        cancellable.initCancellability()
        block (cancellable)
        cancellable. getResult()
    }

It can be seen that it provides a CancellableContinuation parameter for the incoming Lambda. CancellableContinuation provides the invokeOnCancellation function. When a suspended coroutine is canceled, the code in the invokeOnCancellation function will be executed strong>:

 suspendCancellableCoroutine { continuation ->
         val resource = openResource() // Opens some resourcecontinuation. invokeOnCancellation {
             resource.close() // Ensures the resource is closed on cancellation
         }
         //...
     }

When using CancellableContinuation to resume the coroutine, that is, when calling its resume function, an additional onCancellation parameter can be passed in:

suspendCancellableCoroutine { continuation ->
    val callback = object : Callback { // Implementation of some callback interface
        // A callback provides a reference to some closeable resource
        override fun onCompleted(resource: T) {
            // Resume coroutine with a value provided by the callback and ensure the
            // resource is closed in case when the coroutine is canceled before the
            // caller gets a reference to the resource.
            continuation.resume(resource) { // The additional onCancellation parameter passed in is a Lambda
                resource.close() // Close the resource on cancellation
            }
        }
    }
    //...
}

If the coroutine is canceled before the resume of the continuation is executed, the caller will not be able to obtain a reference to the resource, and the caller will not be able to close the resource resource. At this time, the onCancellation parameter comes in handy. It can close the resource resource when the above situation occurs.

yield

If you want the current coroutine to yield the thread, you can use the suspending function yield().

As mentioned before, the yield function can respond to the cancellation of the coroutine, because the first operation in the yield function is to check the status of the coroutine , a CancellationException will be thrown when the coroutine is not Active:

public suspend fun yield(): Unit = suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
    val context = uCont.context
    context. checkCompletion()
    ...
}

internal fun CoroutineContext. checkCompletion() {
    val job = get(Job)
    if (job != null && !job.isActive) throw job.getCancellationException()
}

Reference

  1. Kotlin coroutines on Android | Android Developers.

  2. Coroutines: first things first. Cancellation and Exceptions in... | by Manuel Vivo | Android Developers | Medium.

Leave a Reply

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