【背上Jetpack之LiveData】ViewModel 的左膀右臂 数据驱动真的香

系列文章

【背上Jetpack】Jetpack 主要组件的依赖及传递关系

【背上Jetpack】AdroidX下使用Activity和Fragment的变化

【背上Jetpack之Fragment】你真的会用Fragment吗?Fragment常见问题以及androidx下Fragment的使用新姿势

【背上Jetpack之Fragment】从源码角度看 Fragment 生命周期 AndroidX Fragment1.2.2源码分析

【背上Jetpack之OnBackPressedDispatcher】Fragment 返回栈预备篇

【背上Jetpack之Fragment】从源码的角度看Fragment 返回栈 附多返回栈demo

【背上Jetpack】绝不丢失的状态 androidx SaveState ViewModel-SaveState 分析

【背上Jetpack之ViewModel】即使您不使用MVVM也要了解ViewModel ——ViewModel 的职能边界

【背上Jetpack之Lifecycle】万物基于Lifecycle 默默无闻大用处

目录

前言

之前我们讨论过 ViewModel 的职能边界 ,得益于 ViewModel 的生命周期更长,我们可以在 activity 重建后将数据传递给 activity ,也可以避免内存泄漏。但是如果不是每次需要就获取数据,而是当每次有新数据时通知我们,应该怎么办?

本文介绍 LiveData ,一个 生命周期感知的,可观察的,数据持有者。同时还会简单分析 LiveData 的源码实现

我们都是 Adapter

在谈 LiveData 前我们来思考一个问题

Android 开发(亦或者说前端开发)的本质工作内容是什么?

对于应用层 app 开发者,开发者的工作主要工作就是 Adapter

什么是 Adapter ,下图可能比较直观

Adapter

图片来自 google image

我们的工作本质是 将数据转换成 UI

数据可能来自网络,来自本地数据库,来自内存,而 UI 可能是 activity 或 fragment。

理想的数据模型

上面我们提到 Android 开发者的核心工作就是将数据转换为 UI 。这个过程比较理想的状态是:当数据发生变化时,UI 跟随变化。我们还可以进一步展开:当 UI 对用户可见时,数据发生变化时 UI 跟随变化;当 UI 对用户不可见时,我们希望数据变化时什么都不做,当 UI 再次对用户可见时根据最新的数据进行 UI 的处理。

LiveData 就是我们理想中的数据模型

LiveData

图片来自 Android Dev Summit '18-Fun with LiveData

LiveData 可以三个关键词概括

  • lifecycle-aware

  • observable

  • data holder

observable

Android 中不同的组件有着不同的生命周期,不同的存活时间

ViewModel

因此我们不会在 ViewModel 中持有 Activity 的引用,因为这会导致当 Activity 重建时内存泄漏,甚至出现空指针的情况

observable

通常我们会在 Activity 中持有 ViewModel 的引用,那么如何进行二者间的通信,如何向 Activity 发送 ViewModel 中的数据?

答案是让 Activity 观察 ViewModel

image

LiveDataobservable

lifecycle-aware

当观察者观察着某个数据时,该数据必须保留对观察者的引用才能调用它,为了解决这个问题,LiveData 被设计成可感知生命周期

image

当 activity / fragment 被销毁后,它会自动的取消订阅

data holder

LiveData 仅持有 单个且最新 的数据

data holder

上图中,最右侧是在 ViewModel 中的 LiveData,左侧为观察这个 LiveData 的 activity / fragment 。一旦我们为 LiveData 设值,该值会传递到 activity。简而言之,LiveData 值改变,activity 收到最新的值的变化。但是当观察者不再处于活动状态(STARTED 到 RESUMED ),数据 C 不会被发送到 activity 。当 activity 回到前台,它将收到最新的值,数据 D。LiveData 仅持有单个且最新的数据。当 activity 执行销毁流程时,此时的数据 E 也不会产生任何影响

Transformations

LiveData 提供 两种 transformation ,mapswitch map。开发者也可以创建自定义的 MediatorLiveData

我们都知道 LiveData 可以为 View 和 ViewModel 提供通信,但如果有一个第三方组件(例如 repository )也持有 LiveData。那么它应该如何在 ViewModel 中订阅?该组件并没有 lifecycle

