Coroutines, or coroutines, are essentially lightweight threads whose scheduling switches are collaborative and can be actively suspended and resumed

Retrofit2 support for coroutines

Let’s start with retroFIT2, our most common retrofit2. What’s the difference between code that uses coroutines and code that doesn’t

Note that Retrofit2 only started supporting coroutines in 2.6.0, so be sure to upgrade Retrofit2 to 2.6.0 and above

First, define two apis, one that combines the use of RxJavA2 and one that combines the use of coroutines

interface TestApi {
    @GET("api/4/news/latest")
    fun getLatestNews(): Flowable<LatestNews>
    
    @GET("api/4/news/latest")
    suspend fun getLatestNews2(): LatestNews
}

Copy the code

As you can see, retrofit2 supports suspend defining the getLatestNews2api as a suspend function, which you can use in your coroutine

How do you use two different apis

class CoroutineActivity : AppCompatActivity() {... // This is the most common way we use RetroFIT2 to request data + switch threads funrequestData1() {
        testApi.getLatestNews()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(object : DisposableSubscriber<LatestNews>() {
                override fun onComplete() {}

                override fun onNext(t: LatestNews) {
                    tv_text.text = Gson().toJson(t)
                }

                override fun onError(t: Throwable?) {
                    tv_text.text = "error"} // Use coroutine request + render data funrequestData2() {
        GlobalScope.launch(Dispatchers.Main) {
            try {
                tv_text.text = Gson().toJson(testApi.getLatestNews2())
            } catch (e: Exception) {
                tv_text.text = "error"}}}}Copy the code

It is well known that RXJava2 renders data using callbacks

Coroutines need to start a coroutine using GlobalScope.launch (there are many ways to start coroutines, please check out the official documentation). Use dispatchers. Main to specify the coroutine scheduler as the Main thread (i.e., the UI thread) and then handle the normal and abnormal cases with a try catch. (Start the coroutine using the GlobalScope context for the moment.

It seems that using coroutines can simplify a lot of code and make it look more elegant

Let’s look at the concurrent and serial cases of multiple requests

Add a few more apis for easy operation

interface TestApi {
	@GET("api/3/news/latest")
    fun getLatestNews(): Flowable<LatestNews>

    @GET("api/3/news/{id}")
    fun getNewsDetail(@Path("id") id: Long): Flowable<News>


    @GET("api/4/news/latest")
    suspend fun getLatestNews2(): LatestNews

    @GET("api/3/news/{id}")
    suspend fun getNewsDetail2(@Path("id") id: Long): News
}

Copy the code

For example, we call getLatestNews() to request a list of stories, and then call getNewsDetail to request details of the first story, as shown below

// Non-coroutine usagetestApi.getLatestNews()
    .flatMap {
        testApi.getNewsDetail(it.stories!! [0].id!!) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : DisposableSubscriber<News>() { override funonComplete() {}

        override fun onNext(t: News) {
            tv_text.text = t.title
        }

        override fun onError(t: Throwable?) {
            tv_text.text = "error"}}) // Coroutine usage globalscope.launch (dispatchers.main) {try {val lastedNews =testApi.getLatestNews2()
        val detail = testApi.getNewsDetail2(lastedNews.stories!! [0].id!!) tv_text.text = detail.title } catch(e: Exception) { tv_text.text ="error"}}Copy the code

Another example is if we want to call getNewsDetail to request multiple news details at the same time

