ViewModel和LiveData:模式 + 反模式

797 阅读7分钟

1、Views和ViewModels

image.png

理想情况下,ViewModel不应该了解有关Android的任何信息。这提高了可测试性、泄露安全性和模块化性。一般的经验法则是确保android.*包,您的ViewModel没有导入(除了像android.arch.*之类的例外)。这同样适用于Presenters。

❌不要让ViewModel(和Presenter)知道Android框架类。

条件语句、循环和一般决策应在ViewModel或应用程序的其他层中完成,而不是在ActivityFragment中完成。View通常未经单元测试(除非您使用Robolectric),因此代码越少越好。View层应该值知道如何显示数据并将用户事件发送到ViewModel(或Presenter)。这称为被动视图模式

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

2、查看ViewModel中的引用

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

image.png

将View(activity或fragment)的引用传递给ViewModel是一个严重的风险。我们假设ViewModel从网络请求数据,并且数据在一段时间后返回。此时,View引用可能会被销毁,或者可能是不再可见的旧activity,从而产生内存泄露,甚至可能导致崩溃。

❌避免在ViewModel中引用视图

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

3、观察者模式

image.png

在Android中设计表示层的一个非常方便的方法是让View(activity或fragment)观察(订阅)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观察数据的变化。

4、臃肿的ViewModel

任何能让你分离关注点的方法都是好主意。如果您的ViewModel包含太多代码或承担太多责任,请考虑:

  • 将一些逻辑移出到Presenter,其范围与ViewModel相同。它将与应用程序的其他部分通信并更新ViewModel中的LiveData持有者。
  • 添加Domain层并采用Clean Architecture。这导致了一个非常可测试和可维护的架构。它还有助于快速脱离主线程,architecture-samples中有一个干净的架构示例。

✅职责分配,如果需要,添加Domain层

5、使用data repository

大多数应用程序都有多个数据源,例如:

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

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

如果您有多个且截然不同的数据模式,请考虑添加多个Repositories.

✅添加data repository作为数据的单一入口

6、处理数据状态

考虑一下场景:您正在观察ViewModel公开的LiveData,其中包含要显示的项目列表。View如何区分正在加载的数据、网络错误和空列表?

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

image.png

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

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

7、保存activity状态

Activity状态时当activity消失时重新创建屏幕所需的信息,这意味着活动被破坏或进程被终止。旋转是最明显的情况,我们已经用ViewModel涵盖了这一点。如果状态保存在ViewModel中,那么它就是安全的。

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

要有效地保存和恢复UI状态,请结合使用持久性onSavedInstanceState()和ViewModel。

8、Event

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

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

LiveData<String> SnackbarMessage = new MutableLiveData<>()

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

SnackbarMessage.setValue("项目已保存!");

该活动接受该值并显示Snackbar。显然,它有效。

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

不应尝试通过库或架构组件扩展来解决此问题,而应将其视为设计问题。我们建议您将您的事件视为你所在状态的一部分。

✅将事件作为您的状态设计。

9、ViewModel泄露

响应式范式在Android中运行良好,因为它允许UI和应用程序的其余层之间建立便捷的连接。LiveData是此结构的关键组成部分,因此通常您的activity和fragment将观察LiveData实例。

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

image.png

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

image.png

如果ViewModel很轻或者操作保证快速完成,那么这种泄露并不是什么大问题。然而,这并非总是如此。理想情况下,只要没有任何View观察它们,ViewModel就应该可以自由运行:

image.png

您有多重选择来实现这一目标:

  • 使用ViewModel.onCleared(),您可以告诉Repository删除对ViewModel的回调。
  • 在Repository中,您可以使用ReakReference,也可以使用EventBus(两者都很容易被误用,甚至被认为是有害的)
  • 使用LiveData在Repository和ViewModel之间进行通信的方式与在View和ViewModel之间使用LiveData的方式类似。

✅考虑边缘情况、泄露以及长时间运行的操作如何影响架构中的实例。

❌不要将对于保存干净状态或与数据相关的关键逻辑放入ViewModel中。您从ViewModel进行的任何调用都可以是最后一次。

10、Repository中的LiveData

为了避免泄露ViewModel和回调地狱,可以这样观察Repository:

image.png

当ViewModel被清除或者View的生命周期结束时,订阅也会被清除:

image.png

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

LiveData<Repo> repo = Transformations.switchMap(repoIdLiveData, repoId -> {
    if (repoId.isEmpty()) {
        return AbsentLiveData.create();
    }
    return repository.loadRepo(repoId);
}
);

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

✅每当您认为ViewModel中需要一个Lifecycle对象时,Transformation可能就是解决方案。

11、扩展LiveData

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

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

public class MyLiveDate extends LiveData<MyData> {
    
    public MyLiveData(Context context) {
        // Initialize service
    }
    
    @Override
    protected void onActive() {
        // Start listening
    }
    
    @Override
    protected void onInactive() {
        // Stop listening
    }
}

12、何时不扩展LiveData

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

  • 向ViewModel添加一个start()方法并尽快调用它
  • 设置启动加载的属性

❌您通常不会扩展LiveData。让那您的Activity或Fragment告诉ViewModel如何开始加载数据。