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

news2024/11/25 11:40:22

logo

从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,也要注意可能会浪费资源的热流,即使没有任何人收集它们!也有一些合法的用例,但请谨记并进行必要的文档记录。即使浪费资源,使底层流生产者保持活动状态,对某些用例可能会有益处:可以立即获得新数据,而不是赶上并暂时显示陈旧数据。根据用例决定生产者是否需要始终处于活动状态。

MutableStateFlowMutableSharedFlow 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。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/627835.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(2023,语义混合)处理神经网络中语义和视觉对齐的差异

Addressing Discrepancies in Semantic and Visual Alignment in Neural Networks 公众号&#xff1a;EDPJ 目录 0. 摘要 1. 简介 2. 相关工作 3. 方法 4. 实验 5. 结果 6. 讨论与结论 参考 S. 总结 S.1 主要思想 S.2 语义混合 S.3 方法 0. 摘要 对于图像分类任…

苹果Vision Pro将引爆人机交互的重大变革

2023年6月6日&#xff0c;苹果发布了大家期待已久的Vision Pro&#xff0c;Vision Pro是一款专业级MR设备&#xff0c;融合了虚拟现实(VR)和增强现实(AR)技术&#xff0c;可以让用户完全沉浸在高分辨率显示内容中。允许用户以一种全新的方式在其周围的空间中查看APP。用户可以用…

unreal 5 实现角色拾取功能

要实现角色拾取功能&#xff0c;我们需要实现蓝图接口功能&#xff0c;蓝图接口主要提供的是蓝图和蓝图之间可以通信&#xff0c;接下来&#xff0c;跟着教程&#xff0c;实现一下角色的拾取功能。 首先&#xff0c;我们要实现一个就是可视区的物品在朝向它的时候&#xff0c;会…

基于云技术的域名解析系统研究一:传统解析技术弊端(中科三方)

域名解析是互联网的一项重要基础作用&#xff0c;主要用于将域名翻译成计算机可识别的IP地址&#xff0c;完成对网络中其他主机的寻址任务。我们日常工作生活中的大部分网络应用&#xff0c;如网站访问、电子邮件以及其他各种web应用服务&#xff0c;都需要经过域名解析的寻址去…

Oracle的登陆问题和初级学习增删改查

1&#xff1a;学习Oracle首先需要安装Oracle&#xff0c;网上已经有很多很多教程了&#xff0c;这里不做叙述&#xff0c;自己百度即可&#xff0c;这里安装的标准版&#xff0c;个人根据需求安装学习或者企业开发即可。如果安装出错&#xff0c;自己百度Oracle的卸载即可&…

终极指南:Scrum中如何设置需求优先级

需求众多不知道如何下手&#xff1f;总想先做简单的需求&#xff0c;复杂需求却一拖再拖&#xff1f;那么&#xff0c;我们是时候开始考虑如何设置需求优先级了。 本期终极指南将展示如何为需求设置有效优先级&#xff0c;如何有效管理工作量&#xff0c;让效率指数倍增长&…

软件测试和软件开发哪个发展更好?我来告诉你怎么选

目录 一、基本素质要求 二、性格要求 三、入职门槛 四、转行难度 五、知识结构 六、竞争压力 七、职业发展 八、职业前景 总结 一、基本素质要求 这里的基本素质包括比如速辑思维能力、分析问题能力、沟通能力、协作能力、怀疑精神等方面&#xff0c; 【软件测试】: …

详细分析开发一个Servlet的步骤从实现Servlet接口,继承GenericServlet抽象类,继承HttpServlet抽象类逐步完善

实现Servlet接口 jakarta.servlet.Servlet是Servlet规范中的核心接口 Servlet对象的生命周期 Servlet对象的创建&#xff0c;对象上方法的调用&#xff0c;对象最终的销毁都是由Tomcat服务器全权负责的,JavaWeb程序员是无权干预的 第一步: Tomcat服务器本质是一个WEB容器, …

cocos creator 鼠标画笔|画线

cocos creator 版本使用 至少适配版本2.3.2以上 案例: 简要思路:MOUSE_MOVE事件和Graphics组件实现 前端也可以通过canvas和mousemove事件实现&#xff0c;原理一致 具体步骤如下&#xff1a; 1.添加节点Node 2.在Node节点上绑定组件Graphics 3.添加下方脚本drawcontroll.ts…

JMeter测试笔记(三):基本组件的使用

