[Android] 记一次 MVVM 实践

6,164 阅读5分钟

背景:为什么选择了MVVM

公司的项目一直是以 Activity 为载体的 Android 式 MVC 架构,上手快,大多数页面代码也挺容易读的。只是某复杂业务的 Activity 会有上千行的代码,内部复杂的状态判断和异步逻辑特别多,而且原作者早已离职,每次提测都只能祈求这里不出 bug。

为了重构这里的代码,引入 MVP 或 MVVM 是比较合适的方案。精简原有的逻辑也可以一定程度上增加代码的可读性,但还是难以避免单个 Activity 中存在大量代码的问题。MVP 似乎是 Android 开发中更加流行的方案(样本数为2,两个同事都用过),但 MVP 的问题也是可预见的。恰好 Google 在 Jetpack 的 Architecture 中又宣传了一波 MVVM,跟大佬们商量之后,我决定先改写一个 Activity 试试。

MVC/MVP/MVVM 的介绍就省略了,下面简单说明一下用到的技术。

知识准备:数据绑定方法

数据绑定到 View 是 MVVM 的核心,对于不同的应用场景需要使用不同的数据绑定手段:

  • databinding 是常被提及的数据绑定框架,应用于将数据直接绑定到 layout 文件,可以有效的给 Activity 减负。
  • LiveData 是另一种 Google 推荐的方案,当 LiveData 作为数据源,既可以结合 databinding 将数据变化更新到 layout 中,也可以在 Activity 中设置 Observe 回调,实现更多 layout 中做不到的 View 效果(比如显示一个 dialog)。

二者都是 Google 出品,具体如何使用就不赘述了,后面会单独把遇到的坑记录下来。

实践过程简述

先简单说一下功能吧,改动之前的代码就不贴了,毕竟是公司的项目。

核心流程大概是这样的,还有一些暂停、模式切换和 postDelay 制造的延迟效果,整个流程中充满了各种异步操作,每个异步操作的回调中都需要判断一连串的状态,比如页面是否在显示中,音频是否正在播放等。

为了改写成 MVVM 模式的代码,首先要区分开哪些属于 View 层,哪些属于 ViewModel 层。只声明主要流程的话,内容有以下几点:

功能点 View 层 ViewModel 层
倒计时321 显示倒计时的 Dialog 每秒更新剩余时间(321)
播音频 显示播放进度条(%) 控制播放及回调处理
录音 显示录音进度条(时长等于原音频,并非手动停止) 控制录音及回调处理,录音计时停止
提交数据 提交中显示 loading 发起网络请求提交数据
暂停 显示暂停 Dialog,恢复其他 View 停止当前操作,回到当前题目最初状态

稍微分析一下,除了 Dialog 之外的 View 改动尽量使用 databinding 直接放在 layout 中,Dialog 的显示和隐藏都由一个 Boolean 类型的 LiveData 来控制,Activity 中基本只需要写 ViewModel/databinding 的初始化代码和 Dialog 相关代码。

ViewModel 中是全部的业务逻辑代码,但不持有任何 View 相关的实例,所有的改变都作用于数据即可。另外,由于 databinding 和 LiveData 都是对生命周期友好的框架,在 View 层销毁后更新数据并不会导致崩溃,在充满异步操作的逻辑中可以节省很多个 if 判断。

中途发现这个 RecyclerView 的交互太复杂了,databinding 中需要大量的 if 判断,为了加强代码的可读性,就把 adapter 相关的绑定采取 LiveData 实现了。最终 ViewModel 只有以下几个可观察数据:

简单画了一下重写前后的主流程图

修改前的

修改后的

提交后跳转页面的 Intent 需要传递多个数据,这里用了一个 Bundle 类型的 LiveData,在 Activity 中直接使用 Intent.putExtras 把值放进去即可。同理,在 ViewModel 中初始化数据时也可以把 getIntent().getExtras() 传过去。

踩坑记录

1. Kotlin 与 databinding 的兼容性问题以及不同 Android Studio 版本的区别。

在 Android Studio 3.2 之前,databinding 需要添加一个 kapt 的 databinding-compiler 依赖,具体的版本号也需要对应,否则编译后无法生成 Binding 类,就会出现 cannot resolve symbol xxxxBinding 的错误。

在 Android Studio 3.2 之后正好相反,如果不删除这个依赖就会报错。

具体描述可以看这个: stackoverflow.com/questions/5…

2. databinding 和 recyclerview 怎么合作。

直接和数据打交道的 View 很容易转换成使用 databinding 的代码,但 RecyclerView 这种需要一个 adapter 的 View 就会面临许多麻烦。github 上有些针对列表类 View 使用 databinding 的框架, 甚至可以做到不用再写 adapter,大多数场景下是可以用的,唯一的缺点是 debug 艰难。

但是我没选择这种方案,当 RecyclerView 中存在复杂的交互逻辑时,使用 databinding 会产生特别多的状态数据,反而让代码更加复杂了。不改变 adapter 的写法,可以用 LiveData 在 Activity 中把值传到 adapter 中。

3. 只进行一次触发的 View 操作如何处理。

大多数 Data 都表示一个状态,比如 Dialog 是否展示,TextView 的文本内容等。但有些特殊的 View 层变化只需要一个触发,比如 Toast 和 SnackBar。

从原则上看,ViewModel 显然不应该处理 Toast,但 Toast 这种非常轻量又可以用 ApplicationContext 来实现的,稍微违背一下规则我个人也能接受……

当然,不能推荐大家也都这么搞,google sample 的 architecture 项目中有一个SingleLiveEvent,可以直接复制出来用。项目地址:github.com/googlesampl…

完成后的一些个人理解

什么时候需要 MVVM?

什么时候都可以用。虽然对于简单的业务逻辑来说代码量会增加,但是 MVVM 的思考方式会让代码更容易维护。过去代码都写在 Activity 中时,方法的拆解通常只考虑功能的相关程度和代码行数,不会刻意区分 View 和逻辑。后期经过不同的人修改 bug,就会越来越混乱。

如何推广 MVVM

在团队中推广新的技术一向都不容易,好在 MVVM 学习成本不高,带来的好处也是显而易见的。用一点时间重写一遍项目中别人最不愿意改的页面,然后分享一下代码就好了。