原文:https://medium.com/androiddevelopers/migrating-from-livedata-to-kotlins-flow-379292f419fb
LiveData 是我们在 2017 年需要的东西。观察者模式让我们的生活更轻松,但 RxJava 等选项在当时对于初学者来说太复杂了。 架构组件团队创建了 LiveData:一个非常固执的可观察数据持有者类,专为 Android 设计。 它保持简单以使其易于上手,并且建议将 RxJava 用于更复杂的反应式流案例,利用两者之间的集成。
DeadData?
LiveData 仍然是我们为 Java 开发人员、初学者和简单情况提供的解决方案。 对于其余的,一个不错的选择是转向 Kotlin Flows。 Flows 仍然有一个陡峭的学习曲线,但它们是 Kotlin 语言的一部分,由 Jetbrains 提供支持;并且 Compose 即将到来,它非常适合响应式模型。
我们一直在谈论使用 Flows 来连接应用程序的不同部分,除视图和 ViewModel 外。 现在我们有了一种更安全的方法来从 Android UI 收集流,我们可以创建一个完整的迁移指南。
在这篇文章中,您将学习如何将 Flows 暴露给一个视图,如何收集它们,以及如何对其进行微调以满足特定需求。
Flow:简单的事情更难,复杂的事情更容易
LiveData 做了一件事并且做得很好:它在缓存最新值和了解 Android 的生命周期的同时暴露数据。 后来我们了解到它也可以启动协程并创建复杂的转换,但这有点复杂。
让我们看看一些 LiveData 模式和它们的 Flow 等价模式:
#1:使用可变数据持有者暴露一次性操作的结果
这是经典模式,您可以使用协程的结果来改变状态持有者:
使用可变数据持有者 (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
}
}
}
为了对 Flows 做同样的事情,我们使用 (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:
- 它总是有值的。
- 它只有一个值。
- 它支持多个观察者(因此 Flow 是共享的)。
- 它总是重播订阅的最新值,与活跃观察者的数量无关。
向视图暴露 UI 状态时,请使用 StateFlow。 它是一个安全高效的观察者,旨在持有 UI 状态。
#2:暴露一次性操作的结果
这与前面的代码片段等效,暴露了没有可变支持属性的协程调用的结果。
对于 LiveData,我们为此使用了 liveData 协程构建器:
暴露一次性操作的结果 (LiveData)
class MyViewModel(...) : ViewModel() {
val result: LiveData<Result<UiState>> = liveData {
emit(Result.Loading)
emit(repository.fetchItem())
}
}
由于状态持有者总是有一个值,因此最好将我们的 UI 状态包装在某种支持加载、成功和错误等状态的 Result 类中。
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 转换为 StateFlow 的 Flow 运算符。 让我们暂时相信这些参数,因为我们稍后需要更多的复杂性来正确解释它。
#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()
}
使用 Flows 执行此操作看起来非常相似:
带参数的一次性数据加载(StateFlow)
class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }
val result: StateFlow<Result<Item>> = userId.mapLatest { newUserId ->
repository.fetchItem(newUserId)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
}
请注意,如果您需要更大的灵活性,您还可以使用 transformLatest 并显式 emit 发出元素:
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:观察带参数的数据流
现在让我们使这个例子更具响应性。 数据不是获取的,而是观察到的,因此我们将数据源中的更改自动传播到 UI。
继续我们的例子:我们没有在数据源上调用 fetchItem,而是使用一个假设的 observeItem 运算符,它返回一个 Flow。
使用 LiveData,您可以将 flow 转换为 LiveData 并使用 emitSource 发出所有更新:
观察带有参数的流 (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.combine
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 }
您还可以使用 combineTransform 函数或 zip。
配置暴露的 StateFlow(stateIn 运算符)
我们之前使用 stateIn 将常规流转换为 StateFlow,但它需要一些配置。 如果你现在不想详细介绍,只需要复制粘贴,我推荐这种组合:
val result: StateFlow<Result<UiState>> = someFlow
.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
但是,如果您不确定这个看似随机的 5 秒 started 参数,请继续阅读。
stateIn 有 3 个参数(来自文档):
@param scope 开始共享的协程范围。
@param started 控制何时开始和停止共享的策略。
@param initialValue 状态流的初始值。
当使用带有replayExpirationMillis
参数的 [SharingStarted.WhileSubscribed] 策略重置状态流时,也会使用此值。
started 可以采用 3 个值:
- Lazily:在第一个订阅者出现时开始,在 scope 范围取消时停止。
- Eagerly:立即开始并在 scope 范围取消时停止。
- WhileSubscribed:这很复杂。
对于一次性操作,您可以使用 Lazily 或 Eagerly。 但是,如果您正在观察其他 flow,则应该使用 WhileSubscribed 来进行小而重要的优化,如下所述。
WhileSubscribed 策略
WhileSubscribed 在没有收集器时取消上游流。 使用 stateIn 创建的 StateFlow 向视图暴露数据,但它也在观察来自其他层或应用程序(上游)的流。 保持这些流处于活跃状态可能会导致资源浪费,例如,如果它们继续从其他来源(例如数据库连接、硬件传感器等)读取数据。当您的应用程序进入后台时,您应该成为一个好公民并停止这些协程。
WhileSubscribed 有两个参数:
public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
)
Stop timeout
从它的文档:
stopTimeoutMillis 配置最后一个订阅者消失和上游流停止之间的延迟(以毫秒为单位)。 它默认为零(立即停止)。
这很有用,因为如果视图停止监听几分之一秒,您不想取消上游流。 这一直发生——例如,当用户旋转设备然后视图被快速连续地销毁并重新创建时。
转载请说明出处:https://blog.csdn.net/hegan2010/article/details/121379052
liveData 协程构建器中的解决方案是添加 5 秒的延迟,如果没有订阅者,协程将在此后停止。 WhileSubscribed(5000) 正是这样做的:
class MyViewModel(...) : ViewModel() {
val result = userId.mapLatest { newUserId ->
repository.observeItem(newUserId)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
}
这种方法检查所有框:
- 当用户将您的应用程序送到后台时,来自其他层的更新将在 5 秒后停止,从而节省电量。
- 最新的值仍然会被缓存,这样当用户回到它时,视图会立即有一些数据。
- 订阅重新启动,新值将到达,可用时刷新屏幕。
Replay expiration
如果您不希望用户在他们离开太久后看到陈旧数据,并且您更喜欢显示加载屏幕,请查看 WhileSubscribed 中的 replayExpirationMillis 参数。 在这种情况下它非常方便,并且还节省了一些内存,因为缓存的值恢复到 stateIn 中定义的初始值。 回到应用程序不会那么快,但你不会显示旧数据。
replayExpirationMillis 配置共享协程停止和重放缓存重置之间的延迟(以毫秒为单位)(这使得 shareIn 运算符的缓存为空,并将缓存值重置为 stateIn 运算符的 initialValue 初始值)。 它默认为 Long.MAX_VALUE(永远保持重放缓存,从不重置缓冲区)。 使用零值立即使缓存过期。
从视图中观察 StateFlow
到目前为止,我们已经看到,让 ViewModel 中的 StateFlows 知道它们不再监听对于视图来说非常重要。 然而,对于与生命周期相关的一切,事情并没有那么简单。
为了收集流,您需要一个协程。 Activity 和 Fragment 提供了一堆协程构建器:
- Activity.lifecycleScope.launch:立即启动协程,Activity 销毁时取消。
- Fragment.lifecycleScope.launch:立即启动协程,并在 Activity 销毁时取消协程。
- Fragment.viewLifecycleOwner.lifecycleScope.launch:立即启动协程,并在 Fragment 的视图生命周期被销毁时取消协程。 如果您正在修改 UI,则应该使用视图生命周期。
LaunchWhenStarted, launchWhenResumed…
称为 launchWhenX 的 launch 的特殊版本将等到 lifecycleOwner 处于 X 状态并在 lifecycleOwner 低于 X 状态时暂停协程。 需要注意的是,在其生命周期所有者被销毁之前,它们不会取消协程。
使用 launch/launchWhenX 收集 Flows 是不安全的
在应用程序处于后台时接收更新可能会导致崩溃,这可以通过暂停视图中的收集来解决。 但是,当应用程序在后台时,上游流会保持活动状态,这可能会浪费资源。
这意味着到目前为止我们为配置 StateFlow 所做的一切都将毫无用处; 然而,镇上有一个新的 API。
Lifecycle.repeatOnLifecycle 来救援
这个新的协程构建器(从 lifecycle-runtime-ktx 2.4.0-alpha01 中可用)正是我们所需要的:它在特定状态下启动协程,并在生命周期所有者低于它时停止它们。
不一样的流收集方式
例如,在一个 Fragment 中:
onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
myViewModel.myUiState.collect { ... }
}
}
}
这将在 Fragment 的视图 STARTED 时开始收集,将继续经过 RESUMED,并在返回到 STOPPED 时停止。 在从 Android UI 收集流的更安全方式中阅读所有相关信息。
将 repeatOnLifecycle API 与上述 StateFlow 指南相结合,可以在充分利用设备资源的同时获得最佳性能。
StateFlow 使用 WhileSubscribed(5000) 暴露并使用 repeatOnLifecycle(STARTED) 收集
警告:最近添加到数据绑定的 StateFlow 支持使用 launchWhenCreated 来收集更新,当它达到稳定时它将开始使用 repeatOnLifecycle。
对于数据绑定,您应该在各个地方使用 Flows 并简单地添加 asLiveData() 以将它们暴露给视图。 数据绑定将在 Lifecycle-runtime-ktx 2.4.0 稳定后更新。
总结
从 ViewModel 暴露数据并从视图中收集数据的最佳方法是:
任何其他组合都会使上游 Flows 保持活跃状态,从而浪费资源:
- ❌ 使用 WhileSubscribed 暴露并在 lifecycleScope.launch/launchWhenX 内收集。
- ❌ 使用 Lazily/Eagerly 暴露并使用 repeatOnLifecycle 收集。
当然,如果您不需要 Flow 的全部功能……只需使用 LiveData。 😃
感谢 Manuel、Wojtek、Yigit、Alex Cook、Florina 和 Chris!