// Non-coroutine usagetestApi.getLatestNews()
    .flatMap {
        Flowable.zip(
            testApi.getNewsDetail(it.stories!! [0].id!!) .testApi.getNewsDetail(it.stories!! [1].id!!) , BiFunction<News, News, List<News>> { news1, news2-> listOf(news1, news2) } ) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : DisposableSubscriber<List<News>>() { override funonComplete() {}

        override fun onNext(t: List<News>) {
            tv_text.text = t[0].title + t[1].title
        }

        override fun onError(t: Throwable?) {
            tv_text.text = "error"Globalscope.launch (dispatchers.main) {try {val lastedNews =testApi.getlatestnews2 () // Use async to concurrently request details of the first and second news items val detail1 = async {testApi.getNewsDetail2(lastedNews.stories!! [0].id!!) } val detail2 = async {testApi.getNewsDetail2(lastedNews.stories!! [1].id!!) } tv_text.text = detail1.await().title + detail2.await().title } catch(e: Exception) { tv_text.text ="error"}}Copy the code

Coroutines can make your code more concise and elegant than non-coroutines (using RXJava2 in your code). They can clearly describe what you want to do first, second, and so on

Room database support for coroutines

The Room database began supporting coroutines in 2.1.0, and room-KTX dependencies needed to be imported

implementation "Androidx. Room: a room - KTX: 2.1.0." "
Copy the code

Then use suspend in the Dao to define the suspend function

@Dao
abstract class UserDao {
    @Query("select * from tab_user")
    abstract suspend fun getAll(): List<User>
}

Copy the code

Finally, use coroutines just like retroFIT2 did above

class RoomActivity : AppCompatActivity() {
    private var adapter: RoomAdapter? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_room) ... }... private funloadUser() { GlobalScope.launch(Dispatchers.Main) { adapter!! .data = AppDataBase.getInstance().userDao().getAll() } } }Copy the code

The Android Jetpack Room database can be used in conjunction with other libraries. The Android Jetpack Room database can be used in conjunction with other libraries

Coroutines in Android applications

The examples above all use the GlobalScope context to launch the coroutine. In android, it is generally not recommended to use GlobalScope directly because with GlobalScope. Launch, we create a top-level coroutine. Although it is lightweight, it still consumes some memory resources when it runs, and it will continue to run if we forget to keep references to newly launched coroutines, so we must keep all references to globalScope.Launch launch coroutines, Then cancel out all coroutines when the activity destory (or something else that needs to cancel) is called, otherwise you can cause a memory leak and other problems

Such as:

class CoroutineActivity : AppCompatActivity() {
    private lateinit var testApi: TestApi
    private var job1: Job? = null
    private var job2: Job? = null
    private var job3: Job? = null
    
    ...

    override fun onDestroy() { super.onDestroy() job1? .cancel() job2? .cancel() job3? .cancel() } ... // Start the first top-level coroutine funrequestData1() {
        job1 = GlobalScope.launch(Dispatchers.Main) {
            try {
                val lastedNews = testApi.getLatestNews2() tv_text.text = lastedNews.stories!! [0].title } catch(e: Exception) { tv_text.text ="error"// Start the second top-level coroutine funrequestData2() {
        job2 = GlobalScope.launch(Dispatchers.Main) {
            try {
                val lastedNews = testApi.getLatestNews2() // Start the third top-level coroutine inside the coroutine job3 = globalscope.launch (dispatchers.main) {try {val detail =testApi.getNewsDetail2(lastedNews.stories!! [0].id!!) tv_text.text = detail.title } catch (e: Exception) { tv_text.text ="error"
                    }
                }
            } catch(e: Exception) {
                tv_text.text = "error"}}}}Copy the code

As you can see, if more coroutines are started using GlobalScope, the more variables must be defined to hold references to the start coroutine and cancel all of them when onDestroy is done

Let’s take a look at MainScope instead of GlobalScope


class CoroutineActivity : AppCompatActivity() {
    private var mainScope = MainScope()
    private lateinit var testApi: TestApi

    ...

    override fun onDestroy() {super.ondestroy () // Simply call mainscope.cancel to cancel all coroutines mainscope.cancel ()} funrequestData1() {
        mainScope.launch {
            try {
                val lastedNews = testApi.getLatestNews2() tv_text.text = lastedNews.stories!! [0].title } catch(e: Exception) { tv_text.text ="error"
            }
        }
    }

    fun requestData2() {
        mainScope.launch {
            try {
                val lastedNews = testApi.getLatestNews2()
                val detail = testApi.getNewsDetail2(lastedNews.stories!! [0].id!!) tv_text.text = detail.title } catch (e: Exception) { tv_text.text ="error"}}}}Copy the code

Or use the Kotlin delegate pattern to implement the following:

class CoroutineActivity : AppCompatActivity(), CoroutineScope by MainScope() {
    private lateinit var testApi: TestApi

	...
	
    override fun onDestroy() {
        super.onDestroy()
        cancel()
    }

    fun requestData1() {
        launch {
            try {
                val lastedNews = testApi.getLatestNews2() tv_text.text = lastedNews.stories!! [0].title } catch(e: Exception) { tv_text.text ="error"
            }
        }
    }

    fun requestData2() {
        launch {
            try {
                val lastedNews = testApi.getLatestNews2()
                val detail = testApi.getNewsDetail2(lastedNews.stories!! [0].id!!) tv_text.text = detail.title } catch (e: Exception) { tv_text.text ="error"}}}}Copy the code

Meanwhile, let’s first look at the definition of MainScope

@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

Copy the code

Using MainScope is very simple. Just call MainScope’s Cancel method in the Activity onDestroy. There is no need to define references to other coroutines, and MainScope’s scheduler is dispatchers. Main, so there is no need to manually specify the Main scheduler

Lifecycle support for coroutines

Lifecycle component library has support for coroutines in the alpha release of 2.2.0

Lifecycle – Run-time – KTX dependency needs to be added (please use the official version when it is available)

implementation "Androidx. Lifecycle: lifecycle - runtime - KTX: 2.2.0 - alpha05"
Copy the code

Lifecycle runtime-ktx adds lifecycleScope extension property (class MainScope) to LifecycleOwner for easy operation of coroutines.

Let’s look at the source code

val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope
    
    
val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        while (true) {
            val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
            if(existing ! = null) {returnexisting } val newScope = LifecycleCoroutineScopeImpl( this, / / SupervisorJob specified coroutines scope is a one-way transfer / / Dispatchers. Main. The immediate assign coroutines In the Main thread that executes / / Dispatchers. Main. Immediate with Dispatchers. The Main difference is that only if the current in the Main thread, it immediately execute coroutines, rather than walk the Dispatcher distribution process SupervisorJob () + Dispatchers. Main. Immediate)if (mInternalScopeRef.compareAndSet(null, newScope)) {
                newScope.register()
                return newScope
            }
        }
    }    
Copy the code

LifecycleCoroutineScope also provides a way to bind startup coroutines to the LifecycleOwner lifecycle (typically activities and fragments). As follows:

abstract class LifecycleCoroutineScope internal constructor() : CoroutineScope { internal abstract val lifecycle: Lifecycle // Execute the coroutine body fun launchWhenCreated(block:suspendCoroutineScope.() -> Unit): Job = launch {lifecycle. WhenCreated (block)}suspendCoroutineScope.() -> Unit): Job = launch {lifecycle. WhenStarted (block)}suspend CoroutineScope.() -> Unit): Job = launch {
        lifecycle.whenResumed(block)
    }
}
Copy the code

