对于开发者来讲,在开发过程中可以大幅减少UI层和Model层相互调用的代码,转而将更多的重心投入到业务代码的编写。
ViewModel 的概念就是这样被提出来的,我对它的形容类似一个 状态存储器 , 它存储着UI中各种各样的状态, 以 登录界面 为例,我们很容易想到最简单的两种状态 :
class LoginViewModel {
val username: String // 用户名输入框中的内容
val password: String // 密码输入框中的内容
}
先不纠结于代码的细节,现在我们知道了ViewModel的重心是对 数据状态的维护。接下来我们来看看,在17年之前Google还没有推出ViewModel组件之前,Android领域内MVVM 百花齐放的各种形态 吧。
1.群雄割据时代的百花齐放
说到MVVM就不得不提Google在2015年IO大会上提出的DataBinding
库,它的发布直接促进了MVVM在Android领域的发展,开发者可以直接通过将数据状态通过 伪Java代码 的形式绑定在xml
布局文件中,从而将MVVM模式的开发流程形成一个 闭环:
通过 伪Java代码 将UI的逻辑直接粗暴的添加进xml
布局文件中达到和View
的绑定,DataBinding
这种实现方式引起了 强烈的争论。直至如今,依然有很多开发者无法接受DataBinding
,这是完全可以理解的,因为它确实 很难定位语法的错误和运行时的崩溃原因。
MVVM模式并不一定依赖于DataBinding
,但是除了DataBinding
,开发者当时并没有足够多的选择——直至目前,仍然有部分的MVVM开发者坚持不使用 DataBinding
,取而代之使用生态圈极为丰富的RxJava
(或者其他)代替 DataBinding
的数据绑定。
如果说当时对于 数据绑定 的库至少还有官方的DataBinding
可供参考,ViewModel
的规范化则是非常困难——基于ViewModel
层进行状态的管理这个基本的约束,不同的项目、不同的依赖库加上不同的开发者,最终代码中对于 状态管理 的实现方式都有很大的不同。
比如,有的开发者,将 ViewModel 层像 MVP 一样定义为一个接口:
interface IViewModel
open class BaseViewModel: IViewModel
也有开发者(比如这个repo)直接将ViewModel层继承了可观察的属性(比如dataBinding
库的BaseObservable
),并持有Context
的引用:
public class CommentViewModel extends BaseObservable {
@BindingAdapter(“containerMargin”)
public static void setContainerMargin(View view, boolean isTopLevelComment) {
//…
}
}
一千个人有一千个哈姆雷特,不同的MVVM也有截然不同的实现方式,这种百花齐放的代码风格、难以严格统一的 开发流派 导致代码质量的参差不齐,代码的可读性更是天差地别。
再加上DataBinding
本身导致代码阅读性的降低,真可谓南门北派华山论剑,各种思想喷涌而出——从思想的碰撞交流来讲,这并非坏事,但是对于当时想学习MVVM的我来讲,实在是看得眼花缭乱,在学习接触的过程中,我也不可避免的走了许多弯路。
2.Google对于ViewModel的规范化尝试
我们都知道Google在去年的 I/O 大会非常隆重地推出了一系列的 架构组件, ViewModel正是其中之一,也是本文的主角。
有趣的是,相比较于惹眼的 Lifecycle
和 LiveData
, ViewModel
显得非常低调,它主要提供了这些特性:
- 配置更改期间自动保留其数据 (比如屏幕的横竖旋转)
Activity
、Fragment
等UI组件之间的通信
如果让我直接吹捧ViewModel
多么多么优秀,我会非常犯难,因为它表面展现的这些功能实在不够惹眼,但是有幸截止目前为止,我花费了一些笔墨阐述了ViewModel
在这之前的故事——它们是接下来正文不可缺少的铺垫。
3.ViewModel在这之前的窘境
也许您尚未意识到,在官方的ViewModel
发布之前,MVVM开发模式中,ViewModel层的一些窘境,但实际上我已经尽力通过叙述的方式将这些问题描述出来:
3.1 更规范化的抽象接口
在官方的ViewModel
发布之前,ViewModel
层的基类多种多样,内部的依赖和公共逻辑更是五花八门。新的ViewModel
组件直接对ViewModel
层进行了标准化的规范,即使用ViewModel
(或者其子类AndroidViewModel
)。
同时,Google官方建议ViewModel
尽量保证 纯的业务代码,不要持有任何View层(Activity
或者Fragment
)或Lifecycle
的引用,这样保证了ViewModel
内部代码的可测试性,避免因为Context
等相关的引用导致测试代码的难以编写(比如,MVP中Presenter层代码的测试就需要额外成本,比如依赖注入或者Mock,以保证单元测试的进行)。
3.2 更便于保存数据
由系统响应用户交互或者重建组件,用户无法操控。当组件被销毁并重建后,原来组件相关的数据也会丢失——最简单的例子就是屏幕的旋转,如果数据类型比较简单,同时数据量也不大,可以通过onSaveInstanceState()
存储数据,组件重建之后通过onCreate()
,从中读取Bundle
恢复数据。但如果是大量数据,不方便序列化及反序列化,则上述方法将不适用。
ViewModel
的扩展类则会在这种情况下自动保留其数据,如果Activity
被重新创建了,它会收到被之前相同ViewModel
实例。当所属Activity
终止后,框架调用ViewModel
的onCleared()
方法释放对应资源:
这样看来,ViewModel
是有一定的 作用域 的,它不会在指定的作用域内生成更多的实例,从而节省了更多关于 状态维护(数据的存储、序列化和反序列化)的代码。
ViewModel
在对应的 作用域 内保持生命周期内的 局部单例,这就引发一个更好用的特性,那就是Fragment
、Activity
等UI组件间的通信。
3.3 更方便UI组件之间的通信
一个Activity
中的多个Fragment
相互通讯是很常见的,如果ViewModel
的实例化作用域为Activity
的生命周期,则两个Fragment
可以持有同一个ViewModel的实例,这也就意味着数据状态的共享:
public class AFragment extends Fragment {
private CommonViewModel model;
public void onActivityCreated() {
model = ViewModelProviders.of(getActivity()).get(CommonViewModel.class);
}
}
public class BFragment extends Fragment {
private CommonViewModel model;
public void onActivityCreated() {
model = ViewModelProviders.of(getActivity()).get(CommonViewModel.class);
}
}
上面两个Fragment
getActivity()
返回的是同一个宿主Activity
,因此两个Fragment
之间返回的是同一个ViewModel
。
我不知道正在阅读本文的您,有没有冒出这样一个想法:
ViewModel提供的这些特性,为什么感觉互相之间没有联系呢?
这就引发下面这个问题,那就是:
这些特性的本质是什么?
4. ViewModel:对状态的持有和维护
ViewModel
层的根本职责,就是负责维护UI的状态,追根究底就是维护对应的数据——毕竟,无论是MVP还是MVVM,UI的展示就是对数据的渲染。
- 1.定义了
ViewModel
的基类,并建议通过持有LiveData
维护保存数据的状态; - 2.
ViewModel
不会随着Activity
的屏幕旋转而销毁,减少了维护状态的代码成本(数据的存储和读取、序列化和反序列化); - 3.在对应的作用域内,保正只生产出对应的唯一实例,多个
Fragment
维护相同的数据状态,极大减少了UI组件之间的数据传递的代码成本。
现在我们对于ViewModel
的职责和思想都有了一定的了解,按理说接下来我们应该阐述如何使用ViewModel
了,但我想先等等,因为我觉得相比API的使用,掌握其本质的思想会让你在接下来的代码实践中如鱼得水。
不,不是源码解析…
通过库提供的API接口作为开始,阅读其内部的源码,这是标准掌握代码内部原理的思路,这种方式的时间成本极高,即使有相关源码分析的博客进行引导,文章中大片大片的源码和注释也足以让人望而却步,于是我理所当然这么想:
先学会怎么用,再抽空系统学习它的原理和思想吧…
发现没有,这和上学时候的学习方式竟然截然相反,甚至说本末倒置也不奇怪——任何一个物理或者数学公式,在使用它做题之前,对它背后的基础理论都应该是优先去系统性学习掌握的(比如,数学公式的学习一般都需要先通过一定方式推导和证明),这样我才能拿着这个知识点对课后的习题举一反三。这就好比,如果一个老师直接告诉你一个公式,然后啥都不说让你做题,这个老师一定是不合格的。
我也不是很喜欢大篇幅地复制源码,我准备换个角度,站在Google工程师的角度看看怎么样设计出一个ViewModel
。
站在更高的视角,设计ViewModel
现在我们是Google工程师,让我们再回顾一下ViewModel
应起到的作用:
- 1.规范化了
ViewModel
的基类; - 2.
ViewModel
不会随着Activity
的屏幕旋转而销毁; - 3.在对应的作用域内,保正只生产出对应的唯一实例,保证UI组件间的通信。
1.设计基类
这个简直太简单了:
public abstract class ViewModel {
protected void onCleared() {
}
}
我们定义一个抽象的ViewModel
基类,并定义一个onCleared()
方法以便于释放对应的资源,接下来,开发者只需要让他的XXXViewModel
继承这个抽象的ViewModel
基类即可。
2.保证数据不随屏幕旋转而销毁
这是一个很神奇的功能,但它的实现方式却非常简单,我们先了解这样一个知识点:
setRetainInstance(boolean)
是Fragment
中的一个方法。将这个方法设置为true就可以使当前Fragment
在Activity
重建时存活下来
这似乎和我们的功能非常吻合,于是我们不禁这样想,可不可以让Activity
持有这样一个不可见的Fragment
(我们干脆叫他HolderFragment
),并让这个HolderFragment
调用setRetainInstance(boolean)
方法并持有ViewModel
——这样当Activity
因为屏幕的旋转销毁并重建时,该Fragment
存储的ViewModel
自然不会被随之销毁回收了:
public class HolderFragment extends Fragment {
public HolderFragment() { setRetainInstance(true); }
private ViewModel mViewModel;
// getter、setter…
}
当然,考虑到一个复杂的UI组件可能会持有多个ViewModel
,我们更应该让这个不可见的HolderFragment
持有一个ViewModel
的数组(或者Map)——我们干脆封装一个叫ViewModelStore
的容器对象,用来承载和代理所有ViewModel
的管理:
public class ViewModelStore {
private final HashMap<String, ViewModel> mMap = new HashMap<>();
// put(), get(), clear()…
}
public class HolderFragment extends Fragment {
public HolderFragment() { setRetainInstance(true); }
private ViewModelStore mViewModelStore = new ViewModelStore();
}
好了,接下来需要做的就是,在实例化ViewModel
的时候:
1.当前Activity
如果没有持有HolderFragment
,就实例化并持有一个HolderFragment
2.Activity
获取到HolderFragment
,并让HolderFragment
将ViewModel
存进HashMap
中。
这样,具有生命周期的Activity
在旋转屏幕销毁重建时,因为不可见的HolderFragment
中的ViewModelStore
容器持有了ViewModel
,ViewModel
和其内部的状态并没有被回收销毁。
这需要一个条件,在实例化ViewModel
的时候,我们似乎还需要一个Activity
的引用,这样才能保证 获取或者实例化内部的HolderFragment
并将ViewModel
进行存储。
学习路线+知识梳理
花了很长时间,就为了整理这张详细的知识路线脑图。当然由于时间有限、能力也都有限,毕竟嵌入式全体系实在太庞大了,包括我那做嵌入式的同学,也不可能什么都懂,有些东西可能没覆盖到,不足之处,还希望小伙伴们一起交流补充,一起完善进步。
这次就分享到这里吧,下篇见。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!