Flow深入浅出系列之使用Kotlin Flow自动刷新Android数据的策略
- Flow深入浅出系列之在ViewModels中使用Kotlin Flows
- Flow深入浅出系列之更聪明的分享 Kotlin Flows
- Flow深入浅出系列之使用Kotlin Flow自动刷新Android数据的策略
Flow深入浅出系列之使用Kotlin Flow自动刷新Android数据的策略
讨论在Android应用程序中使用Kotlin Flow高效加载数据的用法。它是第二部分“更智能的共享Kotlin Flows”的直接延续,因为它重用了相同的概念,涵盖了另一个用例:用户界面的自动定期刷新。
简单的定期刷新
当无法准确确定UI显示的数据集何时发生变化,或者当变化过于频繁时,一种常见的策略是在屏幕可见时定期以固定间隔重新加载数据。
实现这个的最简单方法之一是从一个无限循环中创建一个Flow,在每次发射之间调用delay()
:
fun tickerFlow(period: Duration): Flow<Unit> = flow {while (true) {emit(Unit) delay(period)}
}
这相当于RxJava中带有固定发射值(Unit)和初始延迟为0的Observable.interval()
符。
然后,使用map()
或mapLatest()
操作符转换这个Flow,在每次定时器的“tick”上执行加载操作并返回结果:
tickerFlow(REFRESH_INTERVAL).map {repository.loadSomeData()}
注意这两个操作符之间的微妙差异:
- 使用
map()
,整个Flow将在单个协程中按顺序执行,这意味着delay()只会在加载操作完成后开始运行。因此,每个加载操作将延迟前一个加载操作所花费的时间加上固定间隔。 - 使用
mapLatest()
,主协程将收集tickerFlow()
的上游值,同时创建一个子协程以并发执行加载操作并收集结果,而不会挂起主协程。这意味着delay()
将立即在上一个tick之后开始运行,每个加载操作将严格按计划开始。这也意味着间隔必须比典型的加载时间长,因为它将充当超时:当tickerFlow()
发射新值时,如果先前的加载操作未能及时完成,将被取消。然后,新的子协程取代之前的子协程执行下一个加载操作。
这个简单的实现将始终在Flow收集开始或重新开始时立即触发新的加载操作,而不考虑前一次运行。对于几秒钟的短时间间隔来说已经足够了,当加载操作被认为是廉价的时候。
使刷新更智能以利用缓存
然而,对于较长的刷新间隔和需要更多资源的加载操作(如执行API调用)来说,上述算法并不是很高效:当一个暂时隐藏的屏幕再次变为可见并且Flow收集重新开始时,我们希望避免重新加载仍然被认为是新鲜数据的那些数据的无谓工作,因为这些数据将在接下来的几分钟或几小时内仍然保持更新。
在第一篇文章中,我们看到StateFlow通常用于缓存Flow的最新值并在多个订阅者之间共享。但是StateFlow
和SharedFlow
有其局限性,因为它们无法根据Activity的生命周期简单地暂停和恢复工作:如果屏幕隐藏时停止了底层的Flow收集,当屏幕再次可见时,它总是需要从头开始重新启动,使得缓存在这种情况下毫无用处。
在第二篇文章中,我们研究了创建一个自定义Flow操作符的方法,旨在解决这些限制:flowWhileShared()
。它允许只将底层Flow的上游部分与生命周期关联起来,以便在与一些过滤逻辑结合使用时,可以避免Flow下游的繁重工作。这样就可以充分利用StateFlow的缓存。
事实证明,我们可以使用相同的操作符来实现更智能的tickerFlow()
版本,旨在与StateFlow结合使用。synchronizedTickerFlow()
具有生命周期感知性,只会在父StateFlow至少有一个订阅者时发射值。使其更智能的是,它还记住了下一次发射的时间,在没有订阅者的暂停恢复后,在发出下一个值之前它将先等待达到那个时间点。
fun synchronizedTickerFlow(period: Duration,subscriptionCount: StateFlow<Int>,timeSource: TimeSource = ElapsedRealTimeSource
): Flow<Unit> {return flow {var nextEmissionTimeMark: TimeMark? = nullflow {nextEmissionTimeMark?.let { delay(-it.elapsedNow()) }while (true) {emit(Unit)nextEmissionTimeMark = timeSource.markNow() + perioddelay(period)}}.flowWhileShared(subscriptionCount, SharingStarted.WhileSubscribed()).collect(this)}
}
让我们详细分析代码。
你可能会注意到的第一件事是,计时逻辑是基于在Kotlin标准库1.9.0版本中稳定的新kotlin.time API。其中包括类Duration,TimeSource和TimeMark。
由函数返回的主要Flow收集了一个内部使用的辅助Flow的输出。主Flow的作用仅是封装状态(nextEmissionTimeMark)并将其局部化到每个收集中,就像所有Flow操作符一样。辅助Flow是根据父生命周期启动和停止的(当提供的subscriptionCount达到零时立即停止),这要归功于flowWhileShared()
操作符。
注意:我们并没有向共享策略
SharingStarted.WhileSubscribed()
传递任何超时值,因为停止和重新启动辅助Flow的成本很低。
辅助流的主要逻辑与tickerFlow()几乎相同,唯一的区别是只有当达到nextEmissionTimeMark
时才会发射第一个“tick”。TimeMark
表示一点时间,nextEmissionTimeMark
是下一个发射需要发生的最早时间点。
当主Flow收集开始时,nextEmissionTimeMark
最初为null,并且第一个tick立即发射,没有延迟。然后,每次发射之后,下一个发射的未来时间点通过使用markNow()
从TimeSource检索当前时间点并将期间Duration
添加到其中来计算。
当辅助Flow在暂停后重新启动且nextEmissionTimeMark
不为null时,调用TimeMark上的elapsedNow()
来计算到达那个时间点所需等待的时间(Duration),并对结果进行否定,因为elapsedNow()
实际上返回TimeMark和现在之间流逝的时间,如果TimeMark在未来,则为负值。请注意,使用负Duration调用delay()
没有效果,将立即返回,因此我们不需要单独处理下一个发射时间已经到达并且现在已经过去的情况。
《IT Crowd》第一集的截图,Roy在电话中问道:对不起,你是来自过去的吗?
正确使用TimeSource的重要性
为了使这段代码正常工作,必须使用基于单调时钟(monotonic clock)而不是挂钟(wall clock)的TimeSource。单调时钟是一种始终向前移动且无法调整或重置的时钟。kotlin.time已经提供了TimeSource.Monotonic
,它基于JVM和Android上的System.nanoTime()
。虽然这个时钟对于JVM来说足够好,但对于Android应用程序可能会造成问题,因为当设备的CPU进入深度休眠时,它会停止运行,而这可能发生在屏幕关闭后。这意味着如果用户解锁刚刚经历了10分钟深度休眠的Android设备并返回到应用程序,数据刷新将会延迟10分钟。
在Android上,更适合此用途的时钟是SystemClock.elapsedRealtimeNanos()
,它是具有纳秒精度的单调时钟,并包含设备在深度休眠模式下花费的时间。由于官方的Kotlin Android Jetpack库尚未提供基于此时钟的TimeSource,因此我们自己创建一个:
object ElapsedRealTimeSource : AbstractLongTimeSource(DurationUnit.NANOSECONDS) {override fun read(): Long = SystemClock.elapsedRealtimeNanos()override fun toString(): String = "TimeSource(SystemClock.elapsedRealtimeNanos())"
}
由于TimeSource作为参数传递给synchronizedTickerFlow()
,因此可以轻松地替换实现,例如使用TestTimeSource进行测试。
将它们整合在一起
以下是如何将synchronizedTickerFlow()
与前文中描述的stateFlow()
工厂函数结合使用的示例。作为提醒,这个工厂函数允许StateFlow与提供数据的基础Flow共享其subscriptionCount
。
@OptIn(ExperimentalCoroutinesApi::class)
val results: StateFlow<Result> = stateFlow(viewModelScope, Result.Empty) { subscriptionCount ->synchronizedTickerFlow(REFRESH_PERIOD, subscriptionCount).mapLatest {repository.loadSomeData()}
}
以下是此代码对UI状态更改的逐步反应:
当UI首次可见并开始收集结果StateFlow时,synchronizedTickerFlow()
将开始定期发出新值,这将触发加载最新数据。此数据将缓存在StateFlow中,并与所有当前和未来的订阅者共享;
当UI变得不可见并停止收集StateFlow时,基础Flow仍然活动,但ticker将不会发出任何新值,因此不会加载新数据。我们节省了资源;
当UI再次可见并重新开始收集时,它将立即接收到StateFlow的缓存值。在基础Flow中,ticker将恢复。但首先,它将等待下一次发出的计划时间到达,然后再发出任何内容。这样,数据将根据其有效性保存在StateFlow缓存中,而不是无条件地被替换。我们现在节省了更多的资源。
高级用例:共享时间参考
有时,屏幕需要分别查询多个数据源,并定期更新。如果数据与时间有关,则所有源都需要使用相同的时间点作为参考,以确保它们的结果彼此一致。
例如:您希望定期加载过去10分钟和接下来10分钟的日程表,并确保结果不重叠。
这个时间参考点可以使用synchronizedTickerFlow()
周期性地更新,并使用stateFlow()
进行缓存和共享,与前面的示例完全相同:
private val timeReferenceFlow: Flow<Instant> = stateFlow(viewModelScope, null) { subscriptionCount ->synchronizedTickerFlow(REFRESH_PERIOD, subscriptionCount).map { Instant.now() }
}.filterNotNull()
不同之处在于,每个数据源将连接到相同的timeReferenceFlow实例,并使用flowWhileShared()
和distinctUntilChanged()
的组合来确保只有当时间参考发生变化时才更新数据。结果也将缓存在StateFlow中:
val results1: StateFlow<Result> = stateFlow(viewModelScope, Result.Empty) { subscriptionCount ->timeReferenceFlow.flowWhileShared(subscriptionCount, SharingStarted.WhileSubscribed()).distinctUntilChanged().map { timeReference: Instant ->repository.loadDataForTime(timeReference)}
}
使用这种模式使一个StateFlow依赖于另一个StateFlow,通过subscriptionCount
将UI生命周期从一个StateFlow传播和聚合到下一个StateFlow。以下是它们对UI状态更改的反应方式:
当UI开始收集第一个结果StateFlow时,其subscriptionCount
将从0更新为1,并且flowWhileShared()
将开始收集上游的timeReferenceFlow
;
作为StateFlow本身,timeReferenceFlow
的subscriptionCount
也会从0更新为1,并且synchronizedTickerFlow()
将在前一个值过期后立即唤醒并开始发出新值;
当更多的结果StateFlow开始收集相同的timeReferenceFlow
时,它们都会增加其subscriptionCount
并立即接收到缓存的时间参考。计时器保持活动状态,并将每个新计算的时间参考分发给所有订阅者;
当所有连接到timeReferenceFlow
的结果StateFlow停止被UI收集时,其subscriptionCount
最终将达到零,并且synchronizedTickerFlow()
将停止发出新值。
我们已经证明了timeReferenceFlow
也具有生命周期感知性,即使它从未直接被UI收集。仅当它至少有一个活跃的订阅者并且前一个值已过期时,其值才会更新。这样可以在整个应用程序中最大限度地利用缓存,同时确保所有结果彼此一致。
结论
使用ticker Flow是在Kotlin应用程序中定期更新向用户呈现的数据的一种简单而优雅的方式。为了在Android上实现高效的缓存,可以通过停止没有更多订阅者的StateFlow来使计时器与UI生命周期同步,并根据正确的单调时钟记住下一个tick的时间。
这种复杂性可以通过几个可重用的Flow操作符来隐藏。
我在实际应用程序中成功使用了这种技术。您认为它是否有意义或太复杂?您是否找到了更好的方法来实现相同的结果?请在评论部分分享您的反馈,并在喜欢的情况下帮助传播这些信息。
相关文章:

