Views and ViewModels
职责分离
理想情况下,ViewModels 不应该知道 Android 平台的任何信息。这可以提高 ViewModel 的可测试性、泄漏安全性和模块性。一般的经验法则是,确保在你的ViewModel 中没有导入 android.*
包(除了 android.arch.*
)。Presenter 也应是如此。
❌ 不要让 ViewModel (和 Presenter )知道 Android 框架类的情况
条件语句、循环和一般决策应该由 ViewModel 或者 app 的其他层而非 Activity 或 Framework 负责完成。 视图通常没有单元测试,所以里面的代码越少越好。视图应该只知道如何显示数据并将用户事件发送给 ViewModel(或Presenter)。这就是所谓的被动视图模式。
✅ 将 Activity 和 Fragment 的逻辑保持在最低限度
ViewModel 中的视图引用
ViewModel 的作用域与 activity 和 fragment 不同。当 一个 ViewModel 处于活动状态并运行时,一个 activity 可以处于其生命周期的任何状态。activity 和 fragment 可以被销毁并再次创建,而 ViewModel 对此一无所知。
将视图(activity 或 fragment)的引用传给 ViewModel 有很大的风险。我们假设 ViewModel 从网络请求数据,数据在一段时间后异步返回。这时,视图引用可能已经被销毁或者 是一个不再可见的旧 activity,就会产生内存泄漏甚至可能导致崩溃。
❌ 避免在 ViewModel 中持有视图的引用
推荐使用观察者模式在 ViewModel 和 View 之间进行通信,可以使用LiveData或其他库提供的可观察变量实现。
观察者模式
在Android中,让View(Activity或Fragment)观察/订阅 ViewModel的变化是设计表现层的一个非常方便的方法。由于ViewModel 对 Android 无感知,所以它不知道 View 会被频繁 kill 。这有一些优点:
- ViewModel 在配置变化时被持久化,所以当配置变更(如旋转)时,不需要重新查询外部数据源(如数据库或网络)。
- 当长时间运行的操作结束时,ViewModel 中的可观察变量会被更新。数据是否正在被观察并不重要。当试图更新不存在的视图时,也不会发生空指针异常。
- ViewModel 不引用视图,所以内存泄漏的风险很小。
// 典型的在 activity 或 fragment 中订阅
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 相同作用域的 presenter 中。由它负责与app 其他部分通信,并更新 ViewModel 中的 LiveData。
- 添加一个网域层(Domain layer)并采用 Clean 架构。这将是一个可测试和可维护性很高的架构。它也有利于快速摆脱主线程的束缚。在 架构蓝图 中有一个Clean 架构的例子。
✅ 职责分离。必要时添加网域层(domain layer)。这是谷歌官方对网域层的介绍链接
使用数据仓库
正如在《应用程序架构指南》中看到的那样,大多数应用程序都有多个数据源,比如:
- 远程:网络或云
- 本地:数据库或文件
- 内存缓存
在你的应用程序中设计一个数据层是个好主意,数据层对表现层完全无感知。让缓存和数据库与网络保持同步的算法并不简单。建议有一个单独的数据仓库类作为单一入口,仓库内部处理这种复杂性。
如果你有多个差别很大的数据模型,可以考虑添加多个数据仓库。
✅ 添加一个数据仓库作为数据访问的唯一入口
处理数据状态
考虑这个场景:你正在观察一个由 ViewModel 暴露的 LiveData ,它包含一个要显示的数据列表。视图如何区分数据是正在加载的数据、网络错误还是一个空列表?
- 你可以从 ViewModel 中暴露出一个 LiveData< MyDataState >。例如,MyDataState 包含数据是否正在加载、是否已经加载成功或失败的信息。
你可以把数据包装在一个带状态和其他元数据(如错误信息)的类中。
✅ 使用包装类或另一个 LiveData 暴露数据的状态信息
保存 activity 的状态
如果一个 Activity 消失了(被销毁或进程被杀死),重新创建屏幕时就需要这个 Activity 的状态信息。最常见的情况是旋转屏幕,ViewModel 覆盖了这种情况。所以,被在ViewModel中保存状态是安全的。
但是,你可能需要在 ViewModel 也消失的其他情况下恢复 activity 的状态:比如,当操作系统因资源不足杀死了activity所在进程时。
为了有效地保存和恢复UI状态,可以结合使用持久化、onSaveInstanceState()
和 ViewModel。
事件
事件是只发生一次的事情。ViewModel 暴露了数据,那事件怎么处理?例如,导航事件或显示 Snackbar 信息都是只应执行一次的动作。
事件的概念与 LiveData 存储和恢复数据的方式并不完全相符,可以考虑在 ViewModel 中定义以下字段 :
LiveData<String> snackbarMessage = new MutableLiveData<>();
一个 Activity 开始观察这个 LiveData,ViewModel 完成操作后,更新消息:
snackbarMessage.setValue("Item saved!");
该 Activity 接收该值并显示 Snackbar。这显然是有效的。
然而,如果用户旋转手机,新的Activity被创建并开始观察该 LiveData。当观察开始时,该Activity会立即收到旧值,这将导致消息再次显示出来!
与其试图用库或架构组件的扩展来解决这个问题,不如将其作为一个设计问题来对待。建议把事件作为状态的一部分。
✅将事件设计成状态的一部分。更多细节请阅读《LiveData与SnackBar、Navigation和其他事件(SingleLiveEvent案例)》或者 google 官方《界面事件指南》
ViewModel 泄露
响应式范式在 Android 中运行良好,因为它允许在 UI 和 app 的其他层之间建立一个方便的连接。LiveData 是这个结构中的关键组件,所以通常 Activity 和Fragment 会观察 LiveData 实例。
开发人员决定了 ViewModel 如何与其他组件通信,但要注意内存泄漏和边界情况。比如考虑这个图,视图层使用观察者模式,数据层使用回调:
Observer pattern in the UI and callbacks in the data layer
如果用户退出了 app,视图就会消失,ViewModel 不再被观察。如果repository 是个单例或应用程序级的作用域,直到进程被杀死时,repository 才会被销毁。 这只会在系统需要资源或用户手动杀死 app 时发生。如果 repository 持有对 ViewModel中回调的引用,ViewModel 就会被暂时泄露:
The activity is finished but the ViewModel is still around
如果 ViewModel 是轻量级的,或者操作可以保证快速完成,这种泄漏就不是大问题。然而情况并不总是这样的。理想情况下,只要没有任何视图在观察,ViewModel 就应该被释放了。
有很多方法可以实现这一点:
- 在 ViewModel.onCleared() 方法中告诉 repository 放弃对 ViewModel 的回调。
- 在 repository 中,可以使用弱引用( WeakReference) ,也可以使用事件总线(EventBus)(两者都容易被滥用,甚至被认为是有害的)。
- 使用 LiveData 在 repository 和 ViewModel 之间进行通信,其方式类似于在View 和 ViewModel 之间使用 LiveData。
✅考虑边界情况、泄漏以及长时间运行的操作会如何影响你架构中的实例。
❌ 不要在 ViewModel 中存放保存清洁状态或与数据有关的关键逻辑。你从ViewModel 进行的任何调用都可能是最后一次。
数据仓库中的LivaData
为了避免泄露 ViewModel 和回调地狱,可以像这样观察 repositories :
当 ViewModel 被清除或视图的生命周期结束时,订阅也被清除。
如果你尝试使用这种方法,会遇到一个问题:无法访问 LifecycleOwner 时,如何从 ViewModel 订阅 Repository 的 LiveData?使用 Transformations 是解决这个问题的一个便捷方法。Transformations.switchMap
可以创建一个新的LiveData 来对其他 LiveData 实例的变化做出反应。它还允许在整个链条上携带观察者的生命周期信息:
LiveData<Repo> repo = Transformations.switchMap(repoIdLiveData, repoId -> {
if (repoId.isEmpty()) {
return AbsentLiveData.create();
}
return repository.loadRepo(repoId);
}
);
在这个例子中,当触发器 LiveData 更新时,该函数被应用,结果被派发到下游。 Activity 将观察 repo
,其 LifecycleOwner 将被用于repository.loadRepo(id)
调用。
✅ 只要你认为 ViewModel 中需要一个 Lifecycle 对象,一个 Transformation 变换可能就是解决方案
扩展 LivatData
LiveData 最常见的使用例子是在 ViewModel 中使用 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()
方法,并尽早调用它 - 设置一个启动加载的属性
❌ 通常不用扩展 LiveData。让 Activity 或 Fragment 告诉 ViewModel 何时开始加载数据