关于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 支持的更多信息,请查看官方文档

引用自A safer way to collect flows from Android UIs

推荐阅读更多精彩内容