ViewModels 和 LiveData:模式 +反模式

原文链接 ViewModels and LiveData: Patterns + AntiPatterns

Views 和 ViewModels

分配职责

 

使用 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。这有一些优势:

  1. ViewModels 在配置更改时保持不变,因此在旋转发生时无需重新查询外部数据源(如数据库或网络)。
  2. 当长时间运行的操作完成时,ViewModel 中的可观察对象将被更新。数据是否被观察并不重要。尝试更新不存在的 View 时不发生空指针异常。
  3. ViewModels 不引用视图,因此内存泄漏的风险较小。
private void subscribeToModel() {
  // 观察 product 数据
  viewModel.getObservableProduct().observe(this, new Observer<Product>() {
      @Override
      public void onChanged(@Nullable Product product) {
        mTitle.setText(product.title);
      }
  });
}

来自 activity 或 fragment 的典型订阅。

✅ 不要将数据推送到 UI,而是让 UI 观察数据的更改。

臃肿的 ViewModels

无论什么,只要让你的关注点分离,就是好主意。 如果你的 ViewModel 持有太多代码或承担太多责任,请考虑:

  • 将一些逻辑移到与 ViewModel 具有相同作用域的 presenter。它将与 app 的其他部分进行通信,并更新 ViewModel 中的 LiveData 持有者。
  • 添加一个Domain 层,并采用 Clean Architecture。这导致了一个非常可测试和可维护的架构。它也有助于快速脱离主线程。 Architecture Blueprints 中有一个 Clean Architecture 示例。
✅ 分配职责,必要时添加 domain(域) 层。

使用数据仓库

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

  1. Remote(远端): 网络或云
  2. Local(本地): 数据库或文件
  3. 内存缓存

在你的 app 中有一个数据层是一个好主意,它完全不知道您的表示层。保持缓存和数据库与网络同步的算法并不简单。建议将单独的仓库(repository)类作为处理此复杂性的单一入口点。

如果您有多个差异较大的数据模型,请考虑添加多个仓库。

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

处理数据状态

请考虑以下情况:您正在观察由 ViewModel 公开的 LiveData,该 ViewModel 包含要显示的条目列表。View 如何在正在加载数据,网络错误和空列表之间进行区分?

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

 

您可以将数据包装在一个具有状态和其他元数据(如错误消息)的类中。参见示例中的 Resource 类。

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

保存 activity 状态

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

但是,您可能需要在 ViewModels 也消失的其他场景中恢复状态:例如,当操作系统资源不足并导致您的进程终止时。

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

有关示例,请参阅:ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders

事件

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

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

LiveData<String> 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)

泄漏的 ViewModels

反应式范式在 Android 中运行良好,因为它允许 UI 与 app 的其他层之间的便捷连接。LiveData 是此结构的关键组件,因此通常您的 activities 和 fragments 将观察 LiveData 实例。

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

 

UI 中的观察者模式和数据层中的回调

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

 

activity 已经 finished,但 ViewModel 仍然存在

如果 ViewModel 很轻量或者保证操作很快完成,那么这个泄漏就不是什么大问题。然而,情况并非总是如此。理想情况下,当 ViewModels 没有任何观察它们的 Views 时,它们应该可以自由行动:

 

您有许多选择来实现这一点:

  • 使用ViewModel.onCleared() 可以告诉仓库删除对 ViewModel 的回调。
  • 在仓库中,您可以使用 WeakReference ,也可以使用 Event Bus(两者都容易滥用甚至被视为有害)。
  • 以在 View 和 ViewModel 之间使用 LiveData 的相似方式,在 Repository 和 ViewModel 之间使用  LiveData 通信。
✅ 考虑边缘情况,泄漏以及长时间运行的操作如何影响架构中的实例。
❌ 不要在 ViewModel 中放置对保存干净状态或与数据相关的关键逻辑。您从 ViewModel 进行的任何调用都可能是最后一个。

仓库中的 LiveData

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

 

ViewModel 被清除或 view 的生命周期被 finished 后,也将订阅清除:

 

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

LiveData<Repo> 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

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

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

public class MyLiveData extends LiveData<MyData> {

    public MyLiveData(Context context) {
        // 初始化服务
    }

    @Override
    protected void onActive() {
        // 开始监听
    }

    @Override
    protected void onInactive() {
        // 停止监听
    }
}

什么时候不继承 LiveData

您还可以使用 onActive() 来启动一些加载数据的服务,但除非您有充分的理由,否则您无需等待 LiveData 被观察。一些常见的模式:

❌ 您通常不会继承 LiveData。 让您的活动或片段告诉 ViewModel 什么时候开始加载数据。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值