原文作者: 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 的。

这有如下好处:

  1. ViewModel 在配置变化时保持不变,所以当设备旋转时不需要再重新请求资源(数据库或者网络)。
  2. 当耗时任务执行结束,ViewModel 中的可观察数据更新了。这个数据是否被观察并不重要,尝试更新一个

    不存在的 View 并不会导致空指针异常。
  3. 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 有多个数据源:

  1. 远程:网络或者云端
  2. 本地:数据库或者文件
  3. 内存缓存

在你的应用中拥有一个数据层是一个好主意,它和你的视图层完全隔离。保持缓存和数据库与网络同步的算法并不简单。建议使用单独的 Repository 类作为处理这种复杂性的单一入口点.

如果你有多个不同的数据模型,考虑使用多个 Repository 仓库。

✅ 添加数据仓库作为你的数据的单一入口点。

处理数据状态

考虑下面这个场景:你正在观察 ViewModel 暴露出来的一个 LiveData,它包含了需要显示的列表项。那么 View 如何区分数据已经加载,网络错误和空集合?

  • 你可以通过 ViewModel 暴露出一个 LiveData<MyDataState>MyDataState 可以包含数据正在加载,已经加载完成,发生错误等信息。

  • 你可以将数据包装在具有状态和其他元数据(如错误消息)的类中。查看示例中的 Resource 类。

✅ 使用包装类或者另一个 LiveData 来暴露数据的状态信息

保存 activity 状态

当 activity 被销毁或者进程被杀导致 activity 不可见时,重新创建屏幕所需要的信息被称为 activity 状态。屏幕旋转就是最明显的例子,如果状态保存在 ViewModel 中,它就是安全的。

但是,你可能需要在 ViewModel 也不存在的情况下恢复状态,例如当操作系统由于资源紧张杀掉你的进程时。

为了有效的保存和恢复 UI 状态,使用 onSaveInstanceState() 和 ViewModel 组合。

详见:ViewModels: Persistence, onSaveInstanceState(), Restoring UI

State and Loaders

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 被观察。下面这些通用的设计模式:

你并不需要经常继承 LiveData 。让 Activity 和 Fragment 告诉 ViewModel 什么时候开始加载数据。

分割线

翻译就到这里了,其实这篇文章已经在我的收藏夹里躺了很久了。

最近 Google 重写了 Plaid 应用,用上了一系列最新技术栈, AAC,MVVM, Kotlin,协程 等等。这也是我很喜欢的一套技术栈,之前基于此开源了 Wanandroid 应用 ,详见 真香!Kotlin+MVVM+LiveData+协程 打造 Wanandroid!

当时基于对 MVVM 的浅薄理解写了一套自认为是 MVVM 的 MVVM 架构,在阅读一些关于架构的文章,以及 Plaid 源码之后,发现了自己的 MVVM 的一些认知误区。后续会对 Wanandroid 应用进行合理改造,并结合上面译文中提到的知识点作一定的说明。欢迎 Star !

文章首发微信公众号: 秉心说 , 专注 Java 、 Android 原创知识分享,LeetCode 题解。

更多最新原创文章,扫码关注我吧!

