关于Android UI State Flow更安全的收集用法
在 Android 应用程序中,Kotlin 流通常从 UI 层收集以在屏幕上显示数据更新。 但是,您希望收集这些流,以确保在视图转到后台时不会做多余的工作、浪费资源(CPU 和内存)或泄漏数据。
在本文中,您将了解 Lifecycle.repeatOnLifecycle 和 Flow.flowWithLifecycle API 如何保护您免于浪费资源,以及为什么它们是 UI 层中用于流收集的良好默认设置。
资源浪费
建议从应用程序层次结构的较低层公开 Flow<T> API,而不管流生成器的实现细节如何。 但是,您也应该安全地收集它们。
由通道支持或使用带有缓冲区(例如 buffer、conflate、flowOn 或 shareIn)的运算符的冷流不安全地使用某些现有 API(例如 CoroutineScope.launch、Flow<T>.launchIn 或 LifecycleCoroutineScope.launchWhenX)收集 ,除非你在活动进入后台时手动取消启动协程的Job。 这些 API 将保持底层流生成器处于活动状态,同时在后台将项目发送到缓冲区中,从而浪费资源。
注意:冷流是一种在新订阅者收集时按需执行生产者代码块的流。
例如,考虑这个使用 callbackFlow 发出位置更新的流:
// Implementation of a cold flow backed by a Channel that sends Location updates
fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
try { offer(result.lastLocation) } catch(e: Exception) {}
}
}
requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
.addOnFailureListener { e ->
close(e) // in case of exception, close the Flow
}
// clean up when Flow collection ends
awaitClose {
removeLocationUpdates(callback)
}
}
注意:在内部,callbackFlow 使用一个通道,它在概念上非常类似于阻塞队列,并且默认容量为 64 个元素。
使用上述任何 API 从 UI 层收集此流,即使视图未在 UI 中显示它们,也会保持流发射位置! 请参阅下面的示例:
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Collects from the flow when the View is at least STARTED and
// SUSPENDS the collection when the lifecycle is STOPPED.
// Collecting the flow cancels when the View is DESTROYED.
lifecycleScope.launchWhenStarted {
locationProvider.locationFlow().collect {
// New location! Update the map
}
}
// Same issue with:
// - lifecycleScope.launch { /* Collect from locationFlow() here */ }
// - locationProvider.locationFlow().onEach { /* ... */ }.launchIn(lifecycleScope)
}
}
LifecycleScope.launchWhenStarted 暂停协程的执行。 新位置不会被处理,但是 callbackFlow 生产者会继续发送位置。 使用lifecycleScope.launch 或launchIn API 更加危险,因为即使视图在后台,它也会不断消耗位置! 这可能会使您的应用程序崩溃。
要通过这些 API 解决这个问题,您需要在视图转到后台时手动取消收集以取消 callbackFlow 并避免位置提供者发出项目并浪费资源。 例如,您可以执行以下操作:
class LocationActivity : AppCompatActivity() {
// Coroutine listening for Locations
private var locationUpdatesJob: Job? = null
override fun onStart() {
super.onStart()
locationUpdatesJob = lifecycleScope.launch {
locationProvider.locationFlow().collect {
// New location! Update the map
}
}
}
override fun onStop() {
// Stop collecting when the View goes to the background
locationUpdatesJob?.cancel()
super.onStop()
}
}
这是一个很好的解决方案,但这是样板文件,朋友们! 如果 Android 开发人员有一个普遍的真理,那就是我们绝对讨厌编写样板代码。 不必编写样板代码的最大好处之一是代码越少,出错的机会就越少!
Lifecycle.repeatOnLifecycle
现在我们知道问题出在哪里,是时候想出一个解决方案了。 解决方案需要 1) 简单,2) 友好或易于记忆/理解,更重要的是 3) 安全! 无论流程实现细节如何,它都应该适用于所有用例。
不用多说,您应该使用的 API 是 Lifecycle.repeatOnLifecycle 可用的 Lifecycle-runtime-ktx 库。
注意:这些 API 在 lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01 库或更高版本中可用。
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create a new coroutine since repeatOnLifecycle is a suspend function
lifecycleScope.launch {
// The block passed to repeatOnLifecycle is executed when the lifecycle
// is at least STARTED and is cancelled when the lifecycle is STOPPED.
// It automatically restarts the block when the lifecycle is STARTED again.
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Safely collect from locationFlow when the lifecycle is STARTED
// and stops collection when the lifecycle is STOPPED
locationProvider.locationFlow().collect {
// New location! Update the map
}
}
}
}
}
repeatOnLifecycle 是一个挂起函数,它以 Lifecycle.State 作为参数,用于在生命周期达到该状态时自动创建和启动一个新的协程,并将块传递给它,并在生命周期达到该状态时取消正在执行块的正在进行的协程 低于状态。
repeatOnLifecycle 是一个挂起函数,它以 Lifecycle.State 作为参数,用于在生命周期达到该状态时自动创建和启动一个新的协程,并将块传递给它,并在生命周期低于状态该状态时取消正在执行的协程 。
这避免了任何样板代码,因为在不再需要协程时取消协程的相关代码是由 repeatOnLifecycle 自动完成的。 如您所料,建议在活动的 onCreate 或片段的 onViewCreated 方法中调用此 API 以避免意外行为。 请参阅以下使用片段的示例:
class LocationFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// ...
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
locationProvider.locationFlow().collect {
// New location! Update the map
}
}
}
}
}
重要提示:Fragment应始终使用 viewLifecycleOwner 来触发 UI 更新。 但是,有时可能没有视图的 DialogFragments 并非如此。 对于 DialogFragments,您可以使用lifecycleOwner。
Note: These APIs are available in the
*lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01*
library or later.
回到开头,直接从以生命周期范围.launch 启动的协程收集 locationFlow 是危险的,因为即使 View 在后台,收集也会继续发生。
repeatOnLifecycle 可防止您浪费资源和应用程序崩溃,因为它会在生命周期移入和移出目标状态时停止并重新启动流收集。
使用和不使用 repeatOnLifecycle API 的区别当您只有一个流要收集时,您也可以使用 Flow.flowWithLifecycle 运算符。 该 API 在底层使用了 repeatOnLifecycle API,并在 Lifecycle 移入和移出目标状态时发出项目或取消底层生产者。
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Listen to one flow in a lifecycle-aware manner using flowWithLifecycle
lifecycleScope.launch {
locationProvider.locationFlow()
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect {
// New location! Update the map
}
}
// Listen to multiple flows
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// As collect is a suspend function, if you want to collect
// multiple flows in parallel, you need to do so in
// different coroutines
launch {
flow1.collect { /* Do something */ }
}
launch {
flow2.collect { /* Do something */ }
}
}
}
}
}
注意:此 API 名称以 Flow.flowOn(CoroutineContext) 运算符为先例,因为 Flow.flowWithLifecycle 更改了用于收集上游流的 CoroutineContext,同时不影响下游。 此外,类似于 flowOn,Flow.flowWithLifecycle 添加了一个缓冲区,以防消费者跟不上生产者。 这是因为它的实现使用了 callbackFlow。
配置底层生产者
即使您使用这些 API,也要注意可能浪费资源的热流,即使它们没有被任何人收集! 它们有一些有效的用例,但请记住这一点并在需要时记录下来。 让底层流生成器在后台处于活动状态,即使浪费资源,对某些用例也是有益的:您可以立即获得可用的新数据,而不是赶上并暂时显示陈旧数据。 根据用例,决定生产者是否需要始终处于活动状态。
MutableStateFlow 和 MutableSharedFlow API 公开了一个 subscriptionCount 字段,您可以使用该字段在 subscriptionCount 为零时停止底层生产者。 默认情况下,只要持有流实例的对象在内存中,它们就会使生产者保持活动状态。 但是,有一些有效的用例,例如,使用 StateFlow 从 ViewModel 向 UI 公开 UiState。 没关系! 此用例要求 ViewModel 始终向 View 提供最新的 UI 状态。
同样, Flow.stateIn 和 Flow.shareIn 运算符可以为此配置共享启动策略。 WhileSubscribed() 将在没有活动观察者时停止底层生产者! 相反,只要他们使用的 CoroutineScope 处于活动状态,Eagerly 或 Lazily 就会使底层生产者保持活动状态。
Note: The APIs shown in this article are a good default to collect flows from the UI and should be used regardless of the flow implementation detail. These APIs do what they need to do: stop collecting if the UI isn’t visible on screen. It’s up to the flow implementation if it should be always active or not.
与 LiveData 的比较
您可能已经注意到这个 API 的行为与 LiveData 类似,这是真的! LiveData 知道 Lifecycle,它的重启行为使其非常适合从 UI 观察数据流。 Lifecycle.repeatOnLifecycle 和 Flow.flowWithLifecycle API 也是如此!
使用这些 API 收集流是纯 Kotlin 应用程序中 LiveData 的自然替代品。 如果您使用这些 API 进行流收集,LiveData 不会比协程和流提供任何好处。 更重要的是,流更加灵活,因为它们可以从任何 Dispatcher 收集,并且可以由所有操作员提供支持。 与 LiveData 不同,LiveData 的可用运算符有限,并且始终从 UI 线程观察其值。
数据绑定中的 StateFlow 支持
另一方面,您可能使用 LiveData 的原因之一是数据绑定支持它。 那么,StateFlow 也是如此! 有关数据绑定中 StateFlow 支持的更多信息,请查看官方文档。