原文作者: Jose Alcérreca
原文地址: ViewModels and LiveData: Patterns + AntiPatterns
译者:秉心说
View 和 ViewModel
分配责任
理想情况下,ViewModel 应该对 Android 世界一无所知。这提升了可测试性,内存泄漏安全性,并且便于模块化。
通常的做法是保证你的 ViewModel 中没有导入任何 android.*
,android.arch.*
(译者注:现在应该再加一个 androidx.lifecycle
)除外。
这对 Presenter(MVP) 来说也一样。
❌ 不要让 ViewModel 和 Presenter 接触到 Android 框架中的类
条件语句,循环和通用逻辑应该放在应用的 ViewModel 或者其它层来执行,而不是在 Activity 和 Fragment 中。
View 通常是不进行单元测试的,除非你使用了 Robolectric,所以其中的代码越少越好。
View 只需要知道如何展示数据以及向 ViewModel/Presenter 发送用户事件。这叫做 Passive View 模式。
✅ 让 Activity/Fragment 中的逻辑尽量精简
ViewModel 中的 View 引用
ViewModel 和 Activity/Fragment
具有不同的作用域。当 Viewmodel 进入 alive 状态且在运行时,activity 可能位于 生命周期状态 的任何状态。
Activitie 和 Fragment 可以在 ViewModel 无感知的情况下被销毁和重新创建。
向 ViewModel 传递 View(Activity/Fragment) 的引用是一个很大的冒险。假设 ViewModel 请求网络,稍后返回数据。
若此时 View 的引用已经被销毁,或者已经成为一个不可见的 Activity。这将导致内存泄漏,甚至 crash。
❌ 避免在 ViewModel 中持有 View 的引用
在 ViewModel 和 View 中通信的建议方式是观察者模式,使用 LiveData 或者其他类库中的可观察对象。
观察者模式
在 Android 中设计表示层的一种非常方便的方法是让 View 观察和订阅 ViewModel(中的变化)。
由于 ViewModel 并不知道 Android 的任何东西,所以它也不知道 Android 是如何频繁的杀死 View 的。
这有如下好处:
- ViewModel 在配置变化时保持不变,所以当设备旋转时不需要再重新请求资源(数据库或者网络)。
- 当耗时任务执行结束,ViewModel 中的可观察数据更新了。这个数据是否被观察并不重要,尝试更新一个
不存在的 View 并不会导致空指针异常。 - ViewModel 不持有 View 的引用,降低了内存泄漏的风险。
private void subscribeToModel() {
// Observe product data
viewModel.getObservableProduct().observe(this, new Observer<Product>() {
@Override
public void onChanged(@Nullable Product product) {
mTitle.setText(product.title);
}
});
}
✅ 让 UI 观察数据的变化,而不是把数据推送给 UI
胖 ViewModel
无论是什么让你选择分层,这总是一个好主意。如果你的 ViewModel 拥有大量的代码,承担了过多的责任,那么:
- 移除一部分逻辑到和 ViewModel 具有同样作用域的地方。这部分将和应用的其他部分进行通信并更新
ViewModel 持有的 LiveData。 - 采用 Clean Architecture,添加一个 domain 层。这是一个可测试,易维护的架构。Architecture Blueprints 中有 Clean Architecture 的示例。
✅ 分发责任,如果需要的话,添加 domain 层
使用数据仓库
如 应用架构指南 中所说,大部分 App 有多个数据源:
- 远程:网络或者云端
- 本地:数据库或者文件
- 内存缓存
在你的应用中拥有一个数据层是一个好主意,它和你的视图层完全隔离。保持缓存和数据库与网络同步的算法并不简单。建议使用单独的 Repository 类作为处理这种复杂性的单一入口点.
如果你有多个不同的数据模型,考虑使用多个 Repository 仓库。
✅ 添加数据仓库作为你的数据的单一入口点。
处理数据状态
考虑下面这个场景:你正在观察 ViewModel 暴露出来的一个 LiveData,它包含了需要显示的列表项。那么 View 如何区分数据已经加载,网络错误和空集合?
-
你可以通过 ViewModel 暴露出一个
LiveData<MyDataState>
,MyDataState
可以包含数据正在加载,已经加载完成,发生错误等信息。 -
你可以将数据包装在具有状态和其他元数据(如错误消息)的类中。查看示例中的 Resource 类。
✅ 使用包装类或者另一个 LiveData 来暴露数据的状态信息
保存 activity 状态
当 activity 被销毁或者进程被杀导致 activity 不可见时,重新创建屏幕所需要的信息被称为 activity 状态。屏幕旋转就是最明显的例子,如果状态保存在 ViewModel 中,它就是安全的。
但是,你可能需要在 ViewModel 也不存在的情况下恢复状态,例如当操作系统由于资源紧张杀掉你的进程时。
为了有效的保存和恢复 UI 状态,使用 onSaveInstanceState()
和 ViewModel 组合。
Event
Event 指只发生一次的事件。ViewModel 暴露出的是数据,那么 Event 呢?例如,导航事件或者展示 Snackbar 消息,都是应该只被执行一次的动作。
LiveData 保存和恢复数据,和 Event 的概念并不完全符合。看看具有下面字段的一个 ViewModel:
LiveData<String> snackbarMessage = new MutableLiveData<>();
Activity 开始观察它,当 ViewModel 结束一个操作时需要更新它的值:
snackbarMessage.setValue("Item saved!");
Activity 接收到了值并且显示了 SnackBar。显然就应该是这样的。
但是,如果用户旋转了手机,新的 Activity 被创建并且开始观察。当对 LiveData 的观察开始时,新的 Activity 会立即接收到旧的值,导致消息再次被显示。
与其使用架构组件的库或者扩展来解决这个问题,不如把它当做设计问题来看。我们建议你把事件当做状态的一部分。
把事件设计成状态的一部分。更多细节请阅读 LiveData with SnackBar,Navigation and other events (the SingleLiveEvent case)
ViewModel 的泄露
得益于方便的连接 UI 层和应用的其他层,响应式编程在 Android 中工作的很高效。LiveData 是这个模式的关键组件,你的 Activity 和 Fragment 都会观察 LiveData 实例。
LiveData 如何与其他组件通信取决于你,要注意内存泄露和边界情况。如下图所示,视图层(Presentation Layer)使用观察者模式,数据层(Data Layer)使用回调。
当用户退出应用时,View 不可见了,所以 ViewModel 不需要再被观察。如果数据仓库 Repository 是单例模式并且和应用同作用域,那么直到应用进程被杀死,数据仓库 Repository 才会被销毁。 只有当系统资源不足或者用户手动杀掉应用这才会发生。如果数据仓库 Repository 持有 ViewModel 的回调的引用,那么 ViewModel 将会发生内存泄露。
如果 ViewModel 很轻量,或者保证操作很快就会结束,这种泄露也不是什么大问题。但是,事实并不总是这样。理想情况下,只要没有被 View 观察了,ViewModel 就应该被释放。
你可以选择下面几种方式来达成目的:
- 通过 ViewModel.onCLeared() 通知数据仓库释放 ViewModel 的回调
- 在数据仓库 Repository 中使用 弱引用 ,或者 Event Bu(两者都容易被误用,甚至被认为是有害的)。
- 通过在 View 和 ViewModel 中使用 LiveData 的方式,在数据仓库和 ViewModel 之间进程通信
✅ 考虑边界情况,内存泄露和耗时任务会如何影响架构中的实例。
❌ 不要在 ViewModel 中进行保存状态或者数据相关的核心逻辑。 ViewModel 中的每一次调用都可能是最后一次操作。
数据仓库中的 LiveData
为了避免 ViewModel 泄露和回调地狱,数据仓库应该被这样观察:
当 ViewModel 被清除,或者 View 的生命周期结束,订阅也会被清除:
如果你尝试这种方式的话会遇到一个问题:如果不访问 LifeCycleOwner 对象的话,如果通过 ViewModel 订阅数据仓库?使用 Transformations 可以很方便的解决这个问题。Transformations.switchMap
可以让你根据一个 LiveData 实例的变化创建新的 LiveData。它还允许你通过调用链传递观察者的生命周期信息:
LiveData<Repo> repo = Transformations.switchMap(repoIdLiveData, repoId -> {
if (repoId.isEmpty()) {
return AbsentLiveData.create();
}
return repository.loadRepo(repoId);
}
);
在这个例子中,当触发更新时,这个函数被调用并且结果被分发到下游。如果一个 Activity 观察了 repo
,那么同样的 LifecycleOwner 将被应用在 repository.loadRepo(repoId)
的调用上。
无论什么时候你在 ViewModel 内部需要一个 LifeCycle 对象时,Transformation 都是一个好方案。
继承 LiveData
在 ViewModel 中使用 LiveData 最常用的就是 MutableLiveData
,并且将其作为 LiveData
暴露给外部,以保证对观察者不可变。
如果你需要更多功能,继承 LiveData 会让你知道活跃的观察者。这对你监听位置或者传感器服务很有用。
public class MyLiveData extends LiveData<MyData> {
public MyLiveData(Context context) {
// Initialize service
}
@Override
protected void onActive() {
// Start listening
}
@Override
protected void onInactive() {
// Stop listening
}
}
什么时候不要继承 LiveData
你也可以通过 onActive()
来开启服务加载数据。但是除非你有一个很好的理由来说明你不需要等待 LiveData 被观察。下面这些通用的设计模式:
- 给
ViewModel
添加start()
方法,并尽快调用它。[见 Blueprints example] - 设置一个触发加载的属性 [见 GithubBrowerExample]
你并不需要经常继承 LiveData 。让 Activity 和 Fragment 告诉 ViewModel 什么时候开始加载数据。
分割线
翻译就到这里了,其实这篇文章已经在我的收藏夹里躺了很久了。
最近 Google 重写了 Plaid 应用,用上了一系列最新技术栈, AAC,MVVM, Kotlin,协程 等等。这也是我很喜欢的一套技术栈,之前基于此开源了 Wanandroid 应用 ,详见 真香!Kotlin+MVVM+LiveData+协程 打造 Wanandroid! 。
当时基于对 MVVM 的浅薄理解写了一套自认为是 MVVM 的 MVVM 架构,在阅读一些关于架构的文章,以及 Plaid 源码之后,发现了自己的 MVVM 的一些认知误区。后续会对 Wanandroid 应用进行合理改造,并结合上面译文中提到的知识点作一定的说明。欢迎 Star !