Cracking Kotlin coroutines (8) - Android articles

Cracking Kotlin coroutines (8) - Android articles

insert image description here

Keywords: Kotlin Coroutine Android Anko

It is actually very easy to use coroutines instead of callbacks or RxJava on Android, and we can even control the execution status of coroutines in a larger range in combination with the life cycle of the UI~

1. Configuration dependencies

We have mentioned that if we develop on Android, we need to introduce

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

copy

This framework contains Android-specific Dispatcher, we can get this instance Dispatchers.Mainthrough ; it also contains MainScope, used to combine with the Android scope.

Anko also provides some more convenient methods, such as onClickand so on , if necessary, you can also introduce its dependencies:

//提供 onClick 类似的便捷的 listener,接收 suspend Lambda 表达式
implementation "org.jetbrains.anko:anko-sdk27-coroutines:$anko_version"
//提供 bg 、asReference,尚未没有跟进 kotlin 1.3 的正式版协程,不过代码比较简单,如果需要可以自己改造
implementation "org.jetbrains.anko:anko-coroutines:$anko_version"

copy

simply put:

  • The kotlinx-coroutines-android framework is a must, and it mainly provides a dedicated scheduler
  • anko-sdk27-coroutines is optional and provides some more concise extensions of UI components, such as onClick, but it also has its own problems, which we will discuss in detail later
  • anko-coroutines is for reference only. At this stage (2019.4), since the official version 1.3 of coroutines has not been followed up, try not to use it in versions after 1.3. The two methods provided are relatively simple. If necessary, you can modify and use them yourself.

We have already discussed a lot about the principle and usage of coroutines. Regarding the use of coroutines on Android, we only give a few practical suggestions.

2. UI life cycle scope

One thing that Android development often thinks of is to make the outgoing request automatically cancel when the current UI or Activity exits or is destroyed. We have also used various solutions to solve this problem when using RxJava.

2.1 Using MainScope

Coroutines have a very natural feature that is just enough to support this, and that is scope. The official also provides MainScopethis function, let's see how to use it:

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

We found that it is actually no different from other CoroutineScopeusages . mainScopeThe coroutines started through the same instance called will follow its scope definition, so MainScopewhat is the definition of ?

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

It turned out to SupervisorJobbe Dispatchers.Mainjust integrated, and its exception propagation is top-down, which is consistent with the behavior supervisorScopeof , in addition, the scheduling in the scope is based on the scheduler of the Android main thread, so unless the scheduler is explicitly declared in the scope, the coordination The program body is scheduled to execute on the main thread. So the result of running the above example is as follows:

2019-04-29 06:51:00.657 D: [main] 1
2019-04-29 06:51:00.659 D: [DefaultDispatcher-worker-1] 2
2019-04-29 06:51:01.662 D: [DefaultDispatcher-worker-2] 3
2019-04-29 06:51:01.664 D: [main] 4

If we trigger the cancellation of the scope elsewhere immediately after triggering the previous operation, then the coroutines in the scope will not continue to execute:

val mainScope = MainScope()

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

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

If we quickly click on the two buttons above in sequence, the result is obvious:

2019-04-29 07:12:20.625 D: [main] 1
2019-04-29 07:12:20.629 D: [DefaultDispatcher-worker-2] 2
2019-04-29 07:12:21.046 D: [main] MainScope is cancelled.

2.2 Construct an abstract Activity with scope

Although we have experienced it before MainScopeand found that it can easily control the cancellation of all coroutines within its scope, and seamlessly switch asynchronous tasks back to the main thread. These are the features we want, but the writing is still not beautiful enough.

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

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

In this way, when Activitythe exits , the corresponding scope will be cancelled, and all Activityrequests initiated in the will be cancelled. When using, you only 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 { // 直接调用 ScopedActivity 也就是 MainScope 的方法
                ...
            }
        }
    }

    suspend fun anotherOps() = coroutineScope {
        ...
    }
}

In addition to the capabilities obtained within the Activitycurrent MainScope, you can also pass this Scope instance to other modules that need it. For example, Presenterit usually needs to Activitymaintain the same life cycle as , so you can also pass this scope if necessary:

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

In most cases, Presenterthe method of will also be called Activitydirectly , so Presenterthe method of can also be created suspendas a method , and then use coroutineScopethe nested scope, so that after MainScopethe is cancelled, the nested sub-scopes will also be cancelled, thereby achieving the cancellation of all The purpose of sub-coroutines:

