(译) ViewModel 背后的 LiveData - 使用 Transfromations 和 MediatorLiveData 的响应式模式

原文链接

多年来,响应式架构一直是 Android 的热门话题。它是 Android 会议不变的主题,通常以 RxJava 为例子举例说明(参见底部的 Rx 章节 )。响应式编程是一种关注数据流动和变化传播的范例,它可以简化构建应用程序和显示来自异步操作的数据。

LiveData 是一个实现一些响应式概念的工具。这是一个知道观察者生命周期的简单可观察者。从你的数据源或存储库中公开 LiveData 是一种可以使你的架构更具反应性简单的方法,但存在一些潜在的缺陷。

这篇博文将帮助你避免陷阱并使用一些模式来帮助你使用 LiveData 构建更具响应性的架构。

LiveData 的目的

在 Android 中,Activity ,Fragment 和视图几乎可以在任何时候被销毁,因此对这些组件之一的任何引用都可能导致泄漏或 NullPointerException

LiveData 旨在实现观察者模式,允许视图控制器(Activity,Fragment等)与UI数据源(通常是 ViewModel )之间实现通信。使用了 LiveData 之后,这种通信变得更为安全:归功于它的可感知生命周期的能力,数据只有在视图处于活动状态时才会被接收。

简而言之,优点是你无需手动取消视图和 ViewModel 之间的订阅。

视图和ViewModel之间的交互

ViewModel 背后的 LiveData

可观察范式在视图控制器和 ViewModel 之间非常有效,因此你可以使用它来观察应用程序的其他组件并其利用生命周期的感知能力。例如:

这种范式的优点是,因为所有内容都连接在一起,所以当数据发生变化时,UI会自动更新。

缺点是 LiveData 没有像Rx一样附带工具包来组合数据流或管理线程。

在典型应用程序的每一层中使用 LiveData 看起来是像这样的:

使用 LiveData 的典型架构

为了在组件之间传递数据,我们需要一种方法来进行映射和组合。 MediatorLiveData 与Transformations 类中的帮助程序结合使用就是用于此的:

请注意,当视图被销毁时,你无需取消这些订阅,因为视图的生命周期会向下游传播到后续订阅。

模式

一对一静态转换-map

观察的是一种类型的数据暴露的却是另一种类型的数据的 ViewModel

在上面的示例中,ViewModel 仅将数据从存储库转发到视图中,并将其转换为 UI 模型。每当存储库有新数据时,ViewModel 只需映射它:

1
2
3
4
5
class MainViewModel {
val viewModelResult = Transformations.map(repository.getDataForUser() { data ->
convertDataToMainUIModel(data)
}
}

这种转变非常简单。但是,如果用户可能会发生变化,则需要switchMap:

一对一动态转换-switchMap

请考虑以下示例:你正在观察公开用户信息的用户管理器,你需要等待其ID才能开始观察存储库。

用户管理者提供了存储库暴露结果之前所需的用户ID

你无法在 ViewModel 初始化时连接它,因为用户 ID 不会立即可用。

你可以使用 switchMap 实现此功能。

switchMap 内部使用 MediatorLiveData ,因此熟悉它是很重要的,因为当你想要组合多个 LiveData 源时需要使用它:

一对多依赖- MediatorLiveData

MediatorLiveData 允许你将一个或多个数据源添加到单个 LiveData 可观察对象。

1
2
3
4
5
6
7
8
9
10
11
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)
}

此示例来自文档,在任何源发生变化的时候结果会被更新。请注意,数据不会合并好给您。 MediatorLiveData 只负责通知。

为了在我们的示例应用程序中实现转换,我们需要将两个不同的 LiveData 合并为一个:

用来结合两个数据源的 MediatorLiveData

使用 MediatorLiveData 组合数据的一种方法是添加源并使用不同的方法设置值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 方法中完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)
}

它检查值是否准备好或正确并发出结果(加载,错误或成功)

请参阅下面的额外的部分,了解如何使用 Kotlin 的扩展功能进行清理。

什么时候不要用 LiveData