Because the method above that starts the coroutine is bound to the activity life cycle, it also automatically cancels the coroutine when the Activity destroys

So the CoroutineActivity Demo code can be written more simply as follows:

class CoroutineActivity : AppCompatActivity() {
    private lateinit var testApi: TestApi

	...

    fun requestData1() {
        lifecycleScope.launchWhenResumed {
            try {
                val lastedNews = testApi.getLatestNews2() tv_text.text = lastedNews.stories!! [0].title } catch(e: Exception) { tv_text.text ="error"}}}}Copy the code

LiveData support for coroutines

LiveData is also supported with coroutines, but with the addition of the lifecycle- Livedata-ktx dependency

// Now it is still 'alpha'. When the official version is released, please replace it with official version"Androidx. Lifecycle: lifecycle - livedata - KTX: 2.2.0 - alpha05"

Copy the code

Lifecycle – Livedata – KTX dependency adds a top-level liveData function that returns CoroutineLiveData

The source code is as follows:

. internal const val DEFAULT_TIMEOUT = 5000L ... fun <T> liveData( context: CoroutineContext = EmptyCoroutineContext, timeoutInMs: Long = DEFAULT_TIMEOUT, @BuilderInference block:suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)

Copy the code

When does coroutine EliveData start coroutine and execute coroutine body?

internal class CoroutineLiveData<T>( context: CoroutineContext = EmptyCoroutineContext, timeoutInMs: Long = DEFAULT_TIMEOUT, block: Block<T> ) : MediatorLiveData<T>() { private var blockRunner: BlockRunner<T>? private var emittedSource: EmittedSource? = null init { val scope = CoroutineScope(Dispatchers.Main.immediate + context + supervisorJob) blockRunner = BlockRunner( liveData = this, block = block, timeoutInMs = timeoutInMs, scope = scope ) { blockRunner = null } } ... Override Fun is executed when observe or observeForever is called for the first timeonActive() {super.onactive () // Start the coroutine and execute the coroutine body blockRunner? .mayberun ()} override Fun is invoked when removeObserver is calledonInactive() {super.oninactive () // blockRunner? .cancel() } }Copy the code

CoroutineLiveData starts the coroutine on onActive() and removes it on onInactive()

So using LiveData support for coroutines, the CoroutineActivity Demo code looks like this


class CoroutineActivity : AppCompatActivity() {
    private lateinit var testApi: TestApi
    
	...