class CoroutinePresenter {
    suspend fun getUserData() = coroutineScope {
        launch { ... }
    }
}

2.3 Provide scope for Activity more friendly

Abstract classes often break our inheritance system, which is still very harmful to the development experience. Therefore, can we consider constructing an interface, as long as this interface Activityis implemented cancel?

First we define an interface:

interface ScopedActivity {
    val scope: CoroutineScope
}

We have a simple wish that we can automatically obtain the scope by implementing this interface, but the question is, how to realize this scopemember ? It is obviously not ideal to leave it to the interface implementer. Let’s implement it ourselves. Since we are an interface, we can only deal with it like this:

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

The next thing is to create and cancel the corresponding scope when appropriate. We then define two methods:

interface MainScoped {
    ...
    fun createScope(){
        //或者改为 lazy 实现,即用到时再创建
        val activity = this as Activity
        scopeMap[activity] ?: MainScope().also { scopeMap[activity] = it }
    }

    fun destroyScope(){
        scopeMap.remove(this as Activity)?.cancel()
    }
}

Because we need Activityto implement this interface, we can just force it directly. Of course, if we consider robustness, we can do some exception handling. Here we only provide the core implementation as an example.

The next step is to consider where to complete the creation and cancellation? Obviously this is Application.ActivityLifecycleCallbacksthe :

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

    override fun onActivityDestroyed(activity: Activity) {
        (activity as? MainScoped)?.destroyScope()
    }
}

All that's left is to register the listener Applicationin , which everyone knows, so I won't give the code.

Let's see how to use it:

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

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

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

In this Activityway it can also be written like this:

withScope {
    launch { ... }
}

Note that it is used in the example IdentityHashMap, which indicates that the reading and writing of scope is not thread-safe, so don't try to get its value in other threads, unless you introduce a third party or implement one yourself IdentityConcurrentHashMap, even so, it scopeis Should be accessed from other threads.

According to this idea, I provide a more complete solution, which not only supports Activitybut also support-fragment versions above 25.1.0 Fragment, and provides some MainScopeuseful , source address: kotlin- coroutines-android (https://github.com/enbandari/kotlin-coroutines-android), introduce this framework to use:

api 'com.bennyhuo.kotlin:coroutines-android-mainscope:1.0'

3. Use GlobalScope with caution

3.1 What's wrong with GlobalScope

We used it often in previous examples GlobalScope, but GlobalScopewill not inherit the external scope, so you must pay attention when using it. If MainScopeafter using the binding life cycle, use internally to GlobalScopestart the coroutine, it means that MainScopeit will not play its due role. role.

The thing to be careful here is that if you use some constructors without dependent scope, you must be careful. For example onClickthe extension :

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

Maybe we are just trying to make it easier. After all, onClickit setOnClickListenertakes a lot less characters to write than , and the name also looks more like an event mechanism, but the hidden risk is that the coroutines onClickstarted will not Activitybe canceled with the destruction of . The risks need to be considered clearly by yourself.

Of course, the fundamental reason why Anko will do this is that there is no scope with life cycle blessing OnClickListenerat all . GlobalScopeThe coroutine cannot be started without using , what should I do? Combined with the example we gave earlier, there is actually a completely different solution to this matter:

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

In MainScopedthe interface , we can scopeget MainScopethe instance , then use it directly to start the coroutine to run OnClickListener. The problem will be solved. So the key point here is how to get the scope.

I have defined such a listener in the framework for everyone, please refer to 2.3.

3.2 Coroutine version of AutoDisposable

Of course, in addition to directly using a suitable scope to start the coroutine, we have other ways to ensure that the coroutine is canceled in time.

You must have used RxJava, and you must know that when you send a task with RxJava, the page will be closed before the task ends. If the task does not come back for a long time, the page will be leaked; if the task comes back later, execute the callback to update the UI. Sometimes there is a high probability of a null pointer.

Therefore, everyone will definitely use Uber's open source framework AutoDispose (https://github.com/uber/AutoDispose). It is actually Viewused OnAttachStateChangeListener. When Viewis taken down, we cancel all the requests sent by RxJava before.

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()) {
      //看到没看到没看到没?
        observer.onComplete();
      }
    }

    @Override protected void onDispose() {
      view.removeOnAttachStateChangeListener(this);
    }
  }