image

一旦我们的应用愈发复杂,repository 可能会观察数据源

image

那么 view 如何获取 repository 中的 LiveData

一对一的静态转换(map)

one-to-one static transformation

在上面的示例中,ViewModel 仅将数据从 repository 转发到 view,然后将其转换为 UI Model。 每当 repository 中有新数据时,ViewModel 只需 map

class MainViewModel {
  val viewModelResult = Transformations.map(repository.getDataForUser()) { data ->
     convertDataToMainUIModel(data)
  }
}

第一个参数为 LiveData 源(来自 repository ),第二个参数是一个转换函数。

// 这里的转换为将 X 转换为 Y
inline fun <X, Y> LiveData<X>.map(crossinline transform: (X) -> Y): LiveData<Y> =
        Transformations.map(this) { transform(it) }

一对一的动态转换(switchMap)

假如您正在观察一个提供用户的用户管理器,并且需要提供用户的 id 才能开始观察 repository

image

您不能将其写到 ViewModel 初始化的过程中,因为此时用户的 id 还不可用

这时 switchMap 就派上用场了

class MainViewModel {
  val repositoryResult = Transformations.switchMap(userManager.userId) { userId ->
     repository.getDataForUser(userId)
  }
}

switchMap 在内部使用 MediatorLiveData,因此了解它非常重要,因为当您要组合多个 LiveData 源时需要使用它

// 这里的转换为将 X 转换为 LiveData<Y>
inline fun <X, Y> LiveData<X>.switchMap(
    crossinline transform: (X) -> LiveData<Y>
): LiveData<Y> = Transformations.switchMap(this) { transform(it) }

一对多依赖(MediatorLiveData)

MediatorLiveData 允许您将一个或多个数据源添加到单个可观察的 LiveData

val liveData1: LiveData<Int> = ...
val liveData2: LiveData<Int> = ...

val result = MediatorLiveData<Int>()

result.addSource(liveData1) { value ->
    result.setValue(value)
}
result.addSource(liveData2) { value ->
    result.setValue(value)
}

在上面的例子中,当任何一个数据源变化时,result 会更新。

注意:数据并不是合并,MediatorLiveData 只是处理通知

为了实现示例中的转换,我们需要将两个不同的 LiveData 组合为一个

image

图片来自 LiveData beyond the ViewModel — Reactive patterns using Transformations and MediatorLiveData

使用 MediatorLiveData 合并数据的一种方法是添加源并以其他方法设置值:

fun blogpostBoilerplateExample(newUser: String): LiveData<UserDataResult> {

    val liveData1 = userOnlineDataSource.getOnlineTime(newUser)
    val liveData2 = userCheckinsDataSource.getCheckins(newUser)

    val result = MediatorLiveData<UserDataResult>()

    result.addSource(liveData1) { value ->
        result.value = combineLatestData(liveData1, liveData2)
    }
    result.addSource(liveData2) { value ->
        result.value = combineLatestData(liveData1, liveData2)
    }
    return result
}

数据的实际组合是在 combineLatestData 方法中完成的

private fun combineLatestData(
        onlineTimeResult: LiveData<Long>,
        checkinsResult: LiveData<CheckinsResult>
): UserDataResult {

    val onlineTime = onlineTimeResult.value
    val checkins = checkinsResult.value

    // Don't send a success until we have both results
    if (onlineTime == null || checkins == null) {
        return UserDataLoading()
    }

    // TODO: Check for errors and return UserDataError if any.

    return UserDataSuccess(timeOnline = onlineTime, checkins = checkins)
}

检查值是否准备好并发出结果(加载中,失败或成功)

LiveData 的错误用法

错误地使用 var LiveData

var lateinit randomNumber: LiveData<Int>

fun onGetNumber() {
   randomNumber = Transformations.map(numberGenerator.getNumber()) {
       it
   }
}

这里有一个重要的问题需要理解:转换会在调用时(mapswitchMap)会创建一个新的 LiveData。 在此示例中,randomNumber 公开给 View ,但是每次用户单击按钮时都会对其进行重新赋值。 观察者只会在订阅时收到分配给 var 的 LiveData 更新的信息

