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
: Thisdispatcher
is optimized for performing disk or networkI/O
.Dispatchers.Default
: Thedispatcher
is suitable for performing tasks that consume a lot ofCPU
resources (sorting lists and parsingJSON
), and is optimized.
Start a coroutine
There are two ways to start a coroutine:
launch
: Start a new coroutine, the return value oflaunch
isJob
, the execution result of the coroutine will not be returned to caller.async
: Start a new coroutine, the return value ofasync
isDeferred
,Deferred
Inherited fromJob
, the execution result of the coroutine can be obtained by callingDeferred::await
, whereawait
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:
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.launch
The abnormality of the coroutine started will be thrown immediately; the exception of the coroutine started byasync
will not be thrown immediately, it will wait until the callDeferred::await
will throw an exception.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. Usingasync
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
.
Java
Part 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-codingDispatchers
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:
- The cancellation of the coroutine is cooperative
- 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 thenawait
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 ofCoroutineContext
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 scope
Other 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:
Under fetchTwoDocs()
is executed afterfetchTwoDocs
After finishing, the code incoroutineScope
runs in the main threadasync
runs inmain
thread, because thescope
created bycoroutineScope
will inherit theCoroutineContext
of the externalGlobalScope.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
-
Kotlin coroutines on Android | Android Developers.
-
Coroutines: first things first. Cancellation and Exceptions in... | by Manuel Vivo | Android Developers | Medium.