即使你想要迈入响应式,你也需要了解在各处添加 LiveData 之前了解它的优势。如果你的应用程序的某个组件与UI没有连接,则可能不需要 LiveData。

例如,应用中的用户管理员会监听您的身份验证提供程序中的更改(例如Firebase身份验证),并将唯一令牌上传到你的服务器。

用户管理和令牌上传之间的交互应该是响应式的吗?

令牌上传者可以观察用户管理器,但是是在哪个的生命周期?此操作与视图完全无关。此外,如果视图被销毁,则可能无法上传用户令牌。

另一种选择是使用令牌上传器中的 observeForever() 并以某种方式 hook 到用户管理器的生命周期中,从而在完成后删除订阅。

但是,你不需要使所有内容都可观察。让用户管理器直接调用令牌上传器。

反模式:共享 LiveData 的实例

当类将 LiveData 公开给其他类时,请仔细考虑是否要公开相同的LiveData实例或不同的实例。

1
2
3
4
5
6
7
8
9
10
class SharedLiveDataSource(val dataSource: MyDataSource) {

// Caution: this LiveData is shared across consumers
private val result = MutableLiveData<Long>()

fun loadDataForUser(userId: String): LiveData<Long> {
result.value = dataSource.getOnlineTime(userId)
return result
}
}

如果这个类是你的应用程序中的单例(它只有一个实例),你可以随时返回相同的LiveData,对吧?不一定:这个类可能有多个消费者。

例如,考虑一下:

1
2
3
sharedLiveDataSource.loadDataForUser("1").observe(this, Observer {
// Show result on screen
})

第二个消费者也使用它:

1
2
3
sharedLiveDataSource.loadDataForUser("2").observe(this, Observer {
// Show result on screen
})

第一个消费者将收到包含属于用户“2”的数据的更新。

即使你认为你只使用来自一个消费者的此类,你最终也可能在使用这个模式的时候产生错误。例如,当从一个Activity实例导航到另一个实例时,新实例可能在短暂的时间内从前一个实例接收数据。请记住,LiveData 会将最新值分派给新的观察者。此外,Lollipop 引入了Activity 转场效果,它们带来了一个有趣的边缘案例:两个处于活动状态的 Activity å。这意味着 LiveData 的唯一消费者可能有两个实例,其中一个可能会显示错误的数据。

这个问题的解决方案就是为每个消费者返回一个新的 LiveData:

1
2
3
4
5
6
7
class SharedLiveDataSource(val dataSource: MyDataSource) {
fun loadDataForUser(userId: String): LiveData<Long> {
val result = MutableLiveData<Long>()
result.value = dataSource.getOnlineTime(userId)
return result
}
}

在多个消费者之间共享 LiveData 的时候请思虑再三。

MediatorLiveData 的味道:在初始化之外添加源

使用观察者模式比保持对视图的引用更安全(你通常在 MVP 体系结构中执行的操作)。但是,这并不意味着你可以不管泄漏这回事了!

考虑这个数据源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SlowRandomNumberGenerator {
private val rnd = Random()

fun getNumber(): LiveData<Int> {
val result = MutableLiveData<Int>()

// Send a random number after a while
Executors.newSingleThreadExecutor().execute {
Thread.sleep(500)
result.postValue(rnd.nextInt(1000))
}

return result
}
}

它只是在500ms后返回一个带有随机值的新LiveData。这没什么不对。

在ViewModel中,我们需要公开一个randomNumber属性,该属性从生成器中获取数字。为此使用MediatorLiveData并不理想,因为它需要您在每次需要新号码时添加源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val randomNumber = MediatorLiveData<Int>()

/**
* *Don't do this.*
*
* Called when the user clicks on a button
*
* This function adds a new source to the result but it doesn't remove the previous ones.
*/
fun onGetNumber() {
randomNumber.addSource(numberGenerator.getNumber()) {
randomNumber.value = it
}
}

如果每次用户点击按钮,我们都会向MediatorLiveData添加一个源,该应用程序将按预期工作。但是,我们泄漏了以前不再发送更新的所有LiveDatas,所以这是浪费。