// 只会收到第一次分配的值
viewmodel.randomNumber.observe(this, Observer { number ->
    numberTv.text = resources.getString(R.string.random_text, number)
})

如果 viewmodel.randomNumber LiveData 实例发生更改,这里永远不会回调。而且这里泄漏了之前的 LiveData ,这些 LiveData 不会再发送更新

image

一言以蔽之,不要在 var 中使用 Livedata

正确示例见 demo

LiveData 粘性事件

一般来说我们使用 LiveData 持有 UI 数据和状态,但是如果通过它来发送事件,可能会出现一些问题。这些问题及解决方案 在这

fragment 中错误地传入 LifecycleOwner

androidx fragment 1.2.0 起,添加了新的 Lint 检查,以确保您在从 onCreateView()、onViewCreated() 或 onActivityCreated() 观察 LiveData 时使用 getViewLifecycleOwner()

image
bug

如图,我们有一个 fragment ,onCreate 观察 LiveData,通过正常的生命周期创建了 View ,接着进入了 resume 状态。此时你使用了 LiveData,UI 将开始展示它。之后,用户点击了按钮,由于跳转了另一个 fragment,所以要 detach 该 fragment,一旦 fragment stop 我们就不需要其中的 view 了,因此 destroyView 。之后用户点击了返回按钮回到了上一个 fragment,由于我们已经 destroyView,因此我们需要创建一个新的 view ,接着进入正常的生命周期,但此时,出现了一个 bug 。这个新 View 不会恢复 LiveData 的状态,因为我们使用的是 fragment 的 lifecycle observe 的 LiveData

我们有两种选择,在 onCreate 或者在 onCreateView 中使用 fragment 的 lifecycle observe LiveData

image

前者的优点是一次注册,缺点是当 recreate 时有bug;后者优点是能够解决 recreate 的 bug,但会导致重复注册

该问题的核心是 fragment 拥有两个生命周期:fragment 自身和 fragment 内部 view 的生命周期

androidx fragment 1.0support library 28 了 viewLifecycle

因此,当需要观察 view 相关的 LiveData ,可以在 onCreateView()、onViewCreated() 或 onActivityCreated() 中 LiveData observe 方法中传入 viewLifecycleOwner 而不是传入 this

源码结构

首先来看 LiveData 主要的源码结构

  • LiveData
  • MutableLiveData
  • Observer

LiveData

LiveData 是可以在给定生命周期内观察到的数据持有者类。 这意味着可以将一个ObserverLifecycleOwner 成对添加,并且只有在配对的 LifecycleOwner 处于活动状态时,才会向该观察者通知有关包装数据的修改。 如果 LifecycleOwner 的状态为 Lifecycle.State.STARTEDLifecycle.State.RESUMED,则将其视为活动状态。 通过 observeForever(Observer)添加的观察者被视为始终处于活动状态,因此将始终收到有关修改的通知。 对于那些观察者,需要手动调用 removeObserver(Observer)

如果相应的生命周期移至 Lifecycle.State.DESTROYED 状态,则添加了生命周期的观察者将被自动删除。 这对于 activity 和 fragment 可以安全地观察 LiveData 而不用担心泄漏

此外,LiveData 具有 onActive() 和 onInactive() 方法,以便在活动观察者的数量在 0 到 1 之间变化时得到通知。这使 LiveData 在没有任何活动观察者的情况下可以释放大量资源。

主要方法有:

  • T getValue() 获取LiveData 包装的数据
  • observe(LifecycleOwner owner, Observer<? super T> observer) 设置观察者(主线程调用)
  • setValue(T value) 设值(主线程调用),可见性为 protected 无法直接使用
  • postValue(T value) 设置(其他线程调用),可见性为 protected 无法直接使用

MutableLiveData

LiveData 实现类,公开了 setValuepostValue 方法

Observer

接口,内部只有 onChanged(T t) 方法,在数据变化时该方法会被调用

源码分析

我们通过源码来看看 LiveData 如何实现它的特性的

    1. 如何控制在 activity 或 fragment 活动状态时接收回调,否则不接收?
    1. 如何在 activity 或 fragment 销毁时自动取消注册观察者?
    1. 如何保证 LiveData 持有最新的数据?

我们查看 LiveData 的 observe 方法

