从 LiveData 迁移到 Kotlin 数据流
LiveData 的历史要追溯到 2017 年。彼时,观察者模式有效简化了开发,但诸如 RxJava 一类的库对新手而言有些太过复杂。为此,架构组件团队打造了 LiveData: 一个专用于 Android 的具备自主生命周期感知能力的可观察的数据存储器类。LiveData 被有意简化设计,这使得开发者很容易上手;而对于较为复杂的交互数据流场景,建议您使用 RxJava,这样两者结合的优势就发挥出来了。
DeadData?
一种更安全的方式来从 Android 的界面中获得数据流
https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
数据流: 把简单复杂化,又把复杂变简单
缓存最新的数据 https://medium.com/androiddevelopers/livedata-with-coroutines-and-flow-part-i-reactive-uis-b20f676d25d7 启动协程 https://medium.com/androiddevelopers/livedata-with-coroutines-and-flow-part-ii-launching-coroutines-with-architecture-components-337909f37ae7 创建复杂的数据转换 https://medium.com/androiddevelopers/livedata-beyond-the-viewmodel-reactive-patterns-using-transformations-and-mediatorlivedata-fda520ba00b7
#1: 使用可变数据存储器暴露一次性操作的结果
<!-- 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
// 从挂起函数和可变状态中加载数据
init {
viewModelScope.launch {
val result = ...
_myUiState.value = result
}
}
}
class MyViewModel {
private val _myUiState = MutableStateFlow<Result<UiState>>(Result.Loading)
val myUiState: StateFlow<Result<UiState>> = _myUiState
// 从挂起函数和可变状态中加载数据
init {
viewModelScope.launch {
val result = ...
_myUiState.value = result
}
}
}
StateFlow 是 SharedFlow 的一个比较特殊的变种,而 SharedFlow 又是 Kotlin 数据流当中比较特殊的一种类型。StateFlow 与 LiveData 是最接近的,因为:
它始终是有值的。 它的值是唯一的。 它允许被多个观察者共用 (因此是共享的数据流)。 它永远只会把最新的值重现给订阅者,这与活跃观察者的数量是无关的。
StateFlow https://developer.android.google.cn/kotlin/flow/stateflow-and-sharedflow#stateflow SharedFlow https://developer.android.google.cn/kotlin/flow/stateflow-and-sharedflow#sharedflow
当暴露 UI 的状态给视图时,应该使用 StateFlow。这是一种安全和高效的观察者,专门用于容纳 UI 状态。
#2: 把一次性操作的结果暴露出来
LiveData https://developer.android.google.cn/topic/libraries/architecture/coroutines#livedata
class MyViewModel(...) : ViewModel() {
val result: LiveData<Result<UiState>> = liveData {
emit(Result.Loading)
emit(repository.fetchItem())
}
}
△ 把一次性操作的结果暴露出来 (StateFlow)
class MyViewModel(...) : ViewModel() {
val result: StateFlow<Result<UiState>> = flow {
emit(repository.fetchItem())
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000), //由于是一次性操作,也可以使用 Lazily
initialValue = Result.Loading
)
}
#3: 带参数的一次性数据加载
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)) }
}
}
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()
}
△ 带参数的一次性数据加载 (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
)
}
val result = userId.transformLatest { newUserId ->
emit(Result.LoadingData)
emit(repository.fetchItem(newUserId))
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.LoadingUser //注意此处不同的加载状态
)
#4: 观察带参数的数据流
△ 观察带参数的数据流 (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 https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flat-map-latest.html
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()
}
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
)
}
#5: 结合多种源: MediatorLiveData -> Flow.combine
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))
}
val flow1: Flow<Int> = ...
val flow2: Flow<Int> = ...
val result = combine(flow1, flow2) { a, b -> a + b }
combineTransform https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/combine-transform.html zip https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/zip.html
通过 stateIn 配置对外暴露的 StateFlow
val result: StateFlow<Result<UiState>> = someFlow
.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
@param scope 共享开始时所在的协程作用域范围
@param started 控制共享的开始和结束的策略
@param initialValue 状态流的初始值
当使用 [SharingStarted.WhileSubscribed] 并带有 `replayExpirationMillis` 参数重置状态流时,也会用到 initialValue。
Lazily: 当首个订阅者出现时开始,在 scope 指定的作用域被结束时终止。 Eagerly: 立即开始,而在 scope 指定的作用域被结束时终止。 WhileSubscribed: 这种情况有些复杂 (后文详聊)。
WhileSubscribed 策略
public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
)
超时停止
stopTimeoutMillis 控制一个以毫秒为单位的延迟值,指的是最后一个订阅者结束订阅与停止上游流的时间差。默认值是 0 (立即停止)。
添加一个 5 秒钟的延迟 https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/CoroutineLiveData.kt;l=356
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— 配置了以毫秒为单位的延迟时间,定义了从停止共享协程到重置缓存 (恢复到 stateIn 运算符中定义的初始值 initialValue) 所需要等待的时间。它的默认值是长整型的最大值 Long.MAX_VALUE (表示永远不将其重置)。如果设置为 0,可以在符合条件时立即重置缓存的数据。
从视图中观察 StateFlow
Activity.lifecycleScope.launch: 立即启动协程,并且在本 Activity 销毁时结束协程。 Fragment.lifecycleScope.launch: 立即启动协程,并且在本 Fragment 销毁时结束协程。 Fragment.viewLifecycleOwner.lifecycleScope.launch: 立即启动协程,并且在本 Fragment 中的视图生命周期结束时取消协程。
LaunchWhenStarted 和 LaunchWhenResumed
△ 使用 launch/launchWhenX 来收集数据流是不安全的
lifecycle.repeatOnLifecycle 前来救场
lifecycle-runtime-ktx 2.4.0-alpha01 https://developer.android.google.cn/jetpack/androidx/releases/lifecycle#2.4.0-alpha01
比如在某个 Fragment 的代码中:
onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
myViewModel.myUiState.collect { ... }
}
}
}
https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
△ 该 StateFlow 通过 WhileSubscribed(5000) 暴露并通过 repeatOnLifecycle(STARTED) 收集
注意: 近期在 Data Binding 中加入的 StateFlow 支持使用了 launchWhenCreated 来描述收集数据更新,并且它会在进入稳定版后转而使用 repeatOnLifecyle。 对于数据绑定,您应该在各处都使用 Kotlin 数据流并简单地加上 asLiveData() 来把数据暴露给视图。数据绑定会在 lifecycle-runtime-ktx 2.4.0 进入稳定版后更新。
近期在 Data Binding 中加入的 StateFlow 支持 https://developer.android.google.cn/topic/libraries/data-binding/observability#stateflow
总结
通过 ViewModel 暴露数据,并在视图中获取的最佳方式是:
✔️ 使用带超时参数的 WhileSubscribed 策略暴露 StateFlow。[示例 1] ✔️ 使用 repeatOnLifecycle 来收集数据更新。[示例 2]
示例 1 https://gist.github.com/JoseAlcerreca/4eb0be817d8f94880dab279d1c27a4af 示例 2 https://gist.github.com/JoseAlcerreca/6e2620b5615425a516635744ba59892e
如果采用其他方式,上游数据流会被一直保持活跃,导致资源浪费:
❌ 通过 WhileSubscribed 暴露 StateFlow,然后在 lifecycleScope.launch/launchWhenX 中收集数据更新。 ❌ 通过 Lazily/Eagerly 策略暴露 StateFlow,并在 repeatOnLifecycle 中收集数据更新。
Manuel https://medium.com/@manuelvicnt Wojtek https://medium.com/@wkalicinski Yigit https://medium.com/@yigit/ Florina https://medium.com/@florina.muntenescu Chris https://chrisbanes.medium.com/
推荐阅读