Getting to Know Kotlin Coroutines

Coroutines allow us to write asynchronous code in a sequential manner without blocking the UI thread.
Kotlin coroutines provide a new way to handle concurrency, and you can use it on the Android platform to simplify asynchronously executed code. Coroutines have been introduced since version 1.3 of Kotlin, but the concept has been around since the dawn of the programming world. The earliest programming languages ​​using coroutines can be traced back to the Simula language in 1967. In the past few years, the concept of coroutines has developed rapidly and has been adopted by many mainstream programming languages, such as Javascript, C#, Python, Ruby, and Go. Kotlin coroutines are based on established concepts from other languages.
Google officially recommends Kotlin coroutines as a solution for asynchronous programming on Android.

Configuration

dependencies {
    // Lifecycle's extended encapsulation of coroutines
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
    // coroutine core library
    implementation"org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0"
    // coroutine Android support library
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0"
}

CoroutineScope

CoroutineScope is the coroutine scope, which is used to track coroutines. If we start multiple coroutines but there is no way to manage them uniformly, it will cause our code to be bloated and messy, and even memory leaks or task leaks will occur. To ensure that all coroutines are tracked, Kotlin does not allow coroutines to be started without a CoroutineScope.
CoroutineScope will track all coroutines, and it can also cancel all coroutines started by it. This is very useful in Android development, for example, it can stop the execution of the coroutine when the user leaves the interface.
Coroutine is a lightweight thread, which does not mean that it does not consume system resources. When the asynchronous operation is time-consuming, or when an error occurs in the asynchronous operation, the Coroutine needs to be canceled to release system resources. In the Android environment, the Coroutine started by each interface (Activity, Fragment, etc.) is only meaningful in the interface. If the user exits the interface while waiting for the Coroutine to execute, it may not be necessary to continue executing the Coroutine. In addition, Coroutine also needs to be executed in an appropriate context, otherwise errors will occur, such as accessing View in a non-UI thread. Therefore, when the Coroutine is designed, it is required to be executed within a scope (Scope), so that when the Scope is cancelled, all the sub-Coroutines in it are also automatically cancelled. So to use Coroutine, you must first create a corresponding CoroutineScope.

1. GlobalScope – global coroutine scope

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

GlobalScope is a singleton implementation that contains an empty CoroutineContext and does not contain any Job. This scope is often used as a sample code.
GlobalScope belongs to the global scope, which means that the life cycle of the coroutine started by GlobalScope is only limited by the life cycle of the entire application. As long as the entire application is still running and the task of the coroutine has not ended, the coroutine The program can run forever, which may cause memory leaks, so it is not recommended.

2. MainScope – main thread coroutine scope

/**
 * Creates the main [CoroutineScope] for UI components.
 *
 * Example of use:
 * ```
 * class MyAndroidActivity {
 * private val scope = MainScope()
 *
 * override fun onDestroy() {
 * super.onDestroy()
 * scope. cancel()
 * }
 * }
 * ```
 *
 * The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements.
 * If you want to append additional elements tothe main scope, use [CoroutineScope.plus] operator:
 * `val scope = MainScope() + CoroutineName("MyActivity")`.
 */
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

is used to return a scope whose CoroutineContext is SupervisorJob() + Dispatchers.Main. The default scheduling mode is Main, which means that the thread environment of the coroutine is the main thread.
This scope is often used in Activity/Fragment, and when the interface is destroyed, the CoroutineScope.cancel() method must be called in onDestroy() to cancel the coroutine. This is a method that can be used in development to obtain The top-level function of the scope.

3, runBlocking

This is a top-level function that runs a new coroutine and blocks the current interruptible thread until the coroutine execution completes. This function is designed to bridge common blocking code to libraries written in suspending style , for the main function and tests. This function is mainly used for testing, not for daily development. This coroutine will block the current thread until the execution of the coroutine body is completed.

4. LifecycleOwner. lifecycleScope

/**
 * [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle].
 *
 * This scope will be canceled when the [Lifecycle] is destroyed.
 *
 * This scope is bound to
 * [Dispatchers. Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
 */
public val LifecycleOwner. lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