Considering the problem that the Anko extension mentioned above onClickcannot cancel the coroutine, we can also make one onClickAutoDisposable.

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

We know launchthat will start one Job, so we can convert it to a type that supports autocancellation asAutoDisposablewith :

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

copy

Then the implementation AutoDisposableJobof just refer to the implementation of AutoDisposable and do the same:

class AutoDisposableJob(private val view: View, private val wrapped: Job)
    //我们实现了 Job 这个接口,但没有直接实现它的方法,而是用 wrapped 这个成员去代理这个接口
     : Job by wrapped, OnAttachStateChangeListener {
    override fun onViewAttachedToWindow(v: View?) = Unit

    override fun onViewDetachedFromWindow(v: View?) {
        //当 View 被移除的时候,取消协程
        cancel()
        view.removeOnAttachStateChangeListener(this)
    }

    private fun isViewAttached() =
            Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && view.isAttachedToWindow || view.windowToken != null

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

        //协程执行完毕时要及时移除 listener 免得造成泄露
        invokeOnCompletion() {
            view.removeOnAttachStateChangeListener(this)
        }
    }
}

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()
    }
}

When buttonthis object is removed from the window, our coroutine will receive the cancel instruction. Although in this case the execution of the coroutine will not beActivity canceled following the , it is closely integrated with the click event of , even if there is no When it is destroyed, it will directly cancel the monitoring coroutine when it is removed.onDestroyViewActivityView

If you want to use this extension, I have put it in jcenter for you, use it directly:

api "com.bennyhuo.kotlin:coroutines-android-autodisposable:1.0"

copy

Add it to the dependencies and use it. The source code is also here: kotlin-coroutines-android (https://github.com/enbandari/kotlin-coroutines-android), don't forget to star.

4. Reasonable use of the scheduler

Using coroutines on Android is more about simplifying the writing of asynchronous logic, and the usage scenarios are more similar to RxJava. When using RxJava, I found that many developers only use its thread cutting function, and because the RxJava thread cutting API itself is simple and easy to use, it will also cause a lot of brainless thread switching operations, which is actually not good. Then you should pay more attention to this problem when using coroutines, because the way of coroutines switching threads is more concise and more transparent by RxJava. This is a good thing, but it is afraid of being abused.

The recommended writing method is that most of the UI logic is processed in the UI thread. Even if it is used Dispatchers.Mainto , if some io operations are involved, use asyncto schedule it Dispatchers.IOon , and the coroutine will help us switch when the result is returned. Back to the main thread - this is very similar to the single-threaded working mode of Nodejs.

For some UI-independent logic, such as batch offline data download tasks, the default scheduler is usually sufficient.

5. Summary

This article is mainly based on the theoretical knowledge we mentioned earlier, and further migrates to the specific practical perspective of Android. Compared with other types of applications, the biggest feature of Android as a UI program is that it needs to coordinate the life cycle of the UI asynchronously. Cheng is no exception. Once we are familiar with the scope rules of coroutines and the relationship between coroutines and UI life cycle, I believe that everyone will be handy when using coroutines.

Kotlin coroutine learning materials can be obtained for free by scanning the QR code below!

"The most detailed Android version of kotlin coroutine entry advanced combat in history"

Chapter 1 Introduction to the Basics of Kotlin Coroutines

            ● 协程是什么

            ● 什么是Job 、Deferred 、协程作用域

            ● Kotlin协程的基础用法

img

Chapter 2 Preliminary Explanation of Key Knowledge Points of Kotlin Coroutine

            ● 协程调度器

            ● 协程上下文

            ● 协程启动模式

            ● 协程作用域

            ● 挂起函数

img

Chapter 3 Exception Handling of Kotlin Coroutines

            ● 协程异常的产生流程

            ● 协程的异常处理

img

Chapter 4 Basic application of kotlin coroutines in Android

            ● Android使用kotlin协程

            ● 在Activity与Framgent中使用协程

            ● ViewModel中使用协程

            ● 其他环境下使用协程

img

Chapter 5 Network request encapsulation of kotlin coroutine

            ● 协程的常用环境

            ● 协程在网络请求下的封装及使用

            ● 高阶函数方式

            ● 多状态函数返回值方式

Guess you like

Origin blog.csdn.net/Android_XG/article/details/130429922