引言&#xff1a; 关于JMeter的测试笔记&#xff0c;这是第三篇&#xff0c;主要是介绍JMeter的基本组件的使用。 在进行性能测试时&#xff0c;了解并掌握基本组件的使用非常重要&#xff0c;因为它们是构成JMeter测试计划的基础。在本文中&#xff0c;我们将深入了解JMeter…

解决IDEA 引入GO SDK无法加载本地Go,The selected directory is not a valid home for Go SDK

解决IDEA 引入GO SDK无法加载本地Go&#xff0c;The selected directory is not a valid home for Go SDK 解决方法&#xff1a; 找到按照go目录下对应&#xff1a; C:\Program Files\go\go1.20.4\src\runtime\internal\sys 位置有一个文件名“zversion.go” 编辑文件 const …

JAVA开发(保姆级微服务搭建过程)

一、微服务的概念以及发展过程 微服务是指只提供一项功能的服务。 微服务是围绕某个业务领域展开的。比如把电商业务比较一个业务领域&#xff0c;技术部、产品部等就是围绕电商业务领域展开的微服务。在电商项目领域&#xff0c;由支付、商品、订单等微服务组成。 微服务架构…

【TypeScript】枚举类型和泛型的详细介绍

目录 TypeScript枚举类型 TypeScript泛型介绍 &#x1f3b2;泛型的基本使用 &#x1f3b2;泛型接口的使用 &#x1f3b2;泛型类的使用 &#x1f3b2;泛型的类型约束 枚举类型 枚举类型是为数不多的TypeScript特有的特性之一, JavaScript是没有的&#xff1a; 枚举其实就…

CiteScore 2022正式发布,AI Open首获即达22.5分,三大高被引论文值得一看

当前&#xff0c;由 ChatGPT、Stable Diffusion 等 AI 大模型掀起的新一轮科技浪潮&#xff0c;正在引领各个行业的变革性发展。及时、深入、全面地了解 AI 行业的前沿动态&#xff0c;有助于我们跟上 AI 行业的发展步伐&#xff0c;抓住时代机遇。 一本学术期刊的高影响力&am…

Java避免踩坑:Set对象排重注意避免重复-以commons-csv读取csv文件并排查为例

场景 HashSet HashSet 基于 HashMap 来实现的&#xff0c;是一个不允许有重复元素的集合。HashSet 允许有 null 值。 HashSet 是无序的&#xff0c;即不会记录插入的顺序。 HashSet 不是线程安全的&#xff0c; 如果多个线程尝试同时修改 HashSet&#xff0c; 则最终结果是…

Classification-based framework for binarization on OCT-ME论文学习和总结

论文&#xff1a;Classification-based framework for binarization on mice eye image in vivo with optical coherence tomography 源码&#xff1a;https://github.com/MIP2019/mip2019.github.io/blob/main/spsvm 目录 一、背景和出发点 二、创新点 三、SPSVM的具体实现…

hive创建udf函数流程

1.编写udf函数 引入pom文件 <dependencies> <dependency> <!-- 这个属于额外的jar包 自己按需引用 比如你想搞得函数 里面要连接mysql 这里肯定需要引入mysql的驱动包 我这个包是为了计算字符串的表达式的。 --> <groupId>org.apache.com…

Git GitLab 使用及规范

Git 基本操作 Git安装配置及基本使用 从官网下载安装包&#xff0c;手动完成安装。打开Git Bash命令行工具&#xff0c;执行命令ssh-keygen -t rsa -C Email-Addresss生成一个密钥对。登录到GitLab&#xff0c;点击右上角你的用户头像&#xff0c;点击Edit Profile settings&…

【Linux】linux和Linus

1991.09.17 21岁的芬兰学生林纳斯.托瓦兹在网上发布开源操作系统Linux0.01。 林纳斯本纳第克特托瓦兹&#xff08;Linus Benedict Torvalds&#xff0c;1969年12月28日- &#xff09;&#xff0c;芬兰赫尔辛基人&#xff0c;著名的电脑程序员&#xff0c;Linux内核的发明人及 …

Revit如何在体量中进行放样及如何生成垫层

一、Revit如何在体量中进行放样 体量中的放样在常规族放样的基础上进行了简化&#xff0c;下面通过实例来说明如何在体量中进行放样。 &#xff08;1&#xff09;新建概念体量。点击“新建概念体量”→“公制体量”→“打开”。 &#xff08;2&#xff09;绘制放样路径。点击“…