SupervisorJob() + Dispatchers.Main.immediate
This extended attribute is a lifecycle-aware coroutine scope provided by Android’s Lifecycle Ktx library, which is used to return a CoroutineContext as SupervisorJob() + Dispatchers.Main The scope of .immediate, which is bound to the Lifecycle of the LifecycleOwner, will be canceled when the Lifecycle is destroyed. This is the recommended scope in Activity/Fragment, because it will bind the life cycle with the current UI component, and the scope of the coroutine will be canceled when the interface is destroyed, which will not cause a leak of the coroutine.

5. ViewModel. viewModelScope

/**
 * [CoroutineScope] tied to this [ViewModel].
 * This scope will be canceled when ViewModel will be cleared, i.e [ViewModel.onCleared] is called
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
 */
public val ViewModel. viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this. getTag(JOB_KEY)if (scope != null) {
            return scope
        }
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }

This extension attribute is an extension attribute of ViewModel, which is basically the same as LifecycleOwner.lifecycleScope. It also returns a scope where CoroutineContext is SupervisorJob() + Dispatchers.Main.immediate. It can be automatically canceled when this ViewModel is destroyed, and it will not Cause a coroutine leak.

6. Use coroutineScope() and supervisorScope() to create subscopes

These two functions are suspending functions, which need to run in coroutines or suspending functions.
Both coroutineScope and supervisorScope will return a scope. The difference between them is exception propagation: the exception inside coroutineScope will be propagated upward, and the uncaught exception of the child coroutine will be passed upward to the parent coroutine. Any child coroutine exception Exit will lead to the exit of the whole; the exception inside the supervisorScope will not be propagated upwards, and a child coroutine exits abnormally, which will not affect the operation of the parent coroutine and brother coroutines.
Therefore, the design and application scenarios of supervisorScope are mostly used when sub-coroutines are independent and equivalent task entities, such as a downloader, and each sub-coroutine is a download task. When a download task is abnormal, it does not Should affect other download tasks.

To sum up, regarding scope, it is more recommended to use LifecycleOwner.lifecycleScope in UI components and V in ViewModelviewModel. viewModelScope


Create a coroutine

There are two common ways to create coroutines: CoroutineScope.launch() and CoroutineScope.async().
The difference between the two lies in the return value: launch creates a new coroutine, but does not return the result, and returns a Job, which is used for the supervision and cancellation of the coroutine, and is suitable for scenarios with no return value. async creates a new coroutine, returns a Deferred subclass of Job, and can get the return value when it is completed through await(). The return value function of async needs to be used in conjunction with the await() suspending function.
In addition, the big difference between the two is that they handle exceptions differently: if async is used as the opening method of the outermost coroutine, it expects to finally get the result (or exception) by calling await, So by default it doesn’t throw an exception. This means that if you use async to start a new outermost coroutine without await, it will silently throw the exception away.

CoroutineScope. launch()

Simple example:

//MainScope() obtains a coroutine scope for creating coroutines
private val mScope = MainScope()

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R. layout. activity_main)
        // Create a coroutine with default parameters, and its default scheduling mode is Main, which means that the thread environment of the coroutine is the Main thread
        mScope. launch {
            // Here is the coroutine body
            // delay 1000 milliseconds delay is a suspending function
            // The thread where the coroutine is located will not block during these 1000 milliseconds
            //The coroutine hands over the execution right of the thread, what should the thread do, and it will resume to this point and continue to execute downward after the time is up
            Log.d(TAG, "launch starts the first coroutine")
            delay(1000)
            Log.d(TAG, "launch starts the first coroutine ends")
        }
        // Create a coroutine that specifies the scheduling mode, and the running thread of the coroutine is the IO thread
        mScope.launch(Dispatchers.IO) {
            // Here is the IO thread mode
            Log.d(TAG, "launch starts the second coroutine")
            delay(1000)
            //Switch the thread environment where the coroutine is located to the specified scheduling mode Main
            //Different from launch, async and runBlocking, withContext does not create new coroutines, and is often used to switch the running thread of code execution.
            // It is also a suspending method until the end returns a result. Multiple withContexts are executed serially, so it is very suitable for the situation where a task depends on the result returned by the previous task
            withContext(Dispatchers. Main) {
                // Now here is the Main thread, you can perform UI operations here
                Log.d(TAG, "Switch to main thread")
            }
        }
    }

override fun onDestroy() {
        super. onDestroy()
        // Cancel the coroutine to prevent the coroutine from leaking If lifecycleScope or viewModelScope does not need to be canceled manually
        mScope. cancel()
    }