// LiveData.java
@MainThread
public void observe(LifecycleOwner owner, Observer<? super T> observer) {
    if (owner.getLifecycle().getCurrentState() == DESTROYED) {
        // 如果 owner 已经是 DESTROYED 状态,则忽略
        return;
    }
    // 使用 LifecycleBoundObserver 包装 owner 和 observer
    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
    // 如果已经添加过直接 return 
    if (existing != null) {
        return;
    }
   
    owner.getLifecycle().addObserver(wrapper);
}

// LifecycleBoundObserver.java
class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
    @NonNull
    final LifecycleOwner mOwner;
    LifecycleBoundObserver(LifecycleOwner owner, Observer<? super T> observer) {
        super(observer);
        mOwner = owner;
    }
}

通过源码我们知道,当我们调用 observe 方法时,内部是通过 LifecycleBoundObserver 将 owner 和 observer 包裹起来并通过 addObserver 方法添加观察者的,因而当数据变化时,会调用 LifecycleBoundObserveronStateChanged 方法

// LiveData.LifecycleBoundObserver.java
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
        @NonNull Lifecycle.Event event) {
    if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
        // 自动移除观察者,问题 2 得到解释
        removeObserver(mObserver);
        return;
    }
    activeStateChanged(shouldBeActive());
}

当什么周期所有者处于 DESTROYED 状态时,会调用 removeObserver 方法,因此问题 2 得到解释

我们继续向下看,activeStateChanged 方法调用时传入了 shouldBeActive()

@Override
boolean shouldBeActive() {
    // 至少是 STARTED 状态 返回 true
    return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
}

void activeStateChanged(boolean newActive) {
    if (newActive == mActive) {
        // 与上次值相同,则直接 return (两次均为活动状态或均为非活动状态)
        return;
    }
    mActive = newActive;
    boolean wasInactive = LiveData.this.mActiveCount == 0;
    // 根据 mActive 修改活动状态观察者的数量(加 1 或减 1 )
    LiveData.this.mActiveCount += mActive ? 1 : -1;
    if (wasInactive && mActive) {
        onActive();
    }
    if (LiveData.this.mActiveCount == 0 && !mActive) {
        onInactive();
    }
    if (mActive) {
        // 如果是活动状态,则发送数据,问题 1 得到解释
        dispatchingValue(this);
    }
}

这里牵扯了 Lifecycle State 比较的知识,详情在这

只有 STARTEDRESUMED 状态 shouldBeActive() 才返回 true,至此问题 1 得到解释

dispatchingValue 方法内部调用了 considerNotify 方法

private void considerNotify(ObserverWrapper observer) {
    if (!observer.mActive) {
        return;
    }
    // 再次判断生命周期所有者状态
    if (!observer.shouldBeActive()) {
        observer.activeStateChanged(false);
        return;
    }
    // 比较版本号
    if (observer.mLastVersion >= mVersion) {
        return;
    }
    observer.mLastVersion = mVersion;
    // 调用我们传入的 mObserver 的 onChanged 方法
    observer.mObserver.onChanged((T) mData);
}

可以看到 considerNotify 中比较了 observer 的版本号,如果是最新的数据,直接 return

mVersionsetValue 方法中 进行更改

@MainThread
protected void setValue(T value) {
    // 每次设置对 mVersion 进行++
    mVersion++;
    mData = value;
    dispatchingValue(null);
}

因此 LiveData 每次都持有最新的数据,问题 3 得到解释

总结

回到本文开头的思考,Android 开发者的主要工作是将数据转换成 UI ,而 LiveData 本质上是一种「数据驱动」,即通过改变状态数据,来驱动视图树中绑定了相应状态数据的控件重新发生绘制。Flutter 和未来的 Jetpack Compose 采用的都是这种机制。使用 ViewModel + LiveData,可以 安全地在订阅者的生命周期内分发正确的数据,使开发者不知不觉中完成了 UI -> ViewModel -> Data 的单向依赖。

所谓架构,很多时候不是使用它能做什么,更多的是不要做什么,使用它时开发者能够得到约束,以便产出更健壮的代码

各位小伙伴如果有什么想法欢迎在评论区留言


关于我


我是 Fly_with24

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容