从Android UI收集流的更安全方法

在安卓应用中,通常从UI层收集Kotlin flows以显示屏幕上的数据更新。但是,为了确保不做过多的工作、浪费资源(包括CPU和内存)或在视图转到后台时泄漏数据,您需要收集这些flows

在本文中,您将学习如何使用Lifecycle.repeatOnLifecycleFlow.flowWithLifecycle API来保护资源,以及为什么它们是UI层flow收集的良好默认值。

浪费资源 建议从应用程序层次结构的较低层公开Flow<T> API,而不考虑flow生产者实现细节。但是,您还应该安全地收集它们。

使用通道支持的cold flow或使用缓冲器(如bufferconflateflowOnshareIn)的运算符不安全,无法与某些现有的API(例如CoroutineScope.launchFlow<T>.launchInLifecycleCoroutineScope.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.launchlaunchIn 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可防止因资源浪费和应用程序崩溃而停止和重新启动流程收集,当生命周期进入和退出目标状态时。

图片
使用和不使用repeatOnLifecycle API的区别

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,而不影响下游。类似于 flowOnFlow.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.repeatOnLifecycleFlow.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.repeatOnLifecycleFlow.flowWithLifecycle API以安全地从Android的UI层中收集flows。

声明:文中观点不代表本站立场。本文传送门:https://eyangzhen.com/47700.html

联系我们
联系我们
分享本页
返回顶部