LiveData 的历史要追溯到 2017 年。彼时,观察者模式有效简化了开发,但诸如 RxJava 一类的库对新手而言有些太过复杂。为此,架构组件团队打造了 LiveData: 一个专用于 Android 的具有自主生命周期感知能力的可观察的数据存储器类。LiveData 被有意简化设计,这使得开发者很容易上手;而对于较为复杂的交互数据流场景,建议您使用 RxJava,这样二者结合的优点就发挥出来了。html
LiveData 对于 Java 开发者、初学者或是一些简单场景而言还是可行的解决方案。而对于一些其余的场景,更好的选择是使用 Kotlin 数据流 (Kotlin Flow)。虽然说数据流 (相较 LiveData) 有更陡峭的学习曲线,但因为它是 JetBrains 力挺的 Kotlin 语言的一部分,且 Jetpack Compose 正式版即将发布,故二者配合更能发挥出 Kotlin 数据流中响应式模型的潜力。java
此前一段时间,咱们探讨了 如何使用 Kotlin 数据流 来链接您的应用当中除了视图和 View Model 之外的其余部分。而如今咱们有了 一种更安全的方式来从 Android 的界面中得到数据流,已经能够创做一份完整的迁移指南了。react
在这篇文章中,您将学到如何把数据流暴露给视图、如何收集数据流,以及如何经过调优来适应不一样的需求。android
LiveData 就作了一件事而且作得不错: 它在 缓存最新的数据 和感知 Android 中的生命周期的同时将数据暴露了出来。稍后咱们会了解到 LiveData 还能够 启动协程 和 建立复杂的数据转换,这可能会须要花点时间。git
接下来咱们一块儿比较 LiveData 和 Kotlin 数据流中相对应的写法吧:github
#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
// 从挂起函数和可变状态中加载数据
init {
viewModelScope.launch {
val result = ...
_myUiState.value = result
}
}
}
复制代码
若是要在 Kotlin 数据流中执行相同的操做,咱们须要使用 (可变的) StateFlow (状态容器式可观察数据流):markdown
△ 使用可变数据存储器 (StateFlow) 暴露一次性操做的结果
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 是最接近的,由于:
当暴露 UI 的状态给视图时,应该使用 StateFlow。这是一种安全和高效的观察者,专门用于容纳 UI 状态。
#2: 把一次性操做的结果暴露出来
这个例子与上面代码片断的效果一致,只是这里暴露协程调用的结果而无需使用可变属性。
若是使用 LiveData,咱们须要使用 LiveData 协程构建器:
△ 把一次性操做的结果暴露出来 (LiveData)
class MyViewModel(...) : ViewModel() {
val result: LiveData<Result<UiState>> = liveData {
emit(Result.Loading)
emit(repository.fetchItem())
}
}
复制代码
因为状态容器老是有值的,那么咱们就能够经过某种 Result 类来把 UI 状态封装起来,好比加载中、成功、错误等状态。
与之对应的数据流方式则须要您多作一点配置:
△ 把一次性操做的结果暴露出来 (StateFlow)
class MyViewModel(...) : ViewModel() {
val result: StateFlow<Result<UiState>> = flow {
emit(repository.fetchItem())
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000), //因为是一次性操做,也可使用 Lazily
initialValue = Result.Loading
)
}
复制代码
stateIn 是专门将数据流转换为 StateFlow 的运算符。因为须要经过更复杂的示例才能更好地解释它,因此这里暂且把这些参数放在一边。
#3: 带参数的一次性数据加载
比方说您想要加载一些依赖用户 ID 的数据,而信息来自一个提供数据流的 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 变化时执行。
如非必需要将 userId
做为 LiveData 使用,那么更好的方案是将流式数据和 Flow 结合,并将最终的结果 (result) 转化为 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()
}
复制代码
若是改用 Kotlin Flow 来编写,代码其实似曾相识:
△ 带参数的一次性数据加载 (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 //注意此处不一样的加载状态
)
复制代码
#4: 观察带参数的数据流
接下来咱们让刚才的案例变得更具交互性。数据再也不被读取,而是被观察,所以咱们对数据源的改动会直接被传递到 UI 界面中。
继续刚才的例子: 咱们再也不对源数据调用 fetchItem 方法,而是经过假定的 observeItem 方法获取一个 Kotlin 数据流。
若使用 LiveData,能够将数据流转换为 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()
}
复制代码
使用 Kotlin 数据流的实现方式很是类似,可是省下了 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
)
}
复制代码
每当用户实例变化,或者是存储区 (repository) 中用户的数据发生变化时,上面代码中暴露出来的 StateFlow 都会收到相应的更新信息。
#5: 结合多种源: MediatorLiveData -> Flow.combine
MediatorLiveData 容许您观察一个或多个数据源的变化状况,并根据获得的新数据进行相应的操做。一般能够按照下面的方式更新 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))
}
复制代码
一样的功能使用 Kotlin 数据流来操做会更加直接:
val flow1: Flow<Int> = ...
val flow2: Flow<Int> = ...
val result = combine(flow1, flow2) { a, b -> a + b }
复制代码
此处也可使用 combineTransform 或者 zip 函数。
早前咱们使用 stateIn
中间运算符来把普通的流转换成 StateFlow,但转换以后还须要一些配置工做。若是如今不想了解太多细节,只是想知道怎么用,那么可使用下面的推荐配置:
val result: StateFlow<Result<UiState>> = someFlow
.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
复制代码
不过,若是您想知道为何会使用这个看似随机的 5 秒的 started 参数,请继续往下读。
根据文档,stateIn
有三个参数:
@param scope 共享开始时所在的协程做用域范围
@param started 控制共享的开始和结束的策略
@param initialValue 状态流的初始值
当使用 [SharingStarted.WhileSubscribed] 并带有 `replayExpirationMillis` 参数重置状态流时,也会用到 initialValue。
复制代码
started
接受如下的三个值:
Lazily
: 当首个订阅者出现时开始,在 scope
指定的做用域被结束时终止。Eagerly
: 当即开始,而在 scope
指定的做用域被结束时终止。WhileSubscribed
: 这种状况有些复杂 (后文详聊)。对于那些只执行一次的操做,您可使用 Lazily 或者 Eagerly。然而,若是您须要观察其余的流,就应该使用 WhileSubscribed 来实现细微但又重要的优化工做,参见后文的解答。
WhileSubscribed 策略会在没有收集器的状况下取消上游数据流。经过 stateIn 运算符建立的 StateFlow 会把数据暴露给视图 (View),同时也会观察来自其余层级或者是上游应用的数据流。让这些流持续活跃可能会引发没必要要的资源浪费,例如一直经过从数据库链接、硬件传感器中读取数据等等。当您的应用转而在后台运行时,您应当保持克制并停止这些协程。
WhileSubscribed
接受两个参数:
public fun WhileSubscribed( stopTimeoutMillis: Long = 0, replayExpirationMillis: Long = Long.MAX_VALUE )
复制代码
超时中止
根据其文档:
stopTimeoutMillis 控制一个以毫秒为单位的延迟值,指的是最后一个订阅者结束订阅与中止上游流的时间差。默认值是 0 (当即中止)。
这个值很是有用,由于您可能并不想由于视图有几秒钟再也不监听就结束上游流。这种状况很是常见——好比当用户旋转设备时,原来的视图会先被销毁,而后数秒钟内重建。
liveData 协程构建器所使用的方法是 添加一个 5 秒钟的延迟,即若是等待 5 秒后仍然没有订阅者存在就终止协程。前文代码中的 WhileSubscribed (5000) 正是实现这样的功能:
class MyViewModel(...) : ViewModel() {
val result = userId.mapLatest { newUserId ->
repository.observeItem(newUserId)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
}
复制代码
这种方法会在如下场景获得体现:
数据重现的过时时间
若是用户离开应用过久,此时您不想让用户看到陈旧的数据,而且但愿显示数据正在加载中,那么就应该在 WhileSubscribed 策略中使用 replayExpirationMillis 参数。在这种状况下此参数很是适合,因为缓存的数据都恢复成了 stateIn 中定义的初始值,所以能够有效节省内存。虽然用户切回应用时可能没那么快显示有效数据,但至少不会把过时的信息显示出来。
replayExpirationMillis
配置了以毫秒为单位的延迟时间,定义了从中止共享协程到重置缓存 (恢复到 stateIn 运算符中定义的初始值 initialValue) 所须要等待的时间。它的默认值是长整型的最大值 Long.MAX_VALUE (表示永远不将其重置)。若是设置为 0,能够在符合条件时当即重置缓存的数据。
咱们此前已经谈到,ViewModel 中的 StateFlow 须要知道它们已经再也不须要监听。然而,当全部的这些内容都与生命周期 (lifecycle) 结合起来,事情就没那么简单了。
要收集一个数据流,就须要用到协程。Activity 和 Fragment 提供了若干协程构建器:
对于一个状态 X,有专门的 launch 方法称为 launchWhenX。它会在 lifecycleOwner 进入 X 状态以前一直等待,又在离开 X 状态时挂起协程。对此,须要注意对应的协程只有在它们的生命周期全部者被销毁时才会被取消。
△ 使用 launch/launchWhenX 来收集数据流是不安全的
当应用在后台运行时接收数据更新可能会引发应用崩溃,但这种状况能够经过将视图的数据流收集操做挂起来解决。然而,上游数据流会在应用后台运行期间保持活跃,所以可能浪费必定的资源。
这么说来,目前咱们对 StateFlow 所进行的配置都是无用功;不过,如今有了一个新的 API。
这个新的协程构建器 (自 lifecycle-runtime-ktx 2.4.0-alpha01 后可用) 刚好能知足咱们的须要: 在某个特定的状态知足时启动协程,而且在生命周期全部者退出该状态时中止协程。
△ 不一样数据流收集方法的比较
好比在某个 Fragment 的代码中:
onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
myViewModel.myUiState.collect { ... }
}
}
}
复制代码
当这个 Fragment 处于 STARTED 状态时会开始收集流,而且在 RESUMED 状态时保持收集,最终在 Fragment 进入 STOPPED 状态时结束收集过程。如需获取更多信息,请参阅: 使用更为安全的方式收集 Android UI 数据流。
结合使用 repeatOnLifecycle API 和上面的 StateFlow 示例能够帮助您的应用妥善利用设备资源的同时,发挥最佳性能。
△ 该 StateFlow 经过 WhileSubscribed(5000) 暴露并经过 repeatOnLifecycle(STARTED) 收集
注意: 近期在 Data Binding 中加入的 StateFlow 支持 使用了
launchWhenCreated
来描述收集数据更新,而且它会在进入稳定版后转而使用repeatOnLifecyle
。对于数据绑定,您应该在各处都使用 Kotlin 数据流并简单地加上
asLiveData()
来把数据暴露给视图。数据绑定会在lifecycle-runtime-ktx 2.4.0
进入稳定版后更新。
经过 ViewModel 暴露数据,并在视图中获取的最佳方式是:
若是采用其余方式,上游数据流会被一直保持活跃,致使资源浪费:
WhileSubscribed
暴露 StateFlow,而后在 lifecycleScope.launch/launchWhenX
中收集数据更新。Lazily/Eagerly
策略暴露 StateFlow,并在 repeatOnLifecycle
中收集数据更新。固然,若是您并不须要使用到 Kotlin 数据流的强大功能,就用 LiveData 好了 :)