响应式编程|Kotlin与LiveData扩展函数实践技巧
前半部分介绍响应式编程的一些思想,后半部分介绍我们如何基于LiveData实现数据流设计的落地实践。
"一切都是对象 ( Everything is an Object! )"
上面是一个很简单的例子,一个简单的赋值语句,但是这种代码有一个缺陷,那就是如果我们想表达的并不是一个赋值动作,而是a和b之间的关系,即无论a,b如何变化,c永远是a,b的和。那么可以想见,我们需要花额外的精力去构建和维护一个b和a的关系。
而响应式编程的想法正是企图用某种操作符帮助你构建这种关系。
它的思想完全可以用下面的代码片段来表达:
2. 流编程思想 ( Thinking in Stream! )
响应式编程并不是一个全新的概念,甚至是一种古老的编程思想。最经典的例子是大家都非常熟悉的Excel的Function:
其实就算是长期接触Java的Android开发者,应该也接触过Rx系列组件,例如RxJava, RxSwift, RxKotlin等等,这些都是典型的基于响应式编程设计的组件。比如RxJava,它的强大能力有目共睹,我一直建议要从响应式编程的角度去理解Rx系列,就像它的命名由来那样“R:表示Reactive,x:表示extension”。
不同于面向对象的设计思想,在响应式编程的思想里,最基础的概念是 流(stream) 。从流的角度,反应性地思考和设计代码。我们首先思考的是 数据 ,让数据经过一系列的变化直接达到所需状态,这看起来就像是[观察者设计模式(Observer pattern)] (https://en.wikipedia.org/wiki/Observer_pattern)。
3. LiveData Extensions 扩展函数库
3.1 常规,但是不优雅的例子
在JAVA中我们想要订阅一个数据源,构建一个最简单的关系:“输出 = 输入”,在最基本的情况下,可以这么做:
在JAVA中,数据的处理过程隐藏在一个个回调里,数据本身被作为参数来回传递,即使是最简单的任务,也变得复杂起来。
而在理想的响应式代码里,这段程序应该是这样的:
3.2 RxJava能简化工作,但我们还想做的更好
上面的例子展示了一个最基础的语言层面上,构建一个响应式关系的例子。但是在Android开发中,我们面临更复杂的问题,例如我们通常最终需要将数据传递到UI线程,在界面上展示出来,我们还需要考虑Activity的生命周期,避免内存泄露等等问题。如果我们基于响应式编程的思想去开发这个程序,比如使用RxJava,继续完善这个例子:
构建一个关系“服务器返回的数据*2,再显示到界面上”:
3.3 最简单的方案
有没有更好的方法呢?我们把目光投在了LiveData和DataBinding上,LiveData的生命周期和Activity或者Fragment绑定,帮助我们解决了一些内存释放的问题,而DataBinding可以避免胶水代码,又可以承担观察LiveData的角色,那我们最理想的代码应该是这样:
不论对比RxJava还是最原始的方法,我们不仅大量减少了代码量,不必切换回主线程绘制UI,而且在这段程序中,我们突出了程序的重点:数据转换。
想要落地例子中的解决方案,我们的工作重点,就在于实现LiveData的扩展函数map。更多的,如果我们想构建多种多样的关系,我们就需要一整套的LiveData Extension库作为解决方案。
遗憾的是我反复翻阅LiveData的源码也未找到合适的观察者接口。不过,柳暗花明最终我在源码里找到了MediatorLiveData这个类,其中一个addSource方法提供了添加观察者的方法。
基于这个方法,我们可以给LiveData添加观察者,打通了最难的一步。很妙的是观察者本身也是LiveData类型,这样我们就可以实现链式观察者的程序。
例如最基础的map操作符:
如前所述,为了应付各种各样的构建关系的情况,事实上我们做了非常多的LiveData Extension。
例如:转换
例如:合并多个LiveData
更多的...
我们在git上开源了这些LiveData扩展函数,你可以通过这个网址[LiveDataExtensions](https://github.com/GunNan/LiveDataExtensions)获取到更多的操作符以及源码的信息。
好的解决方案大概率别人也会想到。对比我们设计的LiveData Extensions和github上两个同类型的库(这两个库排名最靠前,的star都在500左右)。
分别从操作符丰富度、是否支持kotlin、是否支持androidX等几个维度对比这三个库:
我们设计LiveDataExtensions的时候,充分参考了这两个库,综合了他们的优势。所以显然的,LiveDataExtensions的操作符会更加丰富,例如增加了合并操作符、异步操作符。此外,LiveDataExtensions还增加了androidX库的支持,以适应现在越来越多的应用迁移到androidX的情况。
QQ音乐TV版是一款在大屏设备上提供高质量音视频服务的应用。它背靠QQ音乐庞大曲库的内容,提供了丰富的音乐资源,通过精彩的UI视觉效果呈现给用户。
重构播放页,一方面是为了提高播放页的可维护性、可扩展性,另一方面是为了尝试最新的Kotlin语言特性与[《Jetpack应用架构指南》](https://developer.android.google.cn/jetpack/docs/guide)。
在我们重构过程中,大量使用了LiveData Extensions,极大地减少了代码量,提升了我们的工作效率。
以播放页三个最核心的类:播放页Activity,播放器PlayerHelper,播放页View为例,对比他们的循环复杂度WMC、基本复杂度ev(G)和圈复杂度v(G)。
可以看到,在使用了LiveData Extensions之后,我们的代码复杂度得到了明显的改善。
LiveData Extensions在处理页面交互的任务时,表现的极为出色。我们利用它,极大地降低了现有代码的复杂性(在我们播放页Activity的设计中,减少了90%的回调),提高了程序的可读性。
但是,考虑到LiveData会调度到主线程触发这个特点,LiveData Extensions里提供的map,filter等同步转换操作符不适合做耗时操作,例如网络、IO等等。如果确有耗时操作的需求,LiveData Extensions里还提供了switchMap操作符,这是一个异步操作符,它会生成一个新的LiveData,合并到当前的数据流中。
另外,我们更主张使用多个LiveData联合触发而非特别长的链式表达,如果确实需要特别长的链式表达,尤其是需要反复切换线程的情况,我们建议使用RxJava。
《Jetpack 应用架构指南》https://developer.android.google.cn/jetpack/docs/guide
《把 "格子衫" 改造得更时尚 | Kotlin & Jetpack 最佳实践技巧》https://cloud.tencent.com/developer/article/1520003
《Thinking In Stream》https://freecontent.manning.com/reactive-fundamentals-thinking-in-streams/