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> = 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> = 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 = …
val liveData2: LiveData = …
val result = MediatorLiveData()
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 = …
val flow2: Flow = …
val result = combine(flow1, flow2) { a, b -> a + b }
此处也可以使用 combineTransform 或者 zip 函数。
早前我们使用 stateIn
中间运算符来把普通的流转换成 StateFlow,但转换之后还需要一些配置工作。如果现在不想了解太多细节,只是想知道怎么用,那么可以使用下面的推荐配置:
val result: StateFlow<Result> = 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
)
}
这种方法会在以下场景得到体现:
-
用户将您的应用转至后台运行,5 秒钟后所有来自其他层的数据更新会停止,这样可以节省电量。
-
最新的数据仍然会被缓存,所以当用户切换回应用时,视图立即就可以得到数据进行渲染。
-
订阅将被重启,新数据会填充进来,当数据可用时更新视图。
数据重现的过期时间
如果用户离开应用太久,此时您不想让用户看到陈旧的数据,并且希望显示数据正在加载中,那么就应该在 WhileSubscribed 策略中使用 replayExpirationMillis 参数。在这种情况下此参数非常适合,由于缓存的数据都恢复成了 stateIn 中定义的初始值,因此可以有效节省内存。虽然用户切回应用时可能没那么快显示有效数据,但至少不会把过期的信息显示出来。
replayExpirationMillis
配置了以毫秒为单位的延迟时间,定义了从停止共享协程到重置缓存 (恢复到 stateIn 运算符中定义的初始值 initialValue) 所需要等待的时间。它的默认值是长整型的最大值 Long.MAX_VALUE (表示永远不将其重置)。如果设置为 0,可以在符合条件时立即重置缓存的数据。
我们此前已经谈到,ViewModel 中的 StateFlow 需要知道它们已经不再需要监听。然而,当所有的这些内容都与生命周期 (lifecycle) 结合起来,事情就没那么简单了。
要收集一个数据流,就需要用到协程。Activity 和 Fragment 提供了若干协程构建器:
-
Activity.lifecycleScope.launch : 立即启动协程,并且在本 Activity 销毁时结束协程。
-
Fragment.lifecycleScope.launch : 立即启动协程,并且在本 Fragment 销毁时结束协程。
-
Fragment.viewLifecycleOwner.lifecycleScope.launch : 立即启动协程,并且在本 Fragment 中的视图生命周期结束时取消协程。
LaunchWhenStarted 和 LaunchWhenResumed
对于一个状态 X,有专门的 launch 方法称为 launchWhenX。它会在 lifecycleOwner 进入 X 状态之前一直等待,又在离开 X 状态时挂起协程。对此,需要注意对应的协程只有在它们的生命周期所有者被销毁时才会被取消。
△ 使用 launch/launchWhenX 来收集数据流是不安全的
当应用在后台运行时接收数据更新可能会引起应用崩溃,但这种情况可以通过将视图的数据流收集操作挂起来解决。然而,上游数据流会在应用后台运行期间保持活跃,因此可能浪费一定的资源。
这么说来,目前我们对 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 状态时保持收集,最终在 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
进入稳定版后更新。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
点击这里免费领取吧!
同时减轻大家的负担。**
[外链图片转存中…(img-0GdJopJH-1710502016361)]
[外链图片转存中…(img-EVX8K0OP-1710502016362)]
[外链图片转存中…(img-nDeaFPQI-1710502016362)]
[外链图片转存中…(img-tpidJOuD-1710502016363)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-dkvsciTA-1710502016363)]
[外链图片转存中…(img-T5KgFsbU-1710502016364)]
[外链图片转存中…(img-P3bDZm2s-1710502016364)]