ViewModel 与 LiveData - 模式与反模式

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 。这有一些优点:

  1. ViewModel 在配置变化时被持久化,所以当配置变更(如旋转)时,不需要重新查询外部数据源(如数据库或网络)。
  2. 当长时间运行的操作结束时,ViewModel 中的可观察变量会被更新。数据是否正在被观察并不重要。当试图更新不存在的视图时,也不会发生空指针异常。
  3. 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 里的代码太多或者责任太多,可以考虑以下几个办法:

  1. 将一些逻辑转移到与 ViewModel 相同作用域的 presenter 中。由它负责与app 其他部分通信,并更新 ViewModel 中的 LiveData。
  2. 添加一个网域层(Domain layer)并采用 Clean 架构。这将是一个可测试和可维护性很高的架构。它也有利于快速摆脱主线程的束缚。在 架构蓝图 中有一个Clean 架构的例子。

✅ 职责分离。必要时添加网域层(domain layer)。这是谷歌官方对网域层的介绍链接

使用数据仓库

正如在《应用程序架构指南》中看到的那样,大多数应用程序都有多个数据源,比如:

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

在你的应用程序中设计一个数据层是个好主意,数据层对表现层完全无感知。让缓存和数据库与网络保持同步的算法并不简单。建议有一个单独的数据仓库类作为单一入口,仓库内部处理这种复杂性。

如果你有多个差别很大的数据模型,可以考虑添加多个数据仓库。

✅ 添加一个数据仓库作为数据访问的唯一入口

处理数据状态

考虑这个场景:你正在观察一个由 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 layerObserver 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
The activity is finished but the ViewModel is still around

如果 ViewModel 是轻量级的,或者操作可以保证快速完成,这种泄漏就不是大问题。然而情况并不总是这样的。理想情况下,只要没有任何视图在观察,ViewModel 就应该被释放了。

在这里插入图片描述
有很多方法可以实现这一点:

  1. ViewModel.onCleared() 方法中告诉 repository 放弃对 ViewModel 的回调。
  2. 在 repository 中,可以使用弱引用( WeakReference) ,也可以使用事件总线(EventBus)(两者都容易被滥用,甚至被认为是有害的)。
  3. 使用 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 被观察时才加载服务。这里有一些常见的方法:

  1. 给 ViewModel 添加一个 start() 方法,并尽早调用它
  2. 设置一个启动加载的属性

❌ 通常不用扩展 LiveData。让 Activity 或 Fragment 告诉 ViewModel 何时开始加载数据

原文链接:ViewModels and LiveData: Patterns + AntiPatterns

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值