关于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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容