Key words: Kotlin coroutine Android Anko

Using coroutines on Android instead of callbacks or RxJava is actually quite easy, and we can even control the execution state of coroutines on a larger scale with the lifecycle of the UI

This article involves MainScope and AutoDispose source code: Kotlin-Coroutines-Android

1. Configure dependencies

We mentioned that if we’re developing on Android, we need to introduce

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutine_version'
Copy the code

The framework includes a Dispatcher for Android, which can be accessed via dispatchers.main. MainScope is also included for integration with the Android scope.

Anko also provides some handy methods, such as onClick, to introduce its dependencies if needed:

// Provides a convenient listener similar to onClick to receive suspend Lambda expressions
implementation "org.jetbrains.anko:anko-sdk27-coroutines:$anko_version"
// We provide BG and asReference. We have not yet followed up kotlin 1.3's official coroutine, but the code is relatively simple, and we can modify it ourselves if necessary
implementation "org.jetbrains.anko:anko-coroutines:$anko_version"
Copy the code

In a nutshell:

  • The Kotlinx-Coroutines-Android framework is mandatory and mainly provides a proprietary scheduler
  • Anko-sdk27-coroutines is optional and provides a cleaner extension to some UI components, such as onClick, but it has its own problems, which we’ll explore in more detail later
  • Anko-coroutines is for reference only. At the present stage (2019.4), we have not followed up the official version 1.3 coroutines, so try not to use them in the version after 1.3. The two methods provided are relatively simple, and can be modified and used by ourselves if necessary.

We’ve talked a lot about coroutines and how to use them, but we’ll give you a few practical tips on how to use coroutines on Android.

2. UI lifecycle scope

One of the things that Android developers often think about is making requests that automatically cancel when the current UI or Activity exits or is destroyed, and we’ve had various solutions to this problem with RxJava.

2.1 use MainScope

Coroutines have a natural property just enough to support this: scope. MainScope function is also provided officially. Let’s see how it can be used in detail:

val mainScope = MainScope()
launchButton.setOnClickListener {
    mainScope.launch {
        log(1)
        textView.text = async(Dispatchers.IO) {
            log(2)
            delay(1000)
            log(3)
            "Hello1111"
        }.await()
        log(4)}}Copy the code

We found that it works just like any other CoroutineScope. Any coroutine started from the same instance called mainScope will follow its scope definition.

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

The container is designed to handle exceptions from the top down, which is the same thing the Container is designed to handle with the Android Main scheduler. Therefore, unless the scheduler is explicitly declared in scope, the coroutine body is scheduled to execute on the main thread. So the above example runs as follows:

