发布时间:2021年5月17日 - 9分钟阅读
LiveData是我们在2017年时需要的东西。观察者模式让我们的生活更轻松,但当时RxJava等选项对初学者来说太复杂了。架构组件团队创建了LiveData:一个非常有主见的可观察数据持有者类,专为Android设计。该类保持简单,以便于入门,建议使用RxJava来处理更复杂的反应式流案例,利用两者之间的集成优势。
死数据?
LiveData仍然是我们针对Java开发者、初学者和简单情况的解决方案。对于其他人来说,一个好的选择是转向Kotlin Flows。Flows仍然有一个陡峭的学习曲线,但它们是Kotlin语言的一部分,由Jetbrains支持;而且Compose即将到来,这与反应式模型非常吻合。
我们已经讨论了一段时间使用流程来连接你的应用程序的不同部分,除了视图和ViewModel。现在我们有了一个更安全的方法来收集Android UIs的流量,我们可以创建一个完整的迁移指南。
在这篇文章中,你将学习如何将流暴露在视图中,如何收集它们,以及如何进行微调以适应特定的需求。
流程:简单的事情更难,复杂的事情更容易
LiveData只做了一件事,而且做得很好:它暴露了数据,同时缓存了最新的价值并理解了Android的生命周期。后来我们了解到,它还可以启动循环程序并创建复杂的转换,但这是一个比较复杂的问题。
让我们来看看一些LiveData模式和它们的Flow对应物。
#1:用一个可变数据持有者暴露一次操作的结果
这是一个经典的模式,你用一个coroutine的结果来突变一个状态保持器。
用一个可变数据持有者(LiveData)暴露一次操作的结果
<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
class MyViewModel {
private val _myUiState = MutableLiveData<Result<UiState>>(Result.Loading)
val myUiState: LiveData<Result<UiState>> = _myUiState
// Load data from a suspend fun and mutate state
init {
viewModelScope.launch {
val result = ...
_myUiState.value = result
}
}
}
为了对流程做同样的事情,我们使用(Mutable)StateFlow。
用一个可变数据持有者(StateFlow)来显示一次操作的结果。
class MyViewModel {
private val _myUiState = MutableStateFlow<Result<UiState>>(Result.Loading)
val myUiState: StateFlow<Result<UiState>> = _myUiState
// Load data from a suspend fun and mutate state
init {
viewModelScope.launch {
val result = ...
_myUiState.value = result
}
}
}
StateFlow是SharedFlow(它是Flow的一种特殊类型)的一种特殊类型,最接近于LiveData。
- 它总是有一个值。
- 它只有一个值。
- 它支持多个观察者(所以流是共享的)。
- 它总是在订阅时复制最新的值,与活动观察者的数量无关。
当向视图暴露UI状态时,使用StateFlow。它是一个安全、高效的观察者,被设计用来保持UI状态。
#2:暴露一次操作的结果
这相当于前面的片段,暴露了一个没有可变支持属性的coroutine调用的结果。 在LiveData中,我们使用liveData coroutine builder来实现这一点。
暴露一次操作的结果(LiveData)
class MyViewModel(...) : ViewModel() {
val result: LiveData<Result<UiState>> = liveData {
emit(Result.Loading)
emit(repository.fetchItem())
}
}
由于状态持有者总是有一个值,所以用某种支持加载、成功和错误等状态的结果类来包装我们的UI状态是个好主意。
与Flow等价的东西要多一点,因为你必须做一些配置。
暴露一次操作的结果(StateFlow)
class MyViewModel(...) : ViewModel() {
val result: StateFlow<Result<UiState>> = flow {
emit(repository.fetchItem())
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000), // Or Lazily because it's a one-shot
initialValue = Result.Loading
)
}
stateIn是一个Flow操作符,可以将一个Flow转换为StateFlow。让我们暂时相信这些参数,因为我们需要更多的复杂性来正确解释它。
#3:带参数的一次性数据加载
假设你想加载一些取决于用户ID的数据,并且你从一个暴露了Flow的AuthManager那里获得这些信息。
带参数的一次性数据加载(LiveData)
使用LiveData,你会做一些类似的事情。
class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: LiveData<String?> =
authManager.observeUser().map { user -> user.id }.asLiveData()
val result: LiveData<Result<Item>> = userId.switchMap { newUserId ->
liveData { emit(repository.fetchItem(newUserId)) }
}
}
switchMap是一个转换,当userId发生变化时,其主体被执行,结果被订阅。
如果没有理由让userId成为LiveData,那么更好的替代方法是将流与Flow相结合,最后将暴露的结果转换为LiveData。
class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }
val result: LiveData<Result<Item>> = userId.mapLatest { newUserId ->
repository.fetchItem(newUserId)
}.asLiveData()
}
用Flow做这个看起来非常相似。
带> 有参数的一次性数据加载(StateFlow)
注意,如果你需要更多的灵活性,你也可以使用transformLatest并显式地发射项目。
val result = userId.transformLatest { newUserId ->
emit(Result.LoadingData)
emit(repository.fetchItem(newUserId))
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.LoadingUser // Note the different Loading states
)
#4:观察带有参数的数据流
现在让我们把这个例子变得更有反应性。数据不是被获取的,而是被观察的,所以我们将数据源的变化自动传播到用户界面。
继续我们的例子:我们不在数据源上调用fetchItem,而是使用一个假想的observeItem操作符,返回一个Flow。
通过LiveData,你可以将流转换为LiveData,并将所有的更新信息发射出去。
观察一个带参数的流(LiveData)
class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: LiveData<String?> =
authManager.observeUser().map { user -> user.id }.asLiveData()
val result = userId.switchMap { newUserId ->
repository.observeItem(newUserId).asLiveData()
}
}
或者,最好是使用flatMapLatest将两个流结合起来,只将输出转换为LiveData。
class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: Flow<String?> =
authManager.observeUser().map { user -> user?.id }
val result: LiveData<Result<Item>> = userId.flatMapLatest { newUserId ->
repository.observeItem(newUserId)
}.asLiveData()
}
Flow的实现类似,但它没有LiveData的转换。
观察一个带参数的流(StateFlow)
class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: Flow<String?> =
authManager.observeUser().map { user -> user?.id }
val result: StateFlow<Result<Item>> = userId.flatMapLatest { newUserId ->
repository.observeItem(newUserId)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.LoadingUser
)
}
每当用户发生变化或用户在资源库中的数据发生变化时,暴露的StateFlow将收到更新。
#5 结合多个来源。MediatorLiveData -> Flow.combined
MediatorLiveData让你观察一个或多个更新源(LiveData观察者),并在它们获得新数据时做一些事情。通常情况下,你会更新MediatorLiveData的值。
val liveData1: LiveData<Int> = ...
val liveData2: LiveData<Int> = ...
val result = MediatorLiveData<Int>()
result.addSource(liveData1) { value ->
result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))
}
result.addSource(liveData2) { value ->
result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))
}
Flow的等价物就更直接了。
val flow1: Flow<Int> = ...
val flow2: Flow<Int> = ...
val result = combine(flow1, flow2) { a, b -> a + b }
你也可以使用combinedTransform函数,或者zip。
配置暴露的StateFlow(stateIn操作符
我们之前使用stateIn将一个普通的流转换为StateFlow,但它需要一些配置。如果你现在不想细究,只需要复制粘贴,这个组合是我推荐的。
val result: StateFlow<Result<UiState>> = someFlow
.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
然而,如果你对那个看似随机的5秒开始的参数不确定,请继续阅读。 stateIn有3个参数(来自文档)。
@param scope 启动共享的coroutine范围。
@param started 控制何时开始和停止共享的策略。
@param initialValue 状态流的初始值。
当使用带有`replayExpirationMillis'参数的[SharingStarted.WhileSubscribed]策略重置状态流时,也会使用这个值。
started可以取3个值。
- 懒惰地:在第一个订阅者出现时开始,在范围取消时停止。
- 急切地:立即开始,当范围被取消时停止。
- WhileSubscribed。这很复杂。
对于一次性操作,你可以使用Lazily或Eagerly。然而,如果你在观察其他流程,你应该使用WhileSubscribed来做一些小但重要的优化,如下所述。
WhileSubscribed策略
当没有收集器时,WhileSubscribed会取消上游的流。使用stateIn创建的StateFlow将数据暴露给View,但它也在观察来自其他层或应用程序(上游)的流。保持这些流的活动可能会导致资源的浪费,例如,如果它们继续从其他来源读取数据,如数据库连接、硬件传感器等。当你的应用程序转到后台时,你应该做一个好公民,停止这些轮询程序。
WhileSubscribed需要两个参数。
public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
)
停止超时
从它的文档来看。
stopTimeoutMillis配置最后一个用户的消失和上游流的停止之间的延迟(以毫秒为单位)。它的默认值是零(立即停止)。
这很有用,因为你不想在视图停止监听几分之一秒的时候取消上游流量。这种情况经常发生--例如,当用户旋转设备时,视图被快速销毁并重新创建。
在liveData coroutine构建器中的解决方案是添加一个5秒的延迟,之后如果没有订阅者,coroutine将被停止。WhileSubscribed(5000)正是这样做的。
class MyViewModel(...) : ViewModel() {
val result = userId.mapLatest { newUserId ->
repository.observeItem(newUserId)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
}
这种方法检查了所有的盒子。
- 当用户将你的应用程序发送到后台时,来自其他层的更新将在5秒后停止,从而节省电池。
- 最新的值仍将被缓存,因此当用户回来时,视图将立即拥有一些数据。
- 订阅被重新启动,新的值会进来,在可用时刷新屏幕。
重放过期
如果你不想让用户在离开太久后看到陈旧的数据,而你更喜欢显示一个加载屏幕,请查看WhileSubscribed中的replayExpirationMillis参数。在这种情况下,它非常方便,而且还能节省一些内存,因为缓存的值会恢复到stateIn中定义的初始值。回到应用程序不会那么快,但你不会显示旧数据。
replayExpirationMillis--配置共享程序停止和重放缓存重置之间的延迟(以毫秒为单位)(这使得shareIn操作者的缓存为空,并将缓存值重置为stateIn操作者的原始初始值)。它的默认值是Long.MAX_VALUE(永远保持重放缓存,从不重置缓冲区)。使用零值来立即过期缓存。
从视图中观察StateFlow
正如我们到目前为止所看到的,对于视图来说,让ViewModel中的StateFlows知道它们不再监听是非常重要的。然而,就像所有与生命周期有关的事情一样,这不是那么简单。
为了收集一个流程,你需要一个coroutine。活动和片段提供了一堆的coroutine构建器。
- Activity.lifecycleScope.launch:立即启动coroutine,并在活动被破坏时取消它。
- Fragment.lifecycleScope.launch:立即启动循环程序,并在片段被销毁时取消它。
- Fragment.viewLifecycleOwner.lifecycleScope.launch:立即启动coroutine并在片段的视图生命周期被销毁时取消它。如果你要修改用户界面,你应该使用视图生命周期。
LaunchWhenStarted, launchWhenResumed...
launch的专门版本,称为 launchWhenX,它将等到lifecycleOwner处于X状态,并在lifecycleOwner下降到X状态时暂停coroutine。值得注意的是,在其生命周期所有者被销毁之前,它们不会取消循环程序。
用 launch/launchWhenX 收集流是不安全的
在应用程序处于后台时接收更新可能会导致崩溃,这可以通过暂停视图中的收集来解决。然而,当应用程序在后台时,上游流保持活跃,可能会浪费资源。
这意味着我们到目前为止为配置StateFlow所做的一切都将是非常无用的;然而,有一个新的API在城里。
lifecycle.repeatOnLifecycle的救援
这个新的coroutine builder(可从lifecycle-runtime-ktx 2.4.0-alpha01中获得)正是我们所需要的:它在一个特定的状态下启动coroutine,当生命周期所有者低于该状态时,它就会停止它们。
不同的流量收集方法
例如,在一个Fragment中。
onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
myViewModel.myUiState.collect { ... }
}
}
}
这将在Fragment的视图被STARTED时开始收集,将持续到RESUMED,并在返回到STOPPED时停止。请在《从Android UIs收集流量的更安全的方法》中阅读全部内容。
将repeatOnLifecycle API与上面的StateFlow指导混合在一起,可以获得最佳性能,同时很好地利用设备的资源。
用WhileSubscribed(5000)暴露的StateFlow和用 repeatOnLifecycle(STARTED)收集的StateFlow
警告。最近添加到Data Binding的StateFlow支持使用 launchWhenCreated来收集更新,当它达到稳定时,它将开始使用 repeatOnLifecycle代替。
对于数据绑定,你应该在任何地方使用流,并简单地添加asLiveData()来将它们暴露给视图。数据绑定将在lifecycle-runtime-ktx 2.4.0稳定后进行更新。
总结
从ViewModel中暴露数据并从视图中收集数据的最好方法是。
任何其他的组合都会使上游的流保持活跃,浪费资源。
- ❌ 在lifecycleScope.launch/launchWhenX内使用WhileSubscribed和收集来表达。
- ❌ 使用 Lazily/Eagerly 暴露,并通过 repeatOnLifecycle 收集。
当然,如果你不需要Flow的全部功能......就使用LiveData。:)
感谢Manuel, Wojtek, Yigit, Alex Cook, Florina和Chris!