CoroutineScope. async()

Display the return value function of async, which needs to be used in conjunction with the await() suspending function:

//MainScope() obtains a coroutine scope for creating coroutines
private val mScope = MainScope()

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R. layout. activity_main)
        mScope. launch {
            // Start a thread in IO mode and return a Deferred, which can be used to obtain the return value
            // When the code is executed here, a new coroutine will be opened and then the body of the coroutine will be executed, and the code of the parent coroutine will go down
            val deferred = async(Dispatchers.IO) {
                // simulation time-consuming
                delay(2000)
                // return a value
                return@async "Get return value"
            }
            // Wait for the async execution to complete and obtain the return value. The thread will not be blocked here, but will be suspended, and the execution right of the thread will be handed over
            // After the execution of the async coroutine body is completed, the coroutine will resume and continue to execute
            val data = deferred. await()
            Log.d(TAG, "data:$data")
        }
    }

override fun onDestroy() {
        super. onDestroy()
        // Cancel the coroutine to prevent the coroutine from leaking If lifecycleScope or viewModelScope does not need to be canceled manually
        mScope. cancel()
    }

Demonstrate the concurrency capabilities of async:

//MainScope() gets a coroutine scopefor creating coroutines
private val mScope = MainScope()

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R. layout. activity_main)
        mScope. launch {
            // Here is a requirement to request 5 interfaces at the same time and splice the return values
            val job1 = async {
                // request 1
                delay(5000)
                return@async "1"
            }
            val job2 = async {
                // request 2
                delay(5000)
                return@async "2"
            }
            val job3 = async {
                // request 3
                delay(5000)
                return@async "3"
            }
            val job4 = async {
                // request 4
                delay(5000)
                return@async "4"
            }
            val job5 = async {
                // request 5
                delay(5000)
                return@async "5"
            }
            // code executionAt this point, 5 requests have been executed at the same time
            // Wait for each job to finish executing and combine the results
            Log.d(TAG, "async: ${job1.await()} ${job2.await()} ${job3.await()} ${job4.await()} ${job5.await()}"
            )
        }
    }

override fun onDestroy() {
        super. onDestroy()
        // Cancel the coroutine to prevent the coroutine from leaking If lifecycleScope or viewModelScope does not need to be canceled manually
        mScope. cancel()
    }

suspend suspend function

The Kotlin coroutine provides the suspend keyword, which is used to define a suspend function, which is a tag that tells the compiler that this function needs to be executed in the coroutine, and the compiler will convert the suspend function into a finite state machine It is an optimized version of callback.
When the suspend function suspends the coroutine, it will not block the thread.
A suspend function can only be used in a coroutine or a suspend function.


CoroutineContext

CoroutineContext is a basic structural unit of Kotlin coroutine. Smart use of coroutine context is crucial to achieve correct thread behavior, lifecycle, exceptions, and debugging. It consists of the following items:

  • Job: Control the life cycle of the coroutine;
  • CoroutineDispatcher: Distribute tasks to the appropriate thread;
  • CoroutineName: The name of the coroutine, which is useful when debugging ;
  • CoroutineExceptionHandler: Handle uncaught exceptions.
    CoroutineContext has two very important elements: Job and Dispatcher,Job is the current Coroutine instance and Dispatcher determines the thread that the current Coroutine executes. You can also add CoroutineName for debugging, and add CoroutineExceptionHandler for catching exceptions.
    Simple example:
val coroutineContext = Job() + Dispatchers.Default + CoroutineName("coroutine name")
Log.d(TAG, "coroutineContext:$coroutineContext")

The log output result is:

coroutineContext:[JobImpl{Active}@1f12c95, CoroutineName(coroutine name), Dispatchers.Default]

Job

Job is used to process coroutines. For each created coroutine (via launch or async), it will return a Job instance, which is the unique identifier of the coroutine and is responsible for managing the life cycle of the coroutine.
The CoroutineScope.launch function returns a Job object, representing an asynchronous task. Jobs have a lifecycle and can be canceled. Jobs can also have a hierarchical relationship. A job can contain multiple child jobs. When the parent job is canceled, all child jobs will be automatically canceled; when the child job is canceled or an exception occurs, the parent job will also be canceled.
In addition to creating the Job object through CoroutineScope.launch, you can also create the object through the Job() factory method. By default, the failure of a child job will cause the parent job to be canceled. This default behavior can be modified through SupervisorJob.
A parent job with multiple sub-jobs will wait for all sub-jobs to complete (or cancel), and then automaticallyIt will be executed by itself.

