响应式编程|Kotlin与LiveData扩展函数实践技巧

Posted 腾讯音乐技术团队

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了响应式编程|Kotlin与LiveData扩展函数实践技巧相关的知识,希望对你有一定的参考价值。

前半部分介绍响应式编程的一些思想,后半部分介绍我们如何基于LiveData实现数据流设计的落地实践。

"一切都是对象 ( Everything is an Object! )"

当使用面向对象(Object-Oriented)的思维去开发时,我们被教导:“一切都是对象 ( Everything is an Object! )”。
我们自顶向下地分解问题,将模块封装为 交互(method) 和 状态(property)的集合,通过不断将模块拆分成更细的维度,最后形成一个个具有明确定义的内聚性的 类(Class) 。类是面向对象思想的主要工作单元。
面向对象的编程概念取得了巨大成功,几乎可以解决所有问题。但是,有些时候面向对象的程序却显得乏力。

1. 一个简单的例子(Example)

上面是一个很简单的例子,一个简单的赋值语句,但是这种代码有一个缺陷,那就是如果我们想表达的并不是一个赋值动作,而是a和b之间的关系,即无论a,b如何变化,c永远是a,b的和。那么可以想见,我们需要花额外的精力去构建和维护一个b和a的关系。

而响应式编程的想法正是企图用某种操作符帮助你构建这种关系。

它的思想完全可以用下面的代码片段来表达:

响应式编程|Kotlin与LiveData扩展函数实践技巧
换言之,我们希望给c的定义是一种 关系而不是 赋值

2. 流编程思想 ( Thinking in Stream! )

响应式编程并不是一个全新的概念,甚至是一种古老的编程思想。最经典的例子是大家都非常熟悉的Excel的Function:

响应式编程|Kotlin与LiveData扩展函数实践技巧

其实就算是长期接触Java的android开发者,应该也接触过Rx系列组件,例如RxJava, RxSwift, RxKotlin等等,这些都是典型的基于响应式编程设计的组件。比如RxJava,它的强大能力有目共睹,我一直建议要从响应式编程的角度去理解Rx系列,就像它的命名由来那样“R:表示Reactive,x:表示extension”。