Flow深入浅出系列之使用Kotlin Flow自动刷新Android数据的策略
Flow深入浅出系列之在ViewModels中使用Kotlin FlowsFlow深入浅出系列之更聪明的分享 Kotlin FlowsFlow深入浅出系列之使用Kotlin Flow自动刷新Android数据的策略 Flow深入浅出系列之使用Kotlin Flow自动刷新Android数据的策略 讨论在Android应用程序中使用Kotlin Flow高效加载…...

AC修炼计划(AtCoder Regular Contest 165)
传送门:AtCoder Regular Contest 165 - AtCoder 本次习题参考了樱雪猫大佬的题解,大佬的题解传送门如下:Atcoder Regular Contest 165 - 樱雪喵 - 博客园 (cnblogs.com) A - Sum equals LCM 第一题不算特别难 B - Sliding Window Sort 2 对…...
【Express】登录鉴权 JWT
JWT(JSON Web Token)是一种用于实现身份验证和授权的开放标准。它是一种基于JSON的安全传输数据的方式,由三部分组成:头部、载荷和签名。 使用jsonwebtoken模块,你可以在Node.js应用程序中轻松生成和验证JWT。以下是j…...

【微服务 SpringCloud】实用篇 · Ribbon负载均衡
微服务(4) 文章目录 微服务(4)1. 负载均衡原理2. 源码跟踪1)LoadBalancerIntercepor2)LoadBalancerClient3)负载均衡策略IRule4)总结 3. 负载均衡策略3.1 负载均衡策略3.2 自定义负载…...