Job lifecycle

Job life cycle includes New (newly created), Active (active), Completing (completed), Completed (completed), Canceling (cancelling), Canceled (cancelled). While we can’t access these states directly, we can access the properties of the Job: isActive, isCancelled, and isCompleted.

Job common API

isActive: Boolean //whether alive
isCancelled: Boolean //Cancel
isCompleted: Boolean //Whether it is completed
children: Sequence // all child Jobs
cancel() // cancel the coroutine
join() // Block the current thread until the execution of the coroutine is completed
cancelAndJoin() // Combine the two, cancel and wait for the coroutine to complete
cancelChildren() // Cancel all child coroutines, you can pass in CancellationException as the reason for cancellation
attachChild(child: ChildJob) // attach a child coroutine to the current coroutine
invokeOnCompletion(handler: CompletionHandler): DisposableHandle //Set a completion notification for the Job, and this notification function will be executed synchronously when the Job execution is completed

Deferred

By using async to create a coroutine, you can get a Deferred with a return value. The Deferred interface inherits from the Job interface, and additionally provides a method for obtaining the result returned by Coroutine. Since Deferred inherits from the Job interface, the content related to Job is also applicable to Deferred. Deferred provides three additional functionsTo handle operations related to Coroutine execution results.

await() //Used to wait for this Coroutine to finish executing and return the result
getCompleted() //Used to get the result of Coroutine execution. If the Coroutine has not completed execution, an IllegalStateException will be thrown, and if the task is canceled, the corresponding exception will also be thrown. So before executing this function, you can use isCompleted to judge whether the current task is completed.
getCompletionExceptionOrNull() //Get the Coroutine exception information in the completed state. If the task is completed normally, there is no exception information and null is returned. If it is not in the completed state, calling this function will also throw an IllegalStateException, and you can use isCompleted to judge whether the current task is completed.

Supervisor Job

/**
 * Creates a _supervisor_ job object in an active state.
 * Children of a supervisor job can fail independently of each other.
 *
 * A failure or cancellation of a child does not cause the supervisor job to fail and does not affect its other children,
 * so a supervisor can implement a custom policy for handling failures of its children:
 *
 * * A failure of a child job that was created using [launch][CoroutineScope. launch] can be handled via [CoroutineExceptionHandler] in the context.
 * * A failure ofa child job that was created using [async][CoroutineScope.async] can be handled via [Deferred.await] on the resulting deferred value.
 *
 * If [parent] job is specified, then this supervisor job becomes a child job of its parent and is canceled when its
 * parent fails or is canceled. All this supervisor's children are canceled in this case, too.
 * [cancel][Job.cancel] with exception (other than [CancellationException]) on this supervisor job also cancels parent.
 *
 * @param parent an optional parent job.
 */
@Suppress("FunctionName")
public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)

Job has a parent-child relationship. If the child Job fails, the parent Job will fail automatically. This default behavior may not be what we expect.
And SupervisorJob is such a special job, the sub-jobs in it do not affect each other, and the failure of one sub-job does not affect the execution of other sub-jobs. SupervisorJob(parent:Job?) has a parent parameter. If this parameter is specified, the returned Job is the child Job of the parameter parent. If the Parent Job fails or is canceled, the Supervisor Job will also beCancel. When a Supervisor Job is canceled, all child jobs of the Supervisor Job will also be cancelled.
The implementation of MainScope() uses SupervisorJob and a Main Dispatcher:

/**
 * Creates the main [CoroutineScope] for UI components.
 *
 * Example of use:
 * ```
 * class MyAndroidActivity {
 * private val scope = MainScope()
 *
 * override fun onDestroy() {
 * super.onDestroy()
 * scope. cancel()
 * }
 * }
 * ```
 *
 * The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements.
 * If you want to append additional elements to the main scope, use [CoroutineScope.plus] operator:
 * `val scope = MainScope() + CoroutineName("MyActivity")`.
 */
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

Coroutine exception handling

