原文链接 ViewModels and LiveData: Patterns + AntiPatterns。
分配职责
使用 Architecture Components 构建的 app 中实体的典型交互
理想情况下,ViewModels 不应该知道任何关于 Android 的事情。这提高了可测试性、泄漏安全性和模块性。一般的经验法则是确保在您的 ViewModels 中没有 android.*
imports(android.arch.*
除外)。这同样适用于 presenters。
❌ 不要让 ViewModels(和 Presenters)知道 Android 框架类
条件语句,循环和一般决策应在 ViewModels 或 app 的其他层中完成,而不是在 Activities 或 Fragments 中完成。View 通常不经过单元测试(除非你使用 Robolectric)所以代码行越少越好。Views 应该只知道如何显示数据并将用户事件发送到 ViewModel(或Presenter)。这称为 Passive View(被动视图)模式。
✅ 将 Activities 和 Fragments 中的逻辑保持在最低限度
ViewModels 中的 View 引用
ViewModels 与 activities 或 fragments 具有不同的作用域。当 ViewModel 处于活动状态并正在运行时,activity 可以处于任何 lifecycle states(生命周期状态)。Activities 和fragments 可以在 ViewModel 不知情的情况下被销毁并重新创建。
ViewModels 在配置更改时仍会留存
将 View(activity 或 fragment)的引用传递给 ViewModel 是一个严重的风险。让我们假设 ViewModel 从网络请求数据,并且数据会在一段时间后返回。此时,View 引用可能会被破坏,或者可能是一个不再可见的旧 activity,从而导致内存泄漏,并可能导致崩溃。
❌ 避免在 ViewModels 中引用 Views。
ViewModels 和 Views 之间通信的推荐方法是使用其他库中的 LiveData 或 observables 的观察者模式。
观察者模式
在 Android 中设计表示层的一个非常方便的方法是让 View(activity 或 fragment)观察(订阅更改来自)ViewModel。因为 ViewModel 不知晓 Android,所以它不知道 Android 是多么喜欢频繁地杀死 Views。这有一些优势:
private void subscribeToModel() {
// 观察 product 数据
viewModel.getObservableProduct().observe(this, new Observer() {
@Override
public void onChanged(@Nullable Product product) {
mTitle.setText(product.title);
}
});
}
来自 activity 或 fragment 的典型订阅。
✅ 不要将数据推送到 UI,而是让 UI 观察数据的更改。
无论什么,只要让你的关注点分离,就是好主意。 如果你的 ViewModel 持有太多代码或承担太多责任,请考虑:
✅ 分配职责,必要时添加 domain(域) 层。
如 应用程序架构指南 所示,大多数 apps 都有多个数据源,例如:
在你的 app 中有一个数据层是一个好主意,它完全不知道您的表示层。保持缓存和数据库与网络同步的算法并不简单。建议将单独的仓库(repository)类作为处理此复杂性的单一入口点。
如果您有多个差异较大的数据模型,请考虑添加多个仓库。
✅ 添加数据仓库作为你的数据的单一入口点
请考虑以下情况:您正在观察由 ViewModel 公开的 LiveData,该 ViewModel 包含要显示的条目列表。View 如何在正在加载数据,网络错误和空列表之间进行区分?
LiveData
。 例如,MyDataState
可以包含有关数据当前是否正在加载,是否已成功加载或失败的信息。
您可以将数据包装在一个具有状态和其他元数据(如错误消息)的类中。参见示例中的 Resource 类。
✅ 使用包装器或其他 LiveData 公开有关数据状态的信息。
Activity 状态是 activity 消失后(意味着 activity 已被销毁或进程被终止)重新创建屏幕所需的信息。旋转是最明显的情况,我们已经用 ViewModels 覆盖了它。如果状态保存在 ViewModel 中,则状态是安全的。
但是,您可能需要在 ViewModels 也消失的其他场景中恢复状态:例如,当操作系统资源不足并导致您的进程终止时。
要有效地保存和恢复 UI 状态,请使用 onSaveInstanceState()
和 ViewModels 的持久化组合。
有关示例,请参阅:ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders
一次事件只发生一次。ViewModels 公开数据,但事件呢? 例如,导航事件或显示 Snackbar 消息是应该只执行一次的操作。
事件(Event)的概念与 LiveData 存储和恢复数据的方式不完全吻合。考虑具有以下字段的 ViewModel:
LiveData snackbarMessage = new MutableLiveData<>();
activity 开始观察此操作,ViewModel 完成操作,因此需要更新消息:
snackbarMessage.setValue("Item saved!");
activity 接收该值并显示 Snackbar。很明显,它起作用了。
但是,如果用户旋转手机,则会创建新的 activity 并开始观察。当 LiveData 观察开始时,activity 立即收到旧值,这将导致消息再次显示!
不要试图通过库或对 Architecture Components 的扩展来解决这一问题,而是将其作为设计问题来面对。我们建议您将事件视为状态的一部分。
✅ 将事件设计为你的状态的一部分。有关更多详细信息,请阅读 LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)。
反应式范式在 Android 中运行良好,因为它允许 UI 与 app 的其他层之间的便捷连接。LiveData 是此结构的关键组件,因此通常您的 activities 和 fragments 将观察 LiveData 实例。
ViewModels 如何与其他组件通信取决于您,但要注意泄漏和边缘情况。考虑这个图表,其中 Presentation 层使用观察者模式,Data 层使用回调:
UI 中的观察者模式和数据层中的回调
如果用户退出 app,View 将消失,因此 ViewModel 也将不再被观察。如果仓库是单例的或是以其他方式被限定到应用程序范围内的,则仓库在进程终止之前不会被销毁。这只会在系统需要资源或用户手动杀死 app 时发生。 如果仓库持有对 ViewModel 中回调的引用,则 ViewModel 将暂时泄露
activity 已经 finished,但 ViewModel 仍然存在
如果 ViewModel 很轻量或者保证操作很快完成,那么这个泄漏就不是什么大问题。然而,情况并非总是如此。理想情况下,当 ViewModels 没有任何观察它们的 Views 时,它们应该可以自由行动:
您有许多选择来实现这一点:
✅ 考虑边缘情况,泄漏以及长时间运行的操作如何影响架构中的实例。
❌ 不要在 ViewModel 中放置对保存干净状态或与数据相关的关键逻辑。您从 ViewModel 进行的任何调用都可能是最后一个。
为了避免泄漏 ViewModels 和回调地狱,可以像这样观察仓库:
ViewModel 被清除或 view 的生命周期被 finished 后,也将订阅清除:
如果您尝试这种方法,有一个问题:如果您无法访问 LifecycleOwner,您如何从 ViewModel 订阅 Repository ? 使用 Transformations 是解决此问题的一种非常方便的方法。Transformations.switchMap
允许您创建一个新的 LiveData,以响应其他 LiveData 实例的更改。它还允许在整个链中携带观察者生命周期信息:
LiveData repo = Transformations.switchMap(repoIdLiveData, repoId -> {
if (repoId.isEmpty()) {
return AbsentLiveData.create();
}
return repository.loadRepo(repoId);
}
);
Transformations 示例[资源]
在此示例中,当触发器获得更新时,将应用该函数并在下游调度结果。一个 activity 将观察 repo
,并且相同的 LifecycleOwner 将用于repository.loadRepo(id)
调用。
✅ 每当您认为在 ViewModel 中需要 Lifecycle 对象时, Transformation 可能就是解决方案。
LiveData 最常见的用例是在 ViewModels 中使用 MutableLiveData
并将它们作为 LiveData
公开,以使它们从观察者中不可变。
如果您需要更多功能,继承的 LiveData 会在有活跃的观察者时通知您。例如,当您想要开始监听位置或传感器服务时,这非常有用。
public class MyLiveData extends LiveData {
public MyLiveData(Context context) {
// 初始化服务
}
@Override
protected void onActive() {
// 开始监听
}
@Override
protected void onInactive() {
// 停止监听
}
}
什么时候不继承 LiveData
您还可以使用 onActive()
来启动一些加载数据的服务,但除非您有充分的理由,否则您无需等待 LiveData 被观察。一些常见的模式:
start()
方法添加到 ViewModel 并尽快调用它[参见 Blueprints example]❌ 您通常不会继承 LiveData。 让您的活动或片段告诉 ViewModel 什么时候开始加载数据。