(译) 视图模型和 LiveData :模式和反模式

原文链接

视图和视图模型

分配职责

使用架构构建的应用程序中实体的典型交互

理想情况下,视图模型不应该对 Android 有任何了解。 这提高了可测试性,泄漏安全性和模块化。 一般的经验法则是确保 ViewMode l中没有 android.* 导入(例如androidx.*例外)。这同样适用于 presenter 。

❌ 不要让视图模型(和 presenter ) 知道关于 Android 框架类的事

条件语句,循环和一般决策应在视图模型或应用程序的其它层中完成,而不是在 “Activity” 或 “Fragment” 中完成。 View 通常不经过单元测试(除非你使用 Robolectric )所以代码行越少越好。 视图应该只知道如何显示数据并将用户事件发送到视图模型(或Presenter)。 这称为被动视图模式。

✅ 将 Activity 和 Fragment 中的逻辑保持在最低限度

在视图模型中引用视图

ViewModel 具有与 Activity 和 fragment 不同的范围。 当 ViewModel 处于活动状态并且正在运行时,Activity 可以处于其任何生命周期状态。 在 ViewModel 不知道的情况下,可以销毁并再次创建 Activity 和 Fragment。

将 View( Activity 或 Fragment )的引用传递给 ViewModel 是一个严重的风险。 让我们假设 ViewModel 从网络请求数据,并且数据会在一段时间后返回。 此时,View引用可能会被破坏,或者可能是一个不再可见的旧 Activity,从而导致内存泄漏,并可能导致崩溃。

❌ 避免在视图模型中引用视图

在视图模型和视图之间进行通信的推荐方法,是使用的 LiveData 或其它库中可观察对象的观察者模式。

观察者模式

在 Android 中设计表示层的一种非常方便的方法是让视图( Activity 或 Fragment )观察视图模型。由于视图模型不了解 Android,因此不知道 Android 是如何经常杀死视图的。 这有一些优点:

  1. ViewModel 在配置变化的时候是存留着的,因此在屏幕转向发生时无需重新查询外部数据源(如数据库或网络)。
  2. 长时间运行的操作完成后,ViewModel 中的观察对象会更新。 是否观察数据无关紧要。 尝试更新不存在的视图时,不会发生空指针异常。
  3. ViewModel 不引用视图,因此内存泄漏的风险较小。
1
2
3
4
5
6
7
8
9
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 持有太多代码或承担太多责任,请考虑:

  • 将一些逻辑移出到 presenter,其范围与 ViewModel 相同。 它将与你应用的其他部分进行通信,并在 ViewModel 中更新 LiveData 持有者。

  • 添加一个 Domain 层并采用整洁架构,形成了一个非常可测试和可维护的架构。 它还有助于快速脱离主线程。架构蓝图中有一个 Clean Architecture 示例。

✅ 分配职责,必要的话添加 Domain 层。

使用数据存储库

应用程序架构指南中所见,大多数应用程序都有多个数据源,例如:

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

在你的应用中拥有一个完全不知道你的表示层的数据层是一个好主意。 保持缓存和数据库与网络同步的算法并非易事。 建议将单独的存储库类作为单一入口来处理这种复杂性。

如果你有多个非常不同的数据模型,可以考虑添加多个存储库。

数据状态处理

请考虑这种情况:你正在观察由 ViewModel 公开的 LiveData,该 ViewModel 包含要显示的条目列表。视图如何区分加载的数据,网络错误和空列表?

你可以从 ViewModel 公开 LiveData。 例如,MyDataState 可以包含有关数据当前是否正在加载,是否已成功加载或失败的信息。

你可以将数据包装在具有状态的类和其他元数据(如错误消息)中。 请参阅我们的示例中的 Resource 类。

✅ 使用包装器或其他 LiveData 公开有关数据状态的信息。

保存 activity 状态

Activity 状态是 Activity 消失后重新创建屏幕所需的信息,意味着活动已被破坏或进程被终止。 屏幕转向是最明显的情况,我们已经用 ViewModel 覆盖了它。 如果状态保存在ViewModel 中,则状态是安全的。

但是,你可能需要在 ViewModel 也消失的其它情况下恢复状态:例如,当操作系统资源不足并导致你的进程终止时。

要有效地保存和恢复UI状态,请使用持久化,onSaveInstanceState()和 ViewModel 的组合。

有关示例,请参阅:ViewModels:Persistence,onSaveInstanceState(),还原 UI状态和加载器