    fun requestData1() {
        liveData {
            try {
                val lastedNews = testApi.getLatestNews2() emit(lastedNews.stories!! [0].title!!) } catch(e: Exception) { emit("error")
            }
        }.observe(this, Observer {
            tv_text.text = it
        })
    }
}

Copy the code

We’ve covered some of the most common uses of coroutines in Android. Here are some of the basics

Coroutine context

The CoroutineContext is represented by CoroutineContext, In kotlin, Job, CoroutineDispatcher, and ContinuationInterceptor are subclasses of CoroutineContext, that is, they are coroutine contexts

Take a look at CoroutineContext’s more important plus method, which is an overloaded (+) operator method fixed with operator

@SinceKotlin("1.3")
public interface CoroutineContext {

    /**
     * Returns a context containing elements from this context and elements from  other [context].
     * The elements from this context with the same key as in the other one are dropped.
     */
    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
            context.fold(this) { acc, element ->
                val removed = acc.minusKey(element.key)
                if (removed === EmptyCoroutineContext) element else {
                    // make sure interceptor is always last in the context (and thus is fast to get when present)
                    val interceptor = removed[ContinuationInterceptor]
                    if (interceptor == null) CombinedContext(removed, element) else {
                        val left = removed.minusKey(ContinuationInterceptor)
                        if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                            CombinedContext(CombinedContext(left, element), interceptor)
                    }
                }
            }
}

Copy the code

For example, the above MainScope definition uses the + operator

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
Copy the code

If you look at the source code for launching coroutines, you’ll see that kotlin uses the + operator a lot, so most CoroutineContext objects in Kotlin are CombinedContext objects

The example above uses the launch method to launch a coroutine with three parameters: the coroutine context, the coroutine launch mode, and the coroutine body

Public fun coroutinescope.launch (context: CoroutineContext = EmptyCoroutineContext, // CoroutineContext start: CoroutineStart = coroutinestart. DEFAULT, // coroutine startup mode block:suspend() -> Unit // coroutine body): Job {val newContext = newCoroutineContext(context) val coroutine =if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}
Copy the code

Coroutine start mode

  • DEFAULT

    Execute the coroutine body immediately

    runBlocking {
        val job = GlobalScope.launch(start = CoroutineStart.DEFAULT) {
            println("1."+ thread.currentThread ()} // job.join()}Copy the code

    Print the result

    1: DefaultDispatcher-worker-1
    Copy the code

    The CoroutineStart.DEFAULT startup mode does not require manual calls to join or start methods. Instead, the code of the coroutine body is automatically executed when the launch method is called

  • LAZY

    The coroutine body is executed only if needed

    runBlocking {
        val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
            println("1."+ thread.currentThread () {job.join()}Copy the code

    Print the result

    1: DefaultDispatcher-worker-1
    Copy the code

    CoroutineStart.LAZY startup mode must manually call methods such as join or start, otherwise the coroutine body will not execute

  • ATOMIC

    The coroutine body is executed immediately, but cannot be cancelled before it starts running; that is, starting the coroutine ignores the cancelling state

    runBlocking {
        val job = GlobalScope.launch(start = CoroutineStart.ATOMIC) {
            println("1." + Thread.currentThread().name)
            delay(1000)
            println("2." + Thread.currentThread().name)
        }
        job.cancel()
        delay(2000)
    }
    Copy the code

    Print the result

    1: DefaultDispatcher-worker-1
    Copy the code

    CoroutineStart. ATOMIC start mode coroutines must be executed even if cancel is called, because opening coroutines ignores the cancelling state. JobCancellationException occurs when delay(1000) is executed and the following code is not executed. If delay(1000) catches an exception with a try catch, the following code continues

  • UNDISPATCHED

    The coroutine body is executed immediately in the current thread until the thread following the suspend call depends on the scheduler in context

    runBlocking {
        println("0."+ thread.currentThread ().name) + thread.currentThread ().name) Launch (context = dispatchers.default, start = CoroutineStart.UNDISPATCHED) { println("1." + Thread.currentThread().name)
            delay(1000)
            println("2." + Thread.currentThread().name)
        }
        delay(2000)
    }
    Copy the code

    Print the result

    0: main
    1: main
    2: DefaultDispatcher-worker-1
    Copy the code

    It can be seen that 0 and 1 are executed on the same thread. After delay(1000) is executed, the following code execution thread depends on the thread specified by the Dispatchers.default scheduler, so 2 is executed in another thread

Coroutine scheduler

The coroutine scheduler is also a coroutine context

The coroutine scheduler is used to specify which thread to execute coroutine code blocks. Kotlin provides several Default coroutine schedulers, Default, Main, and Unconfined, and a specific IO scheduler for the JVM

  • Dispatchers.Default

    Specifies that the code block is executed in a thread pool

    GlobalScope.launch(Dispatchers.Default) {
        println("1."+ thread.currentThread ().name) launch (dispatchers.default) {delay(1000)"2." + Thread.currentThread().name)
        }
        println("3." + Thread.currentThread().name)
    }
    Copy the code

    The print result is as follows

    1: DefaultDispatcher-worker-1
    3: DefaultDispatcher-worker-1
    2: DefaultDispatcher-worker-1
    Copy the code
  • Dispatchers.Main

    Specifies that the code block executes in the main thread (UI thread for Android)

    GlobalScope.launch(Dispatchers.Default) {
        println("1."+ thread.currentThread ().name) launch (dispatchers.main) {delay(1000)"2." + Thread.currentThread().name)
        }
        println("3." + Thread.currentThread().name)
    }
    Copy the code

    The print result is as follows:

    1: DefaultDispatcher-worker-1
    3: DefaultDispatcher-worker-1
    2: main
    Copy the code

    Dispatchers.Main specifies that coroutine blocks are executed in the Main thread

  • Dispatchers.Unconfined

    No specific thread is specified in which the coroutine code will be executed, i.e. the thread in which it is currently executed, and the next code in the code block will be executed.

    GlobalScope.launch(Dispatchers.Default) {
        println("1." + Thread.currentThread().name)
        launch (Dispatchers.Unconfined) {
            println("2."+ thread.currentThread ().name) requestApi() // delay(1000)"3." + Thread.currentThread().name)
        }
        println("4."+ thread.currentThread ().name)} // Define a suspend function to execute private in a new child Threadsuspend fun requestApi() = suspendCancellableCoroutine<String> {
        Thread {
            println("5: requestApi: " + Thread.currentThread().name)
            it.resume("success")
        }.start()
    }
    Copy the code

    The print result is as follows:

    1: DefaultDispatcher-worker-1
    2: DefaultDispatcher-worker-1
    5: requestApi: Thread-3
    4: DefaultDispatcher-worker-1
    3: Thread-3
    Copy the code

    You can see that the threads of code execution for 2 and 3 are significantly different; When executing in the requestApi, the code is switched to a child Thread (thread-3), and the next block of coroutine code is executed in Thread-3

  • Dispatchers.IO

    It is based on the thread pool behind the Default scheduler and implements independent queues and limits, so the coroutine scheduler switching from Default to IO does not trigger a thread switch

    GlobalScope.launch(Dispatchers.Default) {
        println("1." + Thread.currentThread().name)
        launch (Dispatchers.IO) {
            println("2." + Thread.currentThread().name)
            requestApi()  // delay(1000)
            println("3." + Thread.currentThread().name)
        }
        println("4." + Thread.currentThread().name)
    }
    Copy the code

    The print result is as follows:

    1: DefaultDispatcher-worker-1
    4: DefaultDispatcher-worker-1
    2: DefaultDispatcher-worker-1
    5: requestApi: Thread-3
    3: DefaultDispatcher-worker-1
    Copy the code
  • A scheduler bound to any custom thread (use this approach with caution)

    You can create a Dispatcher using kotlin’s built-in newSingleThreadContext method or an ExecutorService extension called asCoroutineDispatcher

    Val dispatcher = newSingleThreadContext("custom thread") / / the second method / / val dispatcher. = Executors newSingleThreadExecutor {r - > Thread (r,"custom thread") }.asCoroutineDispatcher()
    GlobalScope.launch(dispatcher) {
        println("1." + Thread.currentThread().name)
        delay(1000)
        println("2."+ thread.currentThread ()} runBlocking {delay(2000L)}Copy the code

    The print result is as follows:

    1: custom thread
    2: custom thread
    Copy the code

    As you can see, you can create your own thread and bind it to the coroutine scheduler, but this is not recommended because once you create a thread manually you need to close it manually, otherwise the thread will never terminate, which is dangerous

CoroutineScope GlobalScope, coroutineScope, container scope

Coroutine scope is a very heavy thing

  • GlobeScope

    GlobeScope launches coroutines that start a separate scope and cannot inherit the scope of an outside coroutine, while its internal child coroutines follow the default scope rules

  • coroutineScope

    A coroutine started by coroutineScope inherits the scope of the parent coroutine, its internal cancel operations propagate in both directions, and exceptions not caught by the child coroutine are passed up to the parent coroutine

  • supervisorScope

    The container it is running on inherits the scope of its parent coroutine, which is different from the coroutineScope in that it is uni-directionalized, meaning that internal cancel operations and exception runs can only be propagated from the parent coroutine to its child coroutine, not its parent coroutine

    The MainScope is the container that is used for the scope, so it’s only necessary that a child coroutine error or cancel won’t affect the parent coroutine, and therefore won’t affect the sibling coroutine

Coroutine exception pass mode

The exception handling of the coroutine is related to the scope of the coroutine, which is either two-way like the coroutineScope or unidirectional like the parent coroutine and the child coroutine it is designed to handle

It is designed for one-way circulation in the container

runBlocking {
    println("1")
    supervisorScope {
        println("2"Launch {1/0} delay(100) println("3")
    }
    println("4")}Copy the code

The print result is as follows:

1
2
Exception in thread "main @coroutine#2" java.lang.ArithmeticException: / by zero
3
4

Copy the code

It’s safe to see that the child coroutine that is launched in the container is running properly, and it’s not going to cause the parent coroutine to run properly

We’re also going to verify that the parent coroutine exception is passed on to the child coroutine in the container it’s handling

runBlocking {
    println("1")
    supervisorScope {
        println("2"Launch {try {delay(1000) println("3")
            } catch (e: Exception) {
                println("error"}} delay(100) 1/0 println("3")}}Copy the code
1
2
error

java.lang.ArithmeticException: / by zero
Copy the code

It’s clear that the parent coroutine is actually passing exceptions to its children in the container it’s handling

Bidirectional delivery for coroutineScope

runBlocking {
    println("1")
    try {
        coroutineScope {
            println("2"Launch {1/0} delay(100) println("3")
        }
    } catch (e: Exception) {
        println("error")}}Copy the code

The print result is as follows:

1
2
error

Copy the code

You can see that a child coroutine started in the coroutineScope scope is passed to the parent coroutine if an exception occurs

Let’s verify again that the parent coroutine exception is passed to the child coroutine in the coroutineScope scope

runBlocking {
    println("1")
    coroutineScope {
        println("2") // Launch a launch {try {delay(1000) println("3")
            } catch (e: Exception) {
                println("error")
            }
        }
        delay(100)
        1/0
        println("3")}}Copy the code

The print result is as follows:

1
2
error

java.lang.ArithmeticException: / by zero
Copy the code

You can see that the parent coroutine does pass exceptions to the child coroutine in the coroutineScope scope

Coroutines cancel

Let’s take a look at some code

GlobalScope.launch {
    println("1"Val job = launch {println("2"Delay (1000)} catch (e: Exception) {println(e: Exception) {delay(1000)}"error")
        }
        println("3")
        if(isActive) {// If the coroutine cancels, isActive isfalse
            println("4"} delay(1000) // If no exception is caught, execute println("5")
    }
    delay(100)
    job.cancel()
}
Copy the code

The print result is as follows:

1
2
error
3
Copy the code

When the coroutine is started first and then cancel, the following situations occur:

  • If code executed into the coroutine body depends on the coroutine’s Cancel state (such as the Delay method), it throws an exception, continues execution if it catches an exception, and terminates execution of the coroutine body if it does not
  • If the code inside the coroutine does not depend on the coroutine’s Cancel state (the println method), execution continues

That is, the cancellation of a coroutine causes the coroutine to stop running by throwing an exception. If the coroutine’s code does not depend on the coroutine’s Cancel state (i.e. there is no error), then the cancellation of the coroutine generally has no effect on the execution of the coroutine

Such as:

GlobalScope.launch {
    val job = launch {
        println("==start==")
        var i = 0
        while (i <= 10) {
            Thread.sleep(100)
            println(i++)
        }
        println("==end==")
    }
    delay(100)
    job.cancel()
}
Copy the code

The print result is as follows:

==start==
0
1
2
3
4
5
6
7
8
9
10
==end==
Copy the code

So even though the coroutine is canceled, the coroutine body continues to run

What do I do if I want to end the coroutine body?

At this point, you can use the isActive field of the CoroutineScope to determine whether the status of the coroutine has been canceled

GlobalScope.launch {
    val job = launch {
        println("==start==")
        var i = 0
        while (i <= 10 && isActive) {
            Thread.sleep(100)
            println(i++)
        }
        println("==end==")
    }
    delay(200)
    job.cancel()
}
Copy the code

Print the result

==start==
0
1
==end==
Copy the code

If the coroutine is cancelled, you can use the isActive field to determine whether a piece of code in the coroutine body needs to be executed

withContext

When executing the coroutine body, it is convenient to switch the thread of code execution using withContext. Such as

Globalscope.launch (dispatchers.default) {// Execute println("1."+ thread.currentThread () withContext(dispatchers.main) {"2."+ thread.currentThread ()} // Execute println() in dispatchers.default Thread pool"3." + Thread.currentThread().name)
    val dispatcher = newSingleThreadContext("custom thread"WithContext (dispatcher) {// Execute println(dispatcher)"4."+ thread.currentThread ()} dispatcher.close()"5." + Thread.currentThread().name)
}
Copy the code

Print the result

1: DefaultDispatcher-worker-1
2: main
3: DefaultDispatcher-worker-2
4: custom thread
5: DefaultDispatcher-worker-2
Copy the code

So you can easily switch the thread in which the code is running using withContext

The withContext can also work with the NonCancellable context to ensure that code blocks cannot be cancelled

GlobalScope.launch(Dispatchers.Default) {
    val job = launch {
        println("1."+ Thread.currentThread().name) try { delay(1000) } catch (e: Exception) {withContext(NonCancellable) {// Work with the NonCancellable context to ensure that the coroutine body cannot be cancelled println("error: "+ e.message) delay(100) // If withContext(NonCancellable) is not wrapped, delay(100) will cause the following code not to execute println("2." + Thread.currentThread().name)
            }
        }
    }
    delay(100)
    job.cancel()
}
Copy the code

Print the result

1: DefaultDispatcher-worker-1
error: Job was cancelled
2: DefaultDispatcher-worker-1
Copy the code

Structured concurrency

What is structured concurrency?

In fact, it is very simple to ensure that the coroutines started are in the same scope.

A top-level coroutine is created when we launch a coroutine using GlobalScope.launch. If we launch a coroutine each time using GlobalScope.launch, many top-level coroutines are created without interfering with each other, that is, even if one coroutine fails or is cancelled, The other coroutine will continue to run because they are not in the same coroutine scope

Globalscope.launch (dispatchers.default) {val a1 = globalscope.async {launch async with launch delay(1000) println("1."+ thread.currentThread ()} val a2 = globalscope.async {delay(100) 1/0"2."+ thread.currentThread ().name)} a1.await() a2.await() // a2.cancel() can also use cancel}Copy the code

The print result is as follows

1: DefaultDispatcher-worker-1
Exception in thread "DefaultDispatcher-worker-1" java.lang.ArithmeticException: / by zero
Copy the code

If a2 reports an error or cancel, A1 will not be affected

What exactly is the problem?

For example, it is common for an activity to have multiple concurrent network requests requesting data (that is, multiple coroutines are started), and when one of the network requests fails (that is, the coroutine fails), we want to close the other parallel network requests and not process them (that is, we want to close the other coroutines), but this is not the case

If we always start coroutines with GlobalScope, we would have to keep referencing each coroutine and cancel all of them when the Activity destroys. Otherwise, the asynchronous request code in the coroutine will continue to execute even if the activity is destroyed, making it prone to errors or memory leaks

How can we solve this problem conveniently?

In fact, we can solve this problem by using structured concurrency (coroutine scope), which ensures that multiple coroutines are started in the same scope. If the scope context is cancelled, all subcoroutines started in this scope are cancelled. It can also coordinate with the coroutineScope, container coroutineScope scope to handle abnormal passing problems

So the above code can be changed like this

GlobalScope.launch(Dispatchers.Default) {
    val a1 = async {
        delay(1000)
        println("1."+ thread.currentThread ().name)} val a2 = async {delay(100) 1/0"2." + Thread.currentThread().name)
    }
    a1.await()
    a2.await()
}
Copy the code

That is, remove the GlobalScope that starts a1 and A2 coroutines to ensure that a1 and A2 are in the same coroutine scope

Analysis of the principle of coroutine suspension function

Let’s take a look at the retroFIT compatible coroutine implementation source code

suspendFun <T: Any> Call<T>.await(): T {// usesuspendCancellableCoroutine defines a suspend function that takes a Continuation objectreturn suspendCancellableCoroutine { continuation ->
    continuation.invokeOnCancellation {
      cancel()
    }
    enqueue(object : Callback<T> {
      override fun onResponse(call: Call<T>, response: Response<T>) {
        if (response.isSuccessful) {
          val body = response.body()
          if (body == null) {
            val invocation = call.request().tag(Invocation::class.java)!!
            val method = invocation.method()
            val e = KotlinNullPointerException("Response from " +
                method.declaringClass.name +
                '. ' +
                method.name +
                " was null but response body type was declared as non-null") / / if the result is unusual, call the Continuation of resumeWithException callback Continuation. ResumeWithException (e)}else{// If the result is normal, the resume callback for the Continuation is called continuation.resume(body)}}else{// If the result is abnormal, Call the Continuation of resumeWithException callback Continuation. ResumeWithException (HttpException (response))}} override fun onFailure(call: Call<T>, t: Throwable) {/ / if the abnormal results, it is called a Continuation of resumeWithException callback Continuation. ResumeWithException (t)}}}})Copy the code

The source code and extension functions for continuations are shown below


@SinceKotlin("1.3")
@InlineOnly
public inline fun <T> Continuation<T>.resume(value: T): Unit =
    resumeWith(Result.success(value))

/**
 * Resumes the execution of the corresponding coroutine so that the [exception] is re-thrown right after the
 * last suspension point.
 */
@SinceKotlin("1.3")
@InlineOnly
public inline fun <T> Continuation<T>.resumeWithException(exception: Throwable): Unit =
    resumeWith(Result.failure(exception))

@SinceKotlin("1.3")
public interface Continuation<in T> {
    /**
     * The context of the coroutine that corresponds to this continuation.
     */
    public val context: CoroutineContext

    /**
     * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
     * return value of the last suspension point.
     */
    public fun resumeWith(result: Result<T>)
}
Copy the code

As you can see, the coroutine suspend function uses a Callback internally to return the result. Continuation calls Resume to return the result if it returns normally, or resumeWithException to throw an exception otherwise, just like Callback

The reason we write coroutines that look synchronous is because the compiler does a lot of work for you.

Note: When decompiling kotlin coroutine code using AndroidStudio, it will cause serious ide lag, and the decompiled Java code will have countless nested layers. It is impossible to decompile the coroutine code. A bug in AndroidStudio that doesn’t work with kotlin’s decomcompiled Java code

State transitions of coroutines

We have already done some analysis of the coroutine suspend function principle. If we use multiple suspend functions, how do they work together?

Note: the code below is someone else’s code that I copied

suspend fun main() {
    log(1) / /returnSuspended () is asuspendfunctionlog(returnSuspended())
    log(2) // Delay is another onesuspendDelay function (1000).log(3) / /returnImmediately is also asuspendfunctionlog(returnImmediately())
    log(4)}Copy the code

The corresponding Java implementation code logic is as follows (note that the following code logic is not very rigorous, only for learning to understand the use of coroutines)

Public class ContinuationImpl implements Continuation<Object> {// Label state default is 0 private int label = 0; private final Continuation<Unit> completion; public ContinuationImpl(Continuation<Unit> completion) { this.completion = completion; } @Override public CoroutineContextgetContext() {
        return EmptyCoroutineContext.INSTANCE;
    }

    @Override
    public void resumeWith(@NotNull Object o) {
        try {
            Object result = o;
            switch (label) {
                case0: { LogKt.log(1); . / / in SuspendFunctionsKt returnSuspended internal to callback to invoke this resumeWith method result = SuspendFunctionsKt. ReturnSuspended (this); // Label status + 1 label++;if (isSuspended(result)) return;
                }
                case1: { LogKt.log(result); LogKt.log(2); Result = delaykt. delay(1000, this); // Call this resumeWith method within delaykt. delay as a callback result = delaykt. delay(1000, this); // Label status + 1 label++;if (isSuspended(result)) return;
                }
                case2: { LogKt.log(3); . / / in SuspendFunctionsKt returnImmediately internal to callback to invoke this resumeWith method result = SuspendFunctionsKt. ReturnImmediately ( this); // Label status + 1 label++;if (isSuspended(result)) return;
                }
                case 3:{
                    LogKt.log(result);
                    LogKt.log(4);
                }
            }
            completion.resumeWith(Unit.INSTANCE);
        } catch (Exception e) {
            completion.resumeWith(e);
        }
    }

    private boolean isSuspended(Object result) {
        returnresult == IntrinsicsKt.getCOROUTINE_SUSPENDED(); }}Copy the code

As you can see, the coordination between multiple suspended functions is achieved by incrementing the label status field by one and calling the resumeWith method repeatedly

The summary is as follows:

  • The suspended function of a coroutine is essentially a callback, and the callback type is a Continuation
  • The execution of the coroutine body is a state machine, and every time it encounters a suspended function, it is a state transition, just as the label in our previous example increments to achieve the state transition

Finally, thank you very much for cracking the Kotlin Coroutine blog. This is a very good article about learning Coroutine