LiveData 数据倒灌:别问,问就是不可预期

12,027 阅读5分钟

背景

LiveData 是效仿响应式编程 BehaviorSubject 的设计,由于

1.Jetpack 架构示例通常只包含 “表现层” 和 “数据层” 两层,缺乏在 “领域层” 分发数据的工具,

2.LiveData Observer 的设计缺乏边界感,

容易让开发者误当做 “一次性事件分发组件” 来使用,造成订阅时 "自动回推脏数据";

容易让开发者误将同一控件实例放在多个 Observer 回调中 造成恢复状态时 “数据不一致” 等问题(具体可参见《MVI 存在意义》篇 关于 “响应式编程漏洞” 的描述)

3.DataBinding ObservableField 组件的 Observer 能限定为 "与控件一对一绑定",更适合承担表现层 BehaviorSubject 工作,

4.LiveData 具备生命周期安全等优势,

因此决定将 LiveData 往领域层 PublishSubject 方向改造,去除其 “自动推送最后一次状态” 的能力,使其专职生命周期安全的数据分发,

 

框架现状

Note 2023.4.25:

由于 LiveData 存在的初衷并非是专业的 “一次性事件分发组件”,改造过的 UnPeekLiveData 也只适用于 “低频次数据分发(例如每秒推送 1 次)” 场景,

因而若想满足 “高频次事件分发” 需求(例如每秒推送 5 次以上),请改用或参考专职 “领域层” 数据分发的 MVI-Dispatcher 组件,该组件内部通过消息队列设计,确保不漏掉每一次推送。

Note 2021.8.20:

腾讯音乐小伙伴贡献过 v5 版重构代码,用于月活过亿 “生产环境” 痛点治理。

最新版基于 2021 年 8 月小伙伴 RebornWolfman 贡献 v7 版重构代码,稳定运行至今。

为契合消息分发语义,我们于最新版加入 MutableResult/Result 类命名。

 


 

前言

大家好,我是《Jetpack MVVM Best Practice》作者 KunMinX。

今天提到 “数据倒灌” 一词,缘于我为方便理解和记忆 “再入二级页面时收到旧数据推送” 情况,而于 2019 年 自创并在网上传播的关于此类现象概括

它主要发生于:SharedViewModel + LiveData 组合实现页面通信场景。

 

背景

由于本文目标是介绍官方 Demo 现有解决方案缺陷,及经过 2 年迭代趋于完美解决方案,

所以我们假设在座诸位对最基本 “背景缘由” 有一定了解,知道:

为何 LiveData 默认被设计为粘性事件

为何 官方文档 推荐使用 SharedViewModel + LiveData(文档没明说,事实上包含三个关键背景缘由)

乃至为何存在 “数据倒灌” 现象

及为何 “页面通信” 场景下,不用静态单例或 EventBus、LiveDataBus 等消息总线,

如对这些 “前置知识” 也尚无体会,可结合个人兴趣前往《LiveData 数据倒灌 背景缘由全貌 独家解析》查阅,此处不再累述。

 

现有解决方案及各自缺陷

《Jetpack MVVM 精讲》中我们分别提到 Event 事件包装器、反射方式、SingleLiveEvent 三种方式解决 “数据倒灌” 问题。它们分别来自文中提到的外网美团文章,和官方最新 demo

但正如《Jetpack MVVM 精讲》所述,它们分别存在如下问题:

Event 事件包装器:

对于多观察者情况,只允许第一个观察者消费,这不符合现实需求;

且手写 Event 事件包装器,在 Java 中存在 Null 安全一致性问题。

反射干预 Version 方式:

存在延迟,无法用于对实时性有要求场景;

且数据会随着 SharedViewModel 长久滞留内存中得不到释放。

官方最新 demo SingleLiveEvent:

是对 Event 事件包装器 “一致性问题” 改进,但未解决 “多观察者消费” 问题;

且额外引入 “消息未能从内存中释放” 问题。

 

UnPeek-LiveData 特点

1.一条消息能被多个观察者消费(since v1.0)

2.消息被所有观察者消费完毕后才开始阻止倒灌(since v4.0)

3.可通过 clear 方法手动将消息从内存中移除(since v4.0)

4.让非入侵设计成为可能,遵循开闭原则(since v3.0)

5.基于 "访问权限控制" 支持 "读写分离",遵循 “唯一可信源” 消息分发理念(since v2.0,详见 ProtectedUnPeekLiveData)

public class TestFragment extends Fragment {
  protected void onViewCreate(){
    viewModel.getXXXResult().observe(this, xxx ->{
      renderUI(...);
    })
​
    viewModel.requestXXX();
  }
}
​
public class SharedViewModel extends ViewModel {
  private final MutableResult<XXX> xxxResult = new MutableResult<>();
​
  public Result<XXX> getXXXResult(){
    return xxxResult;
  }
​
  public void requestXXX(){
    //业务逻辑 ...
    ...
    xxxResult.setValue(...);
  }
}

且 UnPeekLiveData 提供构造器模式,后续可通过构造器组装适合自己业务场景 UnPeekLiveData。

MutableResult<Moment> test =
        new MutableResult.Builder<Moment>()
        .setAllowNullValue(false)
        .create();

 

Maven 依赖

implementation 'com.kunminx.arch:unpeek-livedata:7.8.0'

温馨提示:

1.上述 implementation 命名,我们已从 archi 改为 arch,请注意修改,

2.鉴于 Jcenter 关闭,我们已将仓库迁移至 Maven Central,请自行在根目录 build.gradle 添加 mavenCentral()

 

Thanks

PS:感谢近期 hegaojian、Angki、Flynn、Joker_Wan、小暑知秋、大棋、空白、qh、lvrenzhao、半节树人 等小伙伴积极试用和反馈,使潜在问题被及时发现和纳入考虑。

 

GitHub & Maven 依赖

详见:GitHub:KunMinX/UnPeekLiveData

   

License

本文以 CC 署名-非商业性使用-禁止演绎 4.0 国际协议 发行。

Copyright © 2019-present KunMinX

文中提到的 对 “数据倒灌” 一词及其现象的概括、对 Event 事件包装器、反射方式、SingleLiveEvent 各自存在的缺陷的理解,以及对 UnPeekLiveData 的 “延迟自动清理消息” 的设计,均属于本人独立原创的成果,本人对此享有所有权和最终解释权。

当您借鉴或引用本文的引言、思路、结论进行二次创作,或全文转载时,须注明链接出处,否则我们保留追责的权利。