【Medium 万赞好文】ViewModel 和 LIveData:模式 + 反模式的更多相关文章

  1. ViewModel和LiveData问题思考与解答

    嗨,大家好,面试真题系列又来了,今天我们说说MVVM架构里的两大组件:ViewModel和LiveData. 还是老样子,提出问题,做出解答. ViewModel 是什么? ViewModel 为什么 ...

  2. Jetpack架构组件学习(2)——ViewModel和Livedata使用

    要看本系列其他文章,可访问此链接Jetpack架构学习 | Stars-One的杂货小窝 原文地址:Jetpack架构组件学习(2)--ViewModel和Livedata使用 | Stars-One ...

  3. 转 Android Lifecycle、ViewModel和LiveData

    转自:https://www.jianshu.com/p/982545e01d0a 1.概述 在I / O '17的时候,其中一个重要的主题是Architecture Components.这是一个官 ...

  4. Jetpack的ViewModel与LiveData

    本文基于SDK 29 一.ViewModel与LiveData的作用: 1.viewModel: 数据共享,屏幕旋转不丢失数据,并且在Activity与Fragment之间共享数据. 2.LiveDa ...

  5. Medium高赞系列,如何正确的在Stack Overflow提问

    在我们写程序的时候,经常会遇到各色各样的问题,在国内,小伙伴们经常去知乎.CSDN.博客园.思否.安卓巴士等地方提问并获得答案. 这些地方汇集了很多优秀的.爱分享的国内资源.小编比较自豪的一件事情就是 ...

  6. ViewModel、LiveData、DataBinding

    ViewModel ViewModel的引入 如果系统销毁或重新创建界面控制器,则存储在其中的任何临时性界面相关数据都会丢失.例如,应用的某个 Activity 中可能包含用户列表.因配置更改而重新创 ...

  7. IE文档版本和文档流模式

    使用X-UA-Compatible来设置IE浏览器兼容模式 文件兼容性用于定义让IE如何编译你的网页.此文件解释文件兼容性,如何指定你网站的文件兼容性模式以及如何判断一个网页该使用的文件模式. < ...

  8. Cassandra1.2文档学习(16)—— 模式的变化

    参考文档:http://www.datastax.com/documentation/cassandra/1.2/webhelp/index.html#cassandra/dml/dml_schema ...

  9. 【Ansible 文档】【译文】模式

    Patterns 模式 Ansible中的模式是指我们如何决定那些机器执行管理操作.这里意味着与那些主机通信,但是对于playbook,它是指哪些主机应用特定的配置或执行特定程序. 我们将重温一下In ...

随机推荐

  1. MySQL二进制日志分析-概述篇

    MySQL从3.23版本开始引入了二进制日志,用于的数据复制, 二进制日志根据MySQL的版本不同,目前有4个版本: https://dev.mysql.com/doc/internals/en/bi ...

  2. FreeSql (二十九)Lambda 表达式

    FreeSql 支持功能丰富的表达式函数解析,方便程序员在不了解数据库函数的情况下编写代码.这是 FreeSql 非常特色的功能之一,深入细化函数解析尽量做到满意,所支持的类型基本都可以使用对应的表达 ...

  3. 多线程编程学习十一(ThreadPoolExecutor 详解).

    一.ThreadPoolExecutor 参数说明 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keep ...

  4. NLP舞动之中文分词浅析(一)

    一.简介        针对现有中文分词在垂直领域应用时,存在准确率不高的问题,本文对其进行了简要分析,对中文分词面临的分词歧义及未登录词等难点进行了介绍,最后对当前中文分词实现的算法原理(基于词表. ...

  5. prometheus告警模块alertmanager注意事项(QQ邮箱发送告警)

    配置alertmanager的时候,都是根据网上的教程来配置的. 因为我是用QQ邮箱来发送告警的,所以alertmanager.yml的邮箱配置如下: global: resolve_timeout: ...

  6. Spring——面向切面编程(AOP)详解

    声明:本博客仅仅是一个初学者的学习记录.心得总结,其中肯定有许多错误,不具有参考价值,欢迎大佬指正,谢谢!想和我交流.一起学习.一起进步的朋友可以加我微信Liu__66666666 这是简单学习一遍之 ...

  7. .net core 发布单个exe 文件, 并优化缩小大小

    最新版的.net core 3.0 可以通过命令行发布为exe文件, 操作步骤如下: 在项目目录下打开控制台: 输入命令: dotnet publish -r win-x64 -c Release - ...

  8. web-文件上传漏洞总结

    思维导图: 一,js验证绕过 1.我们直接删除代码中onsubmit事件中关于文件上传时验证上传文件的相关代码即可. 或者可以不加载所有js,还可以将html源码copy一份到本地,然后对相应代码进行 ...

  9. 基于SpringBoot实现AOP+jdk/CGlib动态代理详解

    动态代理是一种设计模式.在Spring中,有俩种方式可以实现动态代理--JDK动态代理和CGLIB动态代理. JDK动态代理 首先定义一个人的接口: public interface Person { ...

  10. 给body设置高度

    今天做一个小demo,需要给body设置高度为100%,然鹅发现只设置body的height不可行,需,html,body{height:100%;}为何呢?一个容器的height是从上一级继承而来的 ...