The normal handling of exceptions is to use try-catch. However, exceptions within the scope of the coroutine cannot be captured by try-catch. At this time, CoroutineExceptionHandler needs to be used to catch the coroutineException in scope.
Simple example:

//MainScope() obtains a coroutine scope for creating coroutines
private val mScope = MainScope()

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R. layout. activity_main)
        mScope. launch(Dispatchers. Default) {
            delay(500)
            Log.e(TAG, "Child 1")
        }
        // Added exception handling in Child 2
        mScope.launch(Dispatchers.Default + CoroutineExceptionHandler { coroutineContext, throwable ->
            Log.e(TAG, "CoroutineExceptionHandler: $throwable")
        }) {
            delay(1000)
            Log.e(TAG, "Child 2")
            throw RuntimeException("--> RuntimeException <--")
        }
        mScope. launch(Dispatchers. Default) {
            delay(1500)
            Log.e(TAG, "Child 3")
        }
    }

override fun onDestroy() {
        super. onDestroy()
        // Cancel the coroutine to prevent the coroutine from leaking If lifecycleScope or viewModelScope does not need to be canceled manuallymScope. cancel()
    }

Log output result:

Child 1
Child 2
CoroutineExceptionHandler: java.lang.RuntimeException: --> RuntimeException <--
Child 3

The exception thrown in the second coroutine can be caught by using CoroutineExceptionHandler.
If an ordinary coroutine generates an unhandled exception, it will propagate the exception to its parent coroutine, and then the parent coroutine will cancel all child coroutines, cancel itself, and continue to pass the exception upwards.
MainScope() is implemented using SupervisorJob, so the execution result is that after Child 2 throws an exception, but does not affect other jobs, Child 3 will execute normally.
Using supervisorScope can also achieve the effect similar to SupervisorJob. A child coroutine exits abnormally, which will not affect the operation of the parent coroutine and sibling coroutines.
Simple example:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R. layout. activity_main)
        val scope = CoroutineScope(Job() + Dispatchers. Default)
        //If supervisorScope is replaced with coroutineScope, the result is not like this, Child 3 is gone
        scope.launch(CoroutineExceptionHandler { coroutineContext, throwable ->
            Log.e(TAG, "CoroutineExceptionHandler: $throwable")
        }) {
            supervisorScope {
                launch {
                    delay(500)
                    Log.e(TAG, "Child 1")
                }
                launch {
                    delay(1000)
                    Log.e(TAG, "Child 2")
                    throw RuntimeException("--> RuntimeException <--")
                }
                launch {
                    delay(1500)
                    Log.e(TAG, "Child 3")
                }
            }
        }
    }

Log output result:

Child 1
Child 2
CoroutineExceptionHandler: java.lang.RuntimeException: --> RuntimeException <--
Child 3

If supervisorScope is replaced with coroutineScope, the result is not like this, Child 3 is gone:
Log output result:

Child 1
Child 2
CoroutineExceptionHandler: java.lang.RuntimeException: --> RuntimeException <--

CoroutineDispatcher

CoroutineDispatcher, the scheduler, defines the threads that Coroutine executes. CoroutineDispatcher can limit Coroutine to execute in a certain thread, or it can be assigned to a thread pool for execution, or it can not limit its execution thread.
CoroutineDispatcher is an abstract class, and all dispatchers should inherit from this class to implement corresponding functions. Dispatchers is a helper class in the standard library that helps us encapsulate thread switching, which can be simply understood as a thread pool.
Kotlin coroutine presets 4 schedulers:

  • Dispatchers.Default: The default scheduler, suitable for processing background calculations, is a CPU-intensive task scheduler. If no dispatcher is specified when creating a Coroutine, this is generally used as the default value by default. Good for doing CPU-intensive work off the main thread, examples include sorting lists and parsing JSON.
    Default dispatcher uses a shared pool of background threads to run tasks inside. Note that it shares the thread pool with Dispatchers.IO, but the maximum number of concurrency is different.
  • Dispatchers.IO: used to perform blocking IO operations, suitable for performing IO-related operations, is an IO-intensive task scheduler. Suitable for performing disk or network I/O outside of the main thread, examples include using Room components, reading data from or writing data to files, and running any network operations.
    Share a thread pool with Dispatchers.Default to execute the tasks inside. Depending on the number of tasks running at the same time, additional threads will be created when needed, and unneeded threads will be released when the tasks are completed.
  • Dispatchers.Main: Depending on the platform, it will be initialized to the scheduler corresponding to the UI thread, such as the main thread of Android. This scheduler should only be used to interact with the interface and perform quick work. Examples include calling suspend functions, running Android UI framework operations, and updating LiveData objects.
  • Dispatchers.Unconfined: No thread is specified, so the thread is started by default during execution. If the sub-coroutine switches threads, the following code will also continue to execute in this thread.
    Since the sub-coroutine will inherit the context of the parent coroutine, for the convenience of use, a Dispatcher is generally set on the parent coroutine, and then all sub-coroutines automatically use this Dispatcher.