不同于面向对象的设计思想,在响应式编程的思想里,最基础的概念是 流(stream) 。从流的角度,反应性地思考和设计代码。我们首先思考的是 数据 ,让数据经过一系列的变化直接达到所需状态,这看起来就像是[观察者设计模式(Observer pattern)] (https://en.wikipedia.org/wiki/Observer_pattern)。

响应式编程|Kotlin与LiveData扩展函数实践技巧
数据源Data经过一系列的变化,直接达到最终在View层展示的状态。例如从远程获取数据的 fetch 方法可以理解为改变数据源的一个“水坝”。理想情况下,数据在流转的过程中,每个环节都不会存储数据,也不会修改数据源的数据,而是生成一个新的数据传递给下一个环节。

3. LiveData Extensions 扩展函数库

本文想要介绍的LiveData Extension借鉴了很多响应式编程的原理,我们要做的就是将数据包装到流中,然后订阅它以监听它的变化。

3.1 常规,但是不优雅的例子

在JAVA中我们想要订阅一个数据源,构建一个最简单的关系:“输出 = 输入”,在最基本的情况下,可以这么做:

响应式编程|Kotlin与LiveData扩展函数实践技巧

在JAVA中,数据的处理过程隐藏在一个个回调里,数据本身被作为参数来回传递,即使是最简单的任务,也变得复杂起来。

而在理想的响应式代码里,这段程序应该是这样的:

响应式编程|Kotlin与LiveData扩展函数实践技巧
在这个例子里,我们更清晰地看到,扩展函数与高阶函数的意义不仅仅是语法糖,更在于实现 链式表达。链式表达是易于理解的,人总会习惯地认为书写在先,时间顺序上也是先发生的。

3.2 RxJava能简化工作,但我们还想做的更好

上面的例子展示了一个最基础的语言层面上,构建一个响应式关系的例子。但是在Android开发中,我们面临更复杂的问题,例如我们通常最终需要将数据传递到UI线程,在界面上展示出来,我们还需要考虑Activity的生命周期,避免内存泄露等等问题。如果我们基于响应式编程的思想去开发这个程序,比如使用RxJava,继续完善这个例子:

构建一个关系“服务器返回的数据*2,再显示到界面上”:

响应式编程|Kotlin与LiveData扩展函数实践技巧
RxJava已经比最原始的方法优秀很多,但是我们仍然不能避免一些胶水代码,同时整段程序最重要的功能 数据转换被隐藏在这段长长的链式表达里,因为我们需要进行线程切换和内存释放。

3.3 最简单的方案

有没有更好的方法呢?我们把目光投在了LiveData和DataBinding上,LiveData的生命周期和Activity或者Fragment绑定,帮助我们解决了一些内存释放的问题,而DataBinding可以避免胶水代码,又可以承担观察LiveData的角色,那我们最理想的代码应该是这样:

响应式编程|Kotlin与LiveData扩展函数实践技巧

不论对比RxJava还是最原始的方法,我们不仅大量减少了代码量,不必切换回主线程绘制UI,而且在这段程序中,我们突出了程序的重点:数据转换

想要落地例子中的解决方案,我们的工作重点,就在于实现LiveData的扩展函数map。更多的,如果我们想构建多种多样的关系,我们就需要一整套的LiveData Extension库作为解决方案。

3.4 实现方案

遗憾的是我反复翻阅LiveData的源码也未找到合适的观察者接口。不过,柳暗花明最终我在源码里找到了MediatorLiveData这个类,其中一个addSource方法提供了添加观察者的方法。

响应式编程|Kotlin与LiveData扩展函数实践技巧

基于这个方法,我们可以给LiveData添加观察者,打通了最难的一步。很妙的是观察者本身也是LiveData类型,这样我们就可以实现链式观察者的程序。

例如最基础的map操作符:

响应式编程|Kotlin与LiveData扩展函数实践技巧

如前所述,为了应付各种各样的构建关系的情况,事实上我们做了非常多的LiveData Extension。

例如:转换

响应式编程|Kotlin与LiveData扩展函数实践技巧

例如:合并多个LiveData

响应式编程|Kotlin与LiveData扩展函数实践技巧

更多的...

响应式编程|Kotlin与LiveData扩展函数实践技巧

我们在git上开源了这些LiveData扩展函数,你可以通过这个网址[LiveDataExtensions](https://github.com/GunNan/LiveDataExtensions)获取到更多的操作符以及源码的信息。


4. 对比

好的解决方案大概率别人也会想到。对比我们设计的LiveData Extensions和github上两个同类型的库(这两个库排名最靠前,的star都在500左右)。

响应式编程|Kotlin与LiveData扩展函数实践技巧

分别从操作符丰富度、是否支持kotlin、是否支持androidX等几个维度对比这三个库:

响应式编程|Kotlin与LiveData扩展函数实践技巧

我们设计LiveDataExtensions的时候,充分参考了这两个库,综合了他们的优势。所以显然的,LiveDataExtensions的操作符会更加丰富,例如增加了合并操作符、异步操作符。此外,LiveDataExtensions还增加了androidX库的支持,以适应现在越来越多的应用迁移到androidX的情况。


5. 在QQ音乐TV版播放页重构中应用

响应式编程|Kotlin与LiveData扩展函数实践技巧

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之后,我们的代码复杂度得到了明显的改善。


6. 局限性

LiveData Extensions在处理页面交互的任务时,表现的极为出色。我们利用它,极大地降低了现有代码的复杂性(在我们播放页Activity的设计中,减少了90%的回调),提高了程序的可读性。

但是,考虑到LiveData会调度到主线程触发这个特点,LiveData Extensions里提供的map,filter等同步转换操作符不适合做耗时操作,例如网络、IO等等。如果确有耗时操作的需求,LiveData Extensions里还提供了switchMap操作符,这是一个异步操作符,它会生成一个新的LiveData,合并到当前的数据流中。

另外,我们更主张使用多个LiveData联合触发而非特别长的链式表达,如果确实需要特别长的链式表达,尤其是需要反复切换线程的情况,我们建议使用RxJava。


7.扩展阅读
  • 《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/


以上是关于响应式编程|Kotlin与LiveData扩展函数实践技巧的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin函数式编程 ③ ( 早集合与惰性集合 | 惰性集合-序列 | generateSequence 序列创建函数 | 序列代码示例 | take 扩展函数分析 )

Kotlin函数式编程 ③ ( 早集合与惰性集合 | 惰性集合-序列 | generateSequence 序列创建函数 | 序列代码示例 | take 扩展函数分析 )

Spring5+Kotlin响应式编程学习

带有livedata的kotlin扩展函数返回null?

理解响应式编程,来一波LiveData的深入解析吧!

Kotlin Flow响应式编程,操作符函数进阶