事件

事件发生一次。 ViewModel 公开数据,但事件呢? 例如,导航事件或显示 Snackbar 消息是应该只执行一次的操作。

事件的概念与LiveData存储和恢复数据的方式不完全吻合。 考虑具有以下字段的ViewModel:

1
LiveData<String> snackbarMessage = new MutableLiveData<>();

活动开始观察此操作,ViewModel 完成操作,因此需要更新消息:

1
snackbarMessage.setValue("Item saved!");

活动接收值并显示Snackbar。 它显然有效。

但是,如果用户旋转手机,则会创建新活动并开始观察。 当LiveData观察开始时,活动会立即收到旧值,这会导致消息再次显示!

不要试图用架构组件的库或扩展来解决这个问题,这应该是个设计问题。 我们建议你将事件视为你状态的一部分

✅ 将事件设计为你的状态的一部分。有关更多详细信息,请看 LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case).

泄露视图模型

响应范式在 Android 中运行良好,因为它允许 UI 与应用程序的其他层之间的便捷连接。 LiveData 是此结构的关键组件,因此通常你的 Activity 和 Fragment 将观察 LiveData 实例。

ViewModel 如何与其他组件通信取决于你,但要注意泄漏和边缘情况。考虑下这个图表,其中表示层使用观察者模式,数据层使用回调:

如果用户退出应用程序,View 将消失,因此不再观察 ViewModel。 如果存储库是单例或以其他方式限定到应用程序,则在该进程被终止之前存储库都不会被销毁。 这只会在系统需要资源或用户手动杀死应用程序时发生。如果存储库在 ViewModel 中持有对回调的引用,则 ViewModel 将暂时泄露

如果 ViewModel 很轻或者保证快速完成操作,这种泄漏并不是什么大问题。 然而,这并非总是如此。 理想情况下,只要 ViewModel 没有任何观察它们的视图,它们就应该被释放:

你有很多选择来实现这个目标:

  • 使用 ViewModel.onCleared(),你可以告诉存储库将回调放到 ViewModel。
  • 在存储库中,你可以使用 WeakReference,也可以使用事件总线(两者都容易被滥用然后甚至是有害的)。
  • 使用 LiveData 以与在 View 和 ViewModel 之间使用 LiveData 类似的方式在Repository 和 ViewModel 之间进行通信。

✅ 考虑下边缘情况,泄漏以及长时间运行的操作是如何影响架构中的实例。
❌ 不要在 ViewModel 中放置对保存干净状态或与数据相关的关键逻辑。 你从 ViewModel进行的任何调用都可以是最后一个。

存储库中的 LivaData

为了避免泄漏 ViewModel 和回调地狱,可以像这样观察存储库:

清除 ViewModel 或视图的生命周期完成后,订阅将被清除:

如果你尝试这种方法,有一个问题:如果你无法访问 LifecycleOwner,你如何从 ViewModel 订阅存储库?使用转换是解决此问题的一种非常方便的方法。 Transformations.switchMap 允许你创建一个新的 LiveData,以响应其他 LiveData 实例的更改。 它还允许在整个链中携带观察者生命周期信息:

1
2
3
4
5
6
7
LiveData<Repo> repo = Transformations.switchMap(repoIdLiveData, repoId -> {
if (repoId.isEmpty()) {
return AbsentLiveData.create();
}
return repository.loadRepo(repoId);
}
);

在此示例中,当触发器获得更新时,将应用该函数并在下游调度结果。 一个活动将观察repo,同样的 LifecycleOwner 将用于 repository.loadRepo(id) 调用。

✅ 只要你认为在 ViewModel 中需要 Lifecycle 对象,转换就可能是解决方案。

扩展 LivaData

LiveData 最常见的用例是在 ViewModel 中使用 MutableLiveData 并将它们作为LiveData 公开,以使它们从观察者中不可变。

如果你需要更多功能,扩展 LiveData 会在有活动观察者时通知你。 例如,当你想要开始监听位置或传感器服务时,这非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 变得可观察。 一些常见的模式:

将一个 start() 方法添加到 ViewModel 并尽快调用它[参见蓝图示例] 设置一个启动记载的属性[请参阅GithubBrowserExample]。

❌ 你通常不会扩展 LiveData。 让你的 Activity 或 Fragment 告诉 ViewModel 什么时候开始加载数据。

非典型前端coder wechat
想要随时Follow我的最新博客,可扫码关注我的公众号