您可以存储对源的引用,然后在添加新引用之前将其删除。 (Spoiler:这是Transformations.switchMap的作用!请参阅下面的解决方案。)

不要使用MediatorLiveData,让我们尝试(并失败)使用Transformation.map修复它:

Transformation 的味道:在初始化之外添加源

使用之前的例子不会有效:

1
2
3
4
5
6
7
8
9
10
var lateinit randomNumber: LiveData<Int>

/**
* Called on button click.
*/
fun onGetNumber() {
randomNumber = Transformations.map(numberGenerator.getNumber()) {
it
}
}

这里有一个重要的问题需要解决:转换在调用时会创建一个新的 LiveData(map和switchMap)。在此示例中,randomNumber 向视图公开,但每次用户单击按钮时都会重新分配。很遗憾,观察者只会在订阅时收到分配给var的 LiveData 的更新。

1
2
3
viewmodel.randomNumber.observe(this, Observer { number ->
numberTv.text = resources.getString(R.string.random_text, number)
})

此订阅发生在onCreate() 中,因此如果 viewmodel.randomNumber LiveData 实例之后发生更改,则永远不会再次调用观察者。

换一种说法:

不要在 var 中使用 Livedata。初始化时的线程转换。

解决方案:在初始化的时候连接转换

将暴露的 LiveData 初始化为 transformation

1
2
3
4
5
private val newNumberEvent = MutableLiveData<Event<Any>>()

val randomNumber: LiveData<Int> = Transformations.switchMap(newNumberEvent) {
numberGenerator.getNumber()
}

使用 LiveData 中的事件来指示何时请求新号码:

1
2
3
4
5
6
/**
* Notifies the event LiveData of a new request for a random number.
*/
fun onGetNumber() {
newNumberEvent.value = Event(Unit)
}

如果您不熟悉此模式,请参阅此帖子

附加部分

使用 Kotlin 进行收拾

上面的 MediatorLiveData 示例显示了一些代码重复,因此我们可以利用 Kotlin 的扩展函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Sets the value to the result of a function that is called when both `LiveData`s have data
* or when they receive updates after that.
*/
fun <T, A, B> LiveData<A>.combineAndCompute(other: LiveData<B>, onChange: (A, B) -> T): MediatorLiveData<T> {

var source1emitted = false
var source2emitted = false

val result = MediatorLiveData<T>()

val mergeF = {
val source1Value = this.value
val source2Value = other.value

if (source1emitted && source2emitted) {
result.value = onChange.invoke(source1Value!!, source2Value!! )
}
}

result.addSource(this) { source1emitted = true; mergeF.invoke() }
result.addSource(other) { source2emitted = true; mergeF.invoke() }

return result

存储库的代码现在看起来整洁多了:

1
2
3
4
5
6
7
8
9
10
11
fun getDataForUser(newUser: String?): LiveData<UserDataResult> {
if (newUser == null) {
return MutableLiveData<UserDataResult>().apply { value = null }
}

return userOnlineDataSource.getOnlineTime(newUser)
.combineAndCompute(userCheckinsDataSource.getCheckins(newUser)) { a, b ->
UserDataSuccess(a, b)
}
}
view raw

LiveData 和 RxJava

最后,让我们解决房间里的大象。 LiveData 旨在允许视图观察 ViewModel。绝对可以用它!即使你已经使用Rx,也可以使用 LiveDataReactiveStreams* 进行通信。

如果要在表示层之外使用 LiveData,你可能会发现 MediatorLiveData 没有像 RxJava 那样提供工具包来组合和操作数据流。然而,Rx 带有陡峭的学习曲线。 LiveData 转换(和Kotlin魔术)的组合可能足以满足你的需求,但如果你(和你的团队)已经投入学习RxJava,你可能不需要 LiveData。

*如果你使用auto-dispose,使用LiveData 将是多余的。

非典型前端coder wechat
想要随时Follow我的最新博客,可扫码关注我的公众号