关键词:Kotlin 协程 Android Anko
Android 上面使用协程来替代回调或者 RxJava 实际上是一件非常轻松的事儿,我们甚至可以在更大的范围内结合 UI 的生命周期做控制协程的执行状态~
我们曾经提到过,如果在 Android 上做开发,那么我们需要引入
Anko 也提供了一些比较方便的方法,例如 onClick 等等,如果需要,也可以引入它的依赖:
简单来说:
协程的原理和用法我们已经探讨了很多了,关于 Android 上面的协程使用,我们就只给出几点实践的建议。
Android 开发经常想到的一点就是让发出去的请求能够在当前 UI 或者 Activity 退出或者销毁的时候能够自动取消,我们在用 RxJava 的时候也有过各种各样的方案来解决这个问题。
协程有一个很天然的特性能刚够支持这一点,那就是作用域。官方也提供了 MainScope 这个函数,我们具体看下它的使用方法:
我们发现它其实与其他的 CoroutineScope 用起来没什么不一样的地方,通过同一个叫 mainScope 的实例启动的协程,都会遵循它的作用域定义,那么 MainScope 的定义时怎样的呢?
如果我们在触发前面的操作之后立即在其他位置触发作用域的取消,那么该作用域内的协程将不再继续执行:
如果我们快速依次点击上面的两个按钮,结果就显而易见了:
尽管我们前面体验了 MainScope 发现它可以很方便的控制所有它范围内的协程的取消,以及能够无缝将异步任务切回主线程,这都是我们想要的特性,不过写法上还是不够美观。
官方推荐我们定义一个抽象的 Activity,例如:
这样在 Activity 退出的时候,对应的作用域就会被取消,所有在该 Activity 中发起的请求都会被取消掉。使用时,只需要继承这个抽象类即可:
除了在当前 Activity 内部获得 MainScope 的能力外,还可以将这个 Scope 实例传递给其他需要的模块,例如 Presenter 通常也需要与 Activity 保持同样的生命周期,因此必要时也可以将该作用域传递过去:
多数情况下, Presenter 的方法也会被 Activity 直接调用,因此也可以将 Presenter 的方法生命成 suspend 方法,然后用 coroutineScope 嵌套作用域,这样 MainScope 被取消后,嵌套的子作用域一样也会被取消,进而达到取消全部子协程的目的:
抽象类很多时候会打破我们的继承体系,这对于开发体验的伤害还是很大的,因此我们是不是可以考虑构造一个接口,只要 Activity 实现这个接口就可以拥有作用域以及自动取消的能力呢?
首先我们定义一个接口:
我们有一个朴实的愿望就是希望实现这个接口就可以自动获得作用域,不过问题来了,这个 scope 成员要怎么实现呢?留给接口实现方的话显然不是很理想,自己实现吧,又碍于自己是个接口,因此我们只能这样处理:
接下来的事情就是在合适的实际去创建和取消对应的作用域了,我们接着定义两个方法:
因为我们需要 Activity 去实现这个接口,因此直接强转即可,当然如果考虑健壮性,可以做一些异常处理,这里作为示例仅提供核心实现。
剩下的就是在 Application 里面注册一下这个监听了,这个大家都会,我就不给出代码了。
我们看下如何使用:
我们也可以增加一些有用的方法来简化这个操作:
这样在 Activity 当中还可以这样写:
注意,示例当中用到了 IdentityHashMap,这表明对于 scope 的读写是非线程安全的,因此不要在其他线程试图去获取它的值,除非你引入第三方或者自己实现一个 IdentityConcurrentHashMap,即便如此,从设计上 scope也不太应该在其他线程访问。
我们之前做例子经常使用 GlobalScope,但 GlobalScope 不会继承外部作用域,因此大家使用时一定要注意,如果在使用了绑定生命周期的 MainScope 之后,内部再使用 GlobalScope 启动协程,意味着 MainScope 就不会起到应有的作用。
这里需要小心的是如果使用了一些没有依赖作用域的构造器,那么一定要小心。例如 Anko 当中的 onClick扩展:
也许我们也就是图个方便,毕竟 onClick 写起来可比 setOnClickListener 要少很多字符,同时名称上看也更加有事件机制的味道,但隐藏的风险就是通过 onClick 启动的协程并不会随着 Activity 的销毁而被取消,其中的风险需要自己思考清楚。
当然,Anko 会这么做的根本原因在于 OnClickListener 根本拿不到有生命周期加持的作用域。不用 GlobalScope 就无法启动协程,怎么办?结合我们前面给出的例子,其实这个事儿完全有别的解法:
我们在前面定义的 MainScoped 接口中,可以通过 scope 拿到有生命周期加持的 MainScope 实例,那么直接用它启动协程来运行 OnClickListener 问题不就解决了嘛。所以这里的关键点在于如何拿到作用域。
这样的 listener 我已经为大家在框架中定义好啦,请参见 2.3。
当然除了直接使用一个合适的作用域来启动协程之外,我们还有别的办法来确保协程及时被取消。
大家一定用过 RxJava,也一定知道用 RxJava 发了个任务,任务还没结束页面就被关闭了,如果任务迟迟不回来,页面就会被泄露;如果任务后面回来了,执行回调更新 UI 的时候也会大概率空指针。
考虑到前面提到的 Anko 扩展 onClick 无法取消协程的问题,我们也可以搞一个 onClickAutoDisposable。
我们知道 launch 会启动一个 Job,因此我们可以通过 asAutoDisposable 来将其转换成支持自动取消的类型:
那么 AutoDisposableJob 的实现只要参考 AutoDisposable 的实现依样画葫芦就好了 :
这样的话,我们就可以使用这个扩展了:
当 button 这个对象从 window 上撤下来的时候,我们的协程就会收到 cancel 的指令,尽管这种情况下协程的执行不会跟随 Activity 的 onDestroy 而取消,但它与 View 的点击事件紧密结合,即便 Activity 没有被销毁, View 本身被移除时也会直接将监听中的协程取消掉。
如果大家想要用这个扩展,我已经帮大家放到 jcenter 啦,直接使用:
在 Android 上使用协程,更多的就是简化异步逻辑的写法,使用场景更多与 RxJava 类似。在使用 RxJava 的时候,我就发现有不少开发者仅仅用到了它的切线程的功能,而且由于本身 RxJava 切线程 API 简单易用,还会造成很多无脑线程切换的操作,这样实际上是不好的。那么使用协程就更要注意这个问题了,因为协程切换线程的方式被 RxJava 更简洁,更透明,本来这是好事情,就怕被滥用。
这一篇文章,主要是基于我们前面讲了的理论知识,进一步往 Android 的具体实战角度迁移,相比其他类型的应用,Android 作为 UI 程序最大的特点就是异步要协调好 UI 的生命周期,协程也不例外。一旦我们把协程的作用域规则以及协程与 UI 生命周期的关系熟稔于心,那么相信大家使用协程时一定会得心应手的。