withContext

Different from launch, async and runBlocking, withContext does not create a new coroutine, and is often used to switch the thread on which the coroutine is executed. It is also a suspending method until the end returns a result. Multiple withContexts are executed serially, so it is very suitable for the situation where a task depends on the result returned by the previous task.


ContinuationInterceptor

ContinuationInterceptor, an interceptor, is used to intercept coroutines to do some additional operations, such as the CoroutineDispatcher scheduler, which is implemented with interceptors.
Simple example: interceptor for printing logs

//MainScope() obtains a coroutine scope for creating coroutines
private val mScope = MainScope()

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R. layout. activity_main)
        mScope.launch(Dispatchers.IO) {
            launch(MyInterceptor()) {
                Log.d(TAG, "1")
                val deferred = async {
                    Log.d(TAG, "2")
                    delay(100)
                    Log.d(TAG, "3")
                    return@async "return value"
                }
                Log.d(TAG, "4")
                val result = deferred. await()
                Log.d(TAG, "5:$result")
            }.join()
            Log.d(TAG, "6")
        }
    }

override fun onDestroy() {
        super. onDestroy()
        // Cancel the coroutine to prevent the coroutine from leaking If lifecycleScope or viewModelScope does not need to be canceled manually
        mScope. cancel()
    }

class MyInterceptor : ContinuationInterceptor {
        override val key: CoroutineContext.Key
            get() = ContinuationInterceptor

        override fun  interceptContinuation(continuation: Continuation): Continuation {
            return MyContinuation(continuation)
        }
    }

class MyContinuation(val continuation: Continuation) : Continuation {
        override val context: CoroutineContextget() = continuation. context

        override fun resumeWith(result: Result) {
            Log.d(TAG, "result:$result")
            continuation. resumeWith(result)
        }
    }

Log output result:

result:Success(kotlin.Unit)
1
result:Success(kotlin.Unit)
2
4
result:Success(kotlin.Unit)
3
result:Success (return value)
5: return value
6

Four interceptions occurred, in order: when the coroutine started (the first two), when it was suspended, and when the result was returned.
Simple example: thread scheduler

//MainScope() obtains a coroutine scope for creating coroutines
private val mScope = MainScope()

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R. layout. activity_main)
        mScope. launch {
            Log.d(TAG, Thread.currentThread().name)
            withContext(MyInterceptor2()) {
                Log.d(TAG, Thread.currentThread().name)
                delay(100)
                Log.d(TAG, Thread.currentThread().name)
            }
            Log.d(TAG, Thread.currentThread().name)
        }
    }

override fun onDestroy() {
        super. onDestroy()
        // Cancel the coroutine to prevent the coroutine from leaking If lifecycleScope or viewModelScope does not need to be canceled manually
        mScope. cancel()
    }

class MyInterceptor : ContinuationInterceptor {
        override val key: CoroutineContext.Key
            get() = ContinuationInterceptor

        override fun  interceptContinuation(continuation: Continuation): Continuation {
            return MyContinuation(continuation)
        }
    }

class MyContinuation(val continuation: Continuation) : Continuation {
        override val context: CoroutineContext
            get() = continuation. context

        override fun resumeWith(result: Result) {
            Log.d(TAG, "result:$result")
            continuation. resumeWith(result)
        }
    }

Log output result:

main
result:Success(kotlin.Unit)
my thread pool
result:Success(kotlin.Unit)my thread pool
main

Reference article:
10,000-word long article – advanced Kotlin coroutine
Kotlin coroutine application practice (hard core sharing, you can see it at a glance)

Leave a Reply

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