zabbix-proxy代理服务器配置
下载zabbix源 rpm -Uvh https://repo.zabbix.com/zabbix/5.0/rhel/7/x86_64/zabbix-release-5.0-1.el7.noarch.rpm 安装 yum -y install zabbix-proxy-mysql zabbix_get 查看相关文件路径 rpm -ql zabbix-proxy-mysql 创建数据库 mysq -uroot -proot mysql> create database…...

【python零基础入门学习】python进阶篇之OOP - 面向对象的程序设计
本站以分享各种运维经验和运维所需要的技能为主 《python零基础入门》:python零基础入门学习 《python运维脚本》: python运维脚本实践 《shell》:shell学习 《terraform》持续更新中:terraform_Aws学习零基础入门到最佳实战 《k8…...

中国xx集团信息技术工程师面试
进入面试间,坐着三位面试官,压力扑面而来,三位面试官先做了自我介绍,介绍了一下面试的流程后才开始面试。 一、自我介绍 不多说。 二、看你学过数据挖掘这门课,能简单介绍一下有哪些章节,学了些什么&…...

Jmeter接口自动化测试 —— Jmeter下载安装及入门
jmeter简介 Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,它最初被设计用于Web应用测试,但后来扩展到其他测试领域。 下载 下载地址:Apache JMeter - Download Apache JMeter 安装 由于Jmeter是基于Java的…...
ARM 学习笔记2 初识Cortex-M33与STM32G4
入门 ARM Cortex-M系列处理器的差异与联系:【ARM Cortex-M 系列 1 – Cortex-M0, M3, M4, M7, M33 差异】两本书籍的英文版和中文版 Definitive Guide to Arm Cortex-M23 and Cortex-M33 Processors Arm Cortex-M23和Cortex-M33微处理器权威指南ST的介绍页 Arm Cor…...

