从Android UI收集流的更安全方法
在安卓应用中,通常从UI层收集Kotlin flows
以显示屏幕上的数据更新。但是,为了确保不做过多的工作、浪费资源(包括CPU和内存)或在视图转到后台时泄漏数据,您需要收集这些flows
。
在本文中,您将学习如何使用Lifecycle.repeatOnLifecycle
和Flow.flowWithLifecycle
API来保护资源,以及为什么它们是UI层flow
收集的良好默认值。
浪费资源
建议从应用程序层次结构的较低层公开Flow<T>
API,而不考虑flow
生产者实现细节。但是,您还应该安全地收集它们。
使用通道支持的cold flow
或使用缓冲器(如buffer
、conflate
、flowOn
或shareIn
)的运算符不安全,无法与某些现有的API(例如CoroutineScope.launch
、Flow<T>.launchIn
或LifecycleCoroutineScope.launchWhenX
)一起收集,除非当活动转到后台时手动取消启动协程的Job
。这些API将保持底层flow
的生产者活动状态,同时在后台向缓冲区发出项,从而浪费资源。
注:cold flow
是一种类型的flow
,当新的订阅者收集时,将按需执行代码块。
例如,考虑使用callbackFlow
发出位置更新的以下flow
:
// 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层收集这个flow
,即使视图没有在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-runtime-ktx库2.4.0或更高版本才能使用。
看一看下面的代码:
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.
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
会自动执行取消协程的相关代码。正如你所猜想的那样,建议在activity的onCreate
或fragment的onViewCreated
方法中调用此API,以避免意外行为。请参考下面使用fragment的示例:
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
}
}
}
}
}
重要提醒:在片段中触发UI更新时应始终使用viewLifecycleOwner
,但DialogFragments
有时可能不存在View。对于DialogFragments
,您可使用lifecycleOwner
。
请注意:这些API在androidx.lifecycle:lifecycle-runtime-ktx:2.4.0库及更高版本中提供。
实质是:repeatOnLifecycle
将挂起调用的协程,在生命周期的进入和离开目标状态时重新启动块的新协程,并在生命周期销毁时恢复调用协程。最后一点非常重要:只有在生命周期销毁时,调用repeatOnLifecycle
的协程才会恢复执行。
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create a coroutine
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
// Repeat when the lifecycle is RESUMED, cancel when PAUSED
}
// `lifecycle` is DESTROYED when the coroutine resumes. repeatOnLifecycle
// suspends the execution of the coroutine until the lifecycle is DESTROYED.
}
}
}
视觉图表
回到起点,通过使用lifecycleScope.launch
启动的协程直接收集locationFlow
是危险的,因为即使View在后台运行时,收集仍将继续发生。
repeatOnLifecycle
可防止因资源浪费和应用程序崩溃而停止和重新启动流程收集,当生命周期进入和退出目标状态时。
Flow.flowWithLifecycle
当您只有一个要收集的Flow时,也可以使用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 {
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()
将在没有活动观察者时停止底层生产者!相反,Eagerly 或 Lazily 将使底层生产者保持处于活动状态,只要它们使用的CoroutineScope
处于活动状态。
注意:本文展示的 API 是从 UI 收集流的良好默认值,无论流实现细节如何,都应该使用这些 API。这些 API 做他们应该做的事情:如果 UI 不在屏幕上可见,则停止收集。如果应始终处于活动状态,则由流实现决定。
在 Jetpack Compose 中进行安全的 Flow 收集
如果您正在使用 Jetpack Compose 构建 Android 应用程序,请使用 collectAsStateWithLifecycle
API 以生命周期感知的方式从 UI 中收集流。
collectAsStateWithLifecycle
是一个可组合函数,它以生命周期感知的方式从流中收集值,并将最新值表示为 Compose State。每当发生新的流发射时,这个 State 对象的值就会更新。这会导致所有 Composition 中 State.value
的使用都被重新编排。
默认情况下,collectAsStateWithLifecycle
使用Lifecycle.State.STARTED
启动和停止从流中收集值。这发生在生命周期移动进入和退出目标状态时。此生命周期状态是您可以在 minActiveState
参数中配置的。
以下代码片段展示了此 API 的实际运用:
@Composable
fun LocationUI(locationFlow: Flow<Location>) {
val location by locationFlow.collectAsStateWithLifecycle()
// Current location, do something with it
}
与LiveData的比较
您可能已经注意到,此API的行为类似于LiveData,这是正确的!LiveData了解Lifecycle,其重新启动行为使其非常适合从UI观察数据流。Lifecycle.repeatOnLifecycle
和Flow.flowWithLifecycle
的情况也是如此!
在仅限Kotlin的应用程序中使用这些API收集flows是LiveData
的自然替代品。如果您使用这些API来收集流,则LiveData没有比协程和flow更多的优势。此外,flows更灵活,因为它们可以从任何调度程序中收集,并且可以使用所有其操作符进行运行。与LiveData相反,LiveData的可用运算符有限,并且其值始终从UI线程观察。
在数据绑定中支持StateFlow
另一方面,您可能正在使用LiveData的原因是它受数据绑定的支持。Well,StateFlow也是如此!有关StateFlow在数据绑定中的支持的更多信息,请查看官方文档。
https://developer.android.com/topic/libraries/data-binding/observability#stateflow
使用Lifecycle.repeatOnLifecycle
或Flow.flowWithLifecycle
API以安全地从Android的UI层中收集flows。