[main] 1 2019-04-29 06:51:00.657 D: [main] 1 2019-04-29 06:51:00.657 D: [2019-04-29 06:51:01.662 D: [2019-04-29 06:51:01.662 D: [2019-04-29 06:51:01.662 D: [2019-04-29 06:51:01.662 D: [main] 4Copy the code

If we trigger scope cancellation elsewhere immediately after the previous operation, the coroutine in that scope will no longer execute:

val mainScope = MainScope()

launchButton.setOnClickListener {
    mainScope.launch {
        ...
    }
}

cancelButton.setOnClickListener {
    mainScope.cancel()
    log("MainScope is cancelled.")}Copy the code

If we click the above two buttons in quick succession, the result is obvious:

[main] 1 2019-04-29 07:12:20.5d: [main] 1 2019-04-29 07:12:20.5d: [defaultDispatcher-worker-2] 2 2019-04-29 07:12:21.046 D: MainScope is cancelled.Copy the code

2.2 Constructing an abstract Activity with scope

Although we experimented with MainScope earlier and found it easy to control the cancellation of all coroutines in its scope, as well as the ability to seamlessly cut asynchronous tasks back to the main thread, these are all desirable features, but the writing is still not pretty.

The official recommendation is that we define an abstract Activity, for example:

abstract class ScopedActivity: Activity(), CoroutineScope by MainScope(){
    override fun onDestroy(a) {
        super.onDestroy()
        cancel()
    }
}
Copy the code

When the Activity exits, the corresponding scope is cancelled, and all requests made in the Activity are cancelled. When used, you just need to inherit this abstract class:

class CoroutineActivity : ScopedActivity() {
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_coroutine)
        launchButton.setOnClickListener {
            launch { // Call the ScopedActivity (MainScope) method directly. } } } suspendfun anotherOps(a)= coroutineScope { ... }}Copy the code

In addition to obtaining MainScope capabilities within the current Activity, you can also pass this Scope instance to other modules that need it. For example, a Presenter usually needs to have the same life cycle as an Activity, so you can pass this Scope if necessary:

class CoroutinePresenter(private val scope: CoroutineScope): CoroutineScope by scope{
    fun getUserData(a){ launch { ... }}}Copy the code

In most cases, the Presenter method is also called by the Activity directly. Therefore, it is possible to render the Presenter method as the suspend method and then nest the scope with coroutineScope. Nested subscopes are also cancelled, thus cancelling all subcoroutines:

class CoroutinePresenter {
    suspend fun getUserData(a)= coroutineScope { launch { ... }}}Copy the code

2.3 Provide scope for activities in a more friendly way

Abstract classes often break our inheritance, which can be very damaging to the development experience. Therefore, should we consider building an interface that can have scope and auto-cancel capabilities as long as the Activity implements this interface?

First we define an interface:

interface ScopedActivity {
    val scope: CoroutineScope
}
Copy the code

It is our humble wish to implement this interface to automatically get scope, but how to implement the scope member? It is not ideal to leave it to the interface implementer, so we can implement it ourselves, because we are an interface, so we have to deal with it like this:

interface MainScoped {
    companion object {
        internal val scopeMap = IdentityHashMap<MainScoped, MainScope>()
    }
    val mainScope: CoroutineScope
        get() = scopeMap[this as Activity]!!
}
Copy the code

The next thing to do is to create and remove the corresponding scopes where appropriate. We then define two methods:

interface MainScoped {...fun createScope(a){
        // Or change to a lazy implementation, which will be created on time
        val activity = this asActivity scopeMap[activity] ? : MainScope().also { scopeMap[activity] = it } }fun destroyScope(a){
        scopeMap.remove(this asActivity)? .cancel() } }Copy the code

Since we need the Activity to implement this interface, we can simply enforce it. Of course, if we consider robustness, we can do some exception handling. Here, as an example, we only provide the core implementation.

Now, where do you do create and cancel? Obviously it with Application. The most appropriate ActivityLifecycleCallbacks:

class ActivityLifecycleCallbackImpl: Application.ActivityLifecycleCallbacks {.override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?). {
        (activity as? MainScoped)? .createScope() }override fun onActivityDestroyed(activity: Activity) {
        (activity as? MainScoped)? .destroyScope() } }Copy the code

All that’s left is to register this listener in the Application, which everyone knows, but I won’t show you the code.

Let’s see how it works:

class CoroutineActivity : Activity(), MainScoped {
    override fun onCreate(savedInstanceState: Bundle?).{... launchButton.setOnClickListener { scope.launch { ... }}}}Copy the code

We can also add some useful methods to simplify the operation:

interface MainScoped {...fun <T> withScope(block: CoroutineScope. () -> T) = with(scope, block)
}
Copy the code

You can also write this in your Activity:

withScope { launch { ... }}Copy the code

Note that USES IdentityHashMap sample, this suggests that for the scope of reading and writing are thread safe, so don’t in other threads trying to obtain its value, unless you introduce a third party or to achieve a IdentityConcurrentHashMap, even so, Scope is also not designed to be accessed by other threads.

According to this idea, I provided a set of more complete solutions, which not only support activities but also support support-fragment versions of fragments above 25.1.0. Anko also provides some useful MainScope-based listener extensions that can be used with this framework:

api 'com. Bennyhuo. Kotlin: coroutines - android - mainscope: 1.0'
Copy the code

3. Use GlobalScope sparingly

3.1 What’s wrong with GlobalScope

We used GlobalScope in our previous examples, but GlobalScope does not inherit external scopes, so it is important to note that if you start the coroutine internally using GlobalScope after using MainScope for the binding life cycle, That means MainScope won’t work as well as it should.

The caution here is to be careful if you use constructors that have no dependent scope. For example, the onClick extension in Anko:

fun View.onClick(
        context: CoroutineContext = Dispatchers.Main,
        handler: suspend CoroutineScope. (v: View) -> Unit
) {
    setOnClickListener { v ->
        GlobalScope.launch(context, CoroutineStart.DEFAULT) {
            handler(v)
        }
    }
}
Copy the code

Perhaps this is just a convenience, since onClick is written with fewer characters than setOnClickListener and has a more event-like name, but the hidden risk is that the coroutine started with onClick will not be cancelled when the Activity is destroyed. You need to think clearly about the risks.

Of course, the fundamental reason Anko does this is that OnClickListener doesn’t have a scope with life-cycle benefits. What if I can’t start coroutines without GlobalScope? Given our previous example, there are other ways to solve this problem:

interface MainScoped {...fun View.onClickSuspend(handler: suspend CoroutineScope. (v: View) -> Unit) {
        setOnClickListener { v ->
            scope.launch {   handler(v)   }
        }
    }
}
Copy the code

In the MainScoped interface defined above, we can get a life-cycle supported MainScope instance through scope, so we can start the coroutine directly to run OnClickListener. So the key here is how to get the scope.

I have defined such a listener for you in the framework, see 2.3.

3.2 Coroutine version AutoDisposable

Of course, there are other ways to ensure that coroutines are cancelled in time than directly starting them with an appropriate scope.

You must have used RxJava, and you must know that you sent a task with RxJava, the task is not finished before the page is closed, if the task does not come back, the page will be leaked; If the task comes back later, the callback to update the UI will also have a high probability of null Pointers.

AutoDispose, Uber’s open source framework, is sure to be used. It is actually using OnAttachStateChangeListener of View, when the View is added, we will cancel all before use RxJava sent request.

  static final class Listener extends MainThreadDisposable implements View.OnAttachStateChangeListener {
    private final View view;
    private final CompletableObserver observer;

    Listener(View view, CompletableObserver observer) {
      this.view = view;
      this.observer = observer;
    }

    @Override public void onViewAttachedToWindow(View v) {}@Override public void onViewDetachedFromWindow(View v) {
      if(! isDisposed()) {// Do you see it?observer.onComplete(); }}@Override protected void onDispose(a) {
      view.removeOnAttachStateChangeListener(this); }}Copy the code

We can also create onClickAutoDisposable for Anko extension onClick.

fun View.onClickAutoDisposable (
        context: CoroutineContext = Dispatchers.Main,
        handler: suspend CoroutineScope. (v: View) -> Unit
) {
    setOnClickListener { v ->
        GlobalScope.launch(context, CoroutineStart.DEFAULT) {
            handler(v)
        }.asAutoDisposable(v)
    }
}
Copy the code

We know that launch launches a Job, so we can use asAutoDisposable to convert it to a Job that supports automatic cancellation:

fun Job.asAutoDisposable(view: View) = AutoDisposableJob(view, this)
Copy the code

So the implementation of AutoDisposableJob can be done by referring to the implementation of AutoDisposables:

class AutoDisposableJob(private val view: View, private val wrapped: Job)
    // We implement the Job interface, but we don't implement its methods directly. Wrapped is used to delegate the interface
     : Job by wrapped, OnAttachStateChangeListener {
    override fun onViewAttachedToWindow(v: View?). = Unit

    override fun onViewDetachedFromWindow(v: View?). {
        // When the View is removed, remove the coroutine
        cancel()
        view.removeOnAttachStateChangeListener(this)}private fun isViewAttached(a)= Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && view.isAttachedToWindow || view.windowToken ! =null

    init {
        if(isViewAttached()) {
            view.addOnAttachStateChangeListener(this)}else {
            cancel()
        }

        // Remove the listener immediately after the execution of the coroutine to avoid leakage
        invokeOnCompletion() {
            view.removeOnAttachStateChangeListener(this)}}}Copy the code

In this case, we can use this extension:

button.onClickAutoDisposable{
    try {
        val req = Request()
        val resp = async { sendRequest(req) }.await()
        updateUI(resp)
    } catch (e: Exception) {
        e.printStackTrace()
    }
}
Copy the code

When the button object is removed from the window, our coroutine receives the cancel instruction. Although the execution of the coroutine is not cancelled by the Activity’s onDestroy, it is tightly tied to the View’s click event. Even if the Activity is not destroyed, removing the View itself cancels the coroutine directly from the listener.

If you want to use this extension, I’ve already put it in jCenter for you to use:

api "Com. Bennyhuo. Kotlin: coroutines - android - autodisposable: 1.0"
Copy the code

Add to a dependency and use it.

4. Use the scheduler wisely

Using coroutines on Android is more about simplifying the writing of asynchronous logic and using scenarios more similar to RxJava. While using RxJava, I’ve seen a lot of developers just use the thread cutting feature, and since the RxJava thread cutting API is easy to use, it also causes a lot of mindless thread switching, which is actually not good. This is especially important when using coroutines, because the way coroutines switch threads is made simpler and more transparent by RxJava, which is a good thing, but it’s not being abused.

The preferred way to write this is that most of the UI logic is handled in the UI thread. Even if the UI uses dispatchers. Main to start the coroutine, if some IO operations are involved, use async to schedule them to Dispatchers.IO. When the result returns, the coroutine will help us cut back to the main thread — this is very similar to the single-threaded mode of working like Nodejs.

For uI-unrelated logic, such as batch offline data download tasks, the default scheduler is usually sufficient.

5. Summary

This article, mainly based on the theoretical knowledge we talked about in front, further to the specific combat perspective of Android migration, compared with other types of applications, Android as a UI program is the biggest characteristic of asynchronous UI to coordinate the life cycle, coroutine is no exception. Once we’re comfortable with the scoping rules of coroutines and how they relate to the UI lifecycle, we’ll be pretty comfortable with using coroutines.


Welcome to Kotlin Chinese community!

Chinese official website: www.kotlincn.net/

Official Chinese blog: www.kotliner.cn/

Public id: Kotlin

Zhihu column: Kotlin

CSDN: Kotlin Chinese Community

Nuggets: Kotlin Chinese Community

Brief: Kotlin Chinese Community