vue中使用coordtransform 互相转换坐标系
官方网站:https://www.npmjs.com/package/coordtransform 在使用高德sdk时,其返回的坐标在地图上显示时有几百米的偏移,这是由于高德用的是 火星坐标(GCJ02),而不是wgs84坐标。为了消除偏移,将G…...

双线性插值详解
双线性插值的原理网上资料非常多,本文重点介绍双线性插值实现的两种方式: 角对齐(coner_align = True) 和 边对齐(coner_align = False)。两种不能的方式下去实现双线性插值,目标图像中的每个像素点,它是如何计算取值的,本文会通过原理结合代码的方式将实现细节讲清楚。 1…...
C++ “”
&加上有时候会加速 如果想该对象跟着函数变化一定要加“&” 在题目函数里面定义的 例如 vector<vector<bool>> visited(grid.size(),vector<bool>(grid[0].size(),false)); 如果自己定义的新void dfs(vector<vector<bool>>…...
计算机三级有必要考吗?计算机三级有哪些科目?
在大学期间,计算机等级考试是一门很火热的考试,很多小伙伴通过二级考试以后在究竟是报考三级还是四级之间徘徊,下面肉丸子就来给大家分析一下,究竟有没有必要考计算机三级考试,以及计算机三级考试的科目有哪些…...

6.5 Elasticsearch(五)Spring Data Elasticsearch - 增删改查API
文章目录 1.Spring Data Elasticsearch2.案例准备2.1 在 Elasticsearch 中创建 students 索引2.2 案例测试说明 3.创建项目3.1 新建工程3.2 新建 springboot module,添加 spring data elasticsearch 依赖3.3 pom.xml 文件3.4 application.yml 配置 4.Student 实体类…...
XPS—专项文献阅读-科学指南针
XPS(X-ray Photoelectron Spectroscopy),X射线光电子能谱,可以说是材料研究中必不可少的一类分析测试手段了。今天我们就来讲讲,什么情况下我们需要用到XPS,以及拿到数据之后应该怎样进行数据处理分析。 XP…...

电脑办公助手之桌面便签,助力高效率办公
在现代办公的快节奏中,大家有应接不暇的工作,每天面对着复杂的工作任务,总感觉时间不够用,而且工作无厘头。对于这种状态,大家可以选择在电脑上安装一款好用的办公便签软件来辅助日常办公。 敬业签是一款专为办公人士…...
【面试题】2023虹软计算机视觉一面
来源:投稿 作者:LSC 编辑:学姐 1.自我介绍 2.介绍了自己的项目,并提问项目,讲了30分钟 3.介绍centernet,它和其他目标检测模型有什么区别 4.介绍yolov5 5.介绍focal loss 6.双线性插值和最近邻插值的区…...

板带纠偏控制系统伺服比例阀放大器
板带纠偏控制系统是集光、机、电、液四方面有机结合在一起的全闭环电液伺服系统,是用途广泛的机电一体化高新技术产品。 板带纠偏控制系统可广泛地应用于机械、冶金、造纸、橡胶、织带、纺织印染、电镀、塑膜胶片等诸多行业的不同种类的带材生产线的在线纠偏。 板…...
视频I420裸流保存为文件
1、从TvCamera的ABK回调的OnImageReceived出来的是I420的数据,保存文件的方式如下: void OnImageReceived(const uint8_t* data, size_t size, uint16_t widht, uint16_t height) { .............. FILE *fp fopen("test.yuv", "wb&quo…...

IDEA中SpringBoot项目的yml多环境配置
SpringBoot的yml多环境配置 创建多个配置文件 application.yml #主配置文件 application-dev.yml #开发环境的配置 application-test.yml #测试环境的配置在application.yml中添加多环境配置属性 spring:profiles:active: profiles.active项目启动可能不会识别&#x…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...

【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)
服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...

【第二十一章 SDIO接口(SDIO)】
第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...

1.3 VSCode安装与环境配置
进入网址Visual Studio Code - Code Editing. Redefined下载.deb文件,然后打开终端,进入下载文件夹,键入命令 sudo dpkg -i code_1.100.3-1748872405_amd64.deb 在终端键入命令code即启动vscode 需要安装插件列表 1.Chinese简化 2.ros …...
【服务器压力测试】本地PC电脑作为服务器运行时出现卡顿和资源紧张(Windows/Linux)
要让本地PC电脑作为服务器运行时出现卡顿和资源紧张的情况,可以通过以下几种方式模拟或触发: 1. 增加CPU负载 运行大量计算密集型任务,例如: 使用多线程循环执行复杂计算(如数学运算、加密解密等)。运行图…...
Android Bitmap治理全解析:从加载优化到泄漏防控的全生命周期管理
引言 Bitmap(位图)是Android应用内存占用的“头号杀手”。一张1080P(1920x1080)的图片以ARGB_8888格式加载时,内存占用高达8MB(192010804字节)。据统计,超过60%的应用OOM崩溃与Bitm…...

听写流程自动化实践,轻量级教育辅助
随着智能教育工具的发展,越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式,也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建,…...
嵌入式常见 CPU 架构
架构类型架构厂商芯片厂商典型芯片特点与应用场景PICRISC (8/16 位)MicrochipMicrochipPIC16F877A、PIC18F4550简化指令集,单周期执行;低功耗、CIP 独立外设;用于家电、小电机控制、安防面板等嵌入式场景8051CISC (8 位)Intel(原始…...
【SpringBoot自动化部署】
SpringBoot自动化部署方法 使用Jenkins进行持续集成与部署 Jenkins是最常用的自动化部署工具之一,能够实现代码拉取、构建、测试和部署的全流程自动化。 配置Jenkins任务时,需要添加Git仓库地址和凭证,设置构建触发器(如GitHub…...

高效的后台管理系统——可进行二次开发
随着互联网技术的迅猛发展,企业的数字化管理变得愈加重要。后台管理系统作为数据存储与业务管理的核心,成为了现代企业不可或缺的一部分。今天我们要介绍的是一款名为 若依后台管理框架 的系统,它不仅支持跨平台应用,还能提供丰富…...