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的最新值并在多个订阅者之间共享。但是StateFlowSharedFlow有其局限性,因为它们无法根据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本身,timeReferenceFlowsubscriptionCount也会从0更新为1,并且synchronizedTickerFlow()将在前一个值过期后立即唤醒并开始发出新值;
当更多的结果StateFlow开始收集相同的timeReferenceFlow时,它们都会增加其subscriptionCount并立即接收到缓存的时间参考。计时器保持活动状态,并将每个新计算的时间参考分发给所有订阅者;
当所有连接到timeReferenceFlow的结果StateFlow停止被UI收集时,其subscriptionCount最终将达到零,并且synchronizedTickerFlow()将停止发出新值。
我们已经证明了timeReferenceFlow也具有生命周期感知性,即使它从未直接被UI收集。仅当它至少有一个活跃的订阅者并且前一个值已过期时,其值才会更新。这样可以在整个应用程序中最大限度地利用缓存,同时确保所有结果彼此一致。

结论

使用ticker Flow是在Kotlin应用程序中定期更新向用户呈现的数据的一种简单而优雅的方式。为了在Android上实现高效的缓存,可以通过停止没有更多订阅者的StateFlow来使计时器与UI生命周期同步,并根据正确的单调时钟记住下一个tick的时间。

这种复杂性可以通过几个可重用的Flow操作符来隐藏。

我在实际应用程序中成功使用了这种技术。您认为它是否有意义或太复杂?您是否找到了更好的方法来实现相同的结果?请在评论部分分享您的反馈,并在喜欢的情况下帮助传播这些信息。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

springBoot复杂对象表示和lombok的使用

springBoot复杂对象表示 前言简单案例lombok的使用通过properties文件进行绑定在yaml文件中使用 前言 对象&#xff1a;键值对的集合&#xff0c;如&#xff1a;映射&#xff08;map)/哈希&#xff08;hash)/字典&#xff08;dictionary&#xff09; 数组&#xff1a;一组按次…

leetcode做题笔记174. 地下城游戏

恶魔们抓住了公主并将她关在了地下城 dungeon 的 右下角 。地下城是由 m x n 个房间组成的二维网格。我们英勇的骑士最初被安置在 左上角 的房间里&#xff0c;他必须穿过地下城并通过对抗恶魔来拯救公主。 骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0…

Java应用性能问题诊断技巧

作者&#xff1a;张彦东 参考&#xff1a;https://developer.aliyun.com/ebook/450?spma2c6h.20345107.ebook-index.28.6eb21f54J7SUYc 文章目录 &#xff08;一&#xff09;内存1.内存2.内存-JMX3.内存-Jmap4.内存-结合代码确认问题 &#xff08;二&#xff09;CPU1.CPU-JMX或…

史上最短的“牛熊转换”:BTC价格昨夜起飞,但却来自一条假新闻!

昨夜&#xff0c;加密市场经历了史上最短的一次“牛熊转换”。 在短短10分钟内&#xff0c;BTC快速走出多根阳线&#xff0c;价格直接起飞&#xff0c;连续突破28000美元、29000美元、30000美元的整数关口&#xff0c;最高触及30535.8美元&#xff0c;涨幅近10%&#xff08;数据…

Compose Canvas基础(2) 图形转换

Compose Canvas基础&#xff08;2&#xff09;图形转换 前言平移 translate缩放 scale旋转 rotate自定义绘图区域及绘制内边距inset组合转换 withTransform完整代码总结 上一篇文章 Compose Canvas基础&#xff08;1&#xff09; drawxxx方法 前言 阅读本文需要一定compose基…

mysql——面试题初体验

查询环境 1、student&#xff08;学生表&#xff09; 2、课程表(course) 3、教师表(teacher) 4、成绩表(score) 问题 (1) 查询所有学生的学号、姓名、选课数、总成绩 mysql> select s.s_id as 学号,s.s_name as 姓名 from student as s; ---------------- | 学号 | 姓名…

E138: Can‘t write viminfo file

E138: Can’t write viminfo file /home/xxx/.viminfo! 原因 进入/home/xxx/目录下&#xff0c;用ls -a你会发现有很多.viminfa.tmp - .viminfz.tmp 这种的临时文件&#xff0c;这是因为使用vim编辑器时&#xff0c;如果编辑器没有正常退出就会生成一个暂存文件&#xff0c;…

Flow深入浅出系列之更聪明的分享 Kotlin Flows

Flow深入浅出系列之在ViewModels中使用Kotlin FlowsFlow深入浅出系列之更聪明的分享 Kotlin FlowsFlow深入浅出系列之使用Kotlin Flow自动刷新Android数据的策略 Flow深入浅出系列之更聪明的分享 Kotlin Flows 使生命周期对上游流有效&#xff0c;以跳过不必要的工作。这是一…

使用kaliber与imu_utils进行IMU、相机+IMU联合标定

目录 1 标定工具编译 1.1 IMU标定工具 imu_utils 1.2 相机标定工具 kaliber 2 标定数据录制 3 开始标定 3.1 IMU标定 3.2 相机标定 3.3 相机IMU联合标定 4 将参数填入ORBSLAM的文件中 1 标定工具编译 1.1 IMU标定工具 imu_utils 标定IMU我们使用imu_utils软件进行标定…

Variations-of-SFANet-for-Crowd-Counting记录

论文&#xff1a;Encoder-Decoder Based Convolutional Neural Networks with Multi-Scale-Aware Modules for Crowd Counting 论文链接&#xff1a;https://arxiv.org/abs/2003.05586 源码链接&#xff1a;GitHub - Pongpisit-Thanasutives/Variations-of-SFANet-for-Crowd-C…

yarn : 无法加载文件 C:\Program Files\nodejs\yarn.ps1

问题描述&#xff1a; 问题分析&#xff1a; 这个错误提示说明在电脑系统上禁止运行 PowerShell 脚本&#xff0c;因此导致无法加载 Yarn 的安装脚本。这是由于系统的执行策略&#xff08;Execution Policies&#xff09;设置所导致的。 解决方法&#xff1a; 1. 以管理员身…

Ubuntu中不能使用ifconfig命令

​ 问题 打开终端使用如下命令不能运行&#xff1a; ifconfig显示如下错误: 解决方法 在VMware中的虚拟机下面打开“编辑虚拟机设置”&#xff0c;或者在已经打开的虚拟机面板上面打开“虚拟机—设置” 选择网络适配器&#xff0c;选择“NAT模式”&#xff0c;没开机的就…

Learning Sample Relationship for Exposure Correction 论文阅读笔记

这是中科大发表在CVPR2023的一篇论文&#xff0c;提出了一个module和一个损失项&#xff0c;能够提高现有exposure correction网络的性能。这已经是最近第三次看到这种论文了&#xff0c;前两篇分别是CVPR2022的ENC&#xff08;和这篇文章是同一个一作作者&#xff09;和CVPR20…

ardupilot开发 --- 起飞前后 篇

起飞前检查 电机响应是否正确&#xff08;转向&#xff09;姿态响应是否正常&#xff08;roll pitch yaw&#xff09;GPS数据是否正常&#xff08;星数&#xff0c;RTK信号&#xff09;电源电压安全开关安全检测&#xff08;armed pre check&#xff09; 起飞前的必调参数 机…

mmlab 做实验

首先 下载项目完整代码&#xff0c;在pycharm中打开 1. comfig 中有各种网络模型&#xff0c;可以直接使用训练好的预训练模型&#xff0c;尽量不要改动网络模型的结构 2. 18表示网络机构18层&#xff0c;8是每个卡的batch&#xff0c;cifar10 是数据集 3.配置文件解析 4. …

工业机器视觉系统构成及功能

工业机器视觉系统构成及功能 工业机器视觉系统由光源、光学传感器、图像采集设备、图像处理设备、机器视觉软件、辅助传感器、控制单元和执行机构等组件构成。 光源提供光线以辅助图像获取。 光学传感器将外部场景转换为电信号。 图像采集设备将信号转换为图像数据&#xf…

VSCode连接代理

VSCode连接代理 首先有代理 然后在设置里搜代理 然后再在windows的设置–>网络–>代理 拼接上就行 最后重启

【小尘送书-第八期】《小团队管理:如何轻松带出1+1>2的团队》

大家好&#xff0c;我是小尘&#xff0c;欢迎你的关注&#xff01;大家可以一起交流学习&#xff01;欢迎大家在CSDN后台私信我&#xff01;一起讨论学习&#xff0c;讨论如何找到满意的工作&#xff01; &#x1f468;‍&#x1f4bb;博主主页&#xff1a;小尘要自信 &#x1…

macos 12 支持机型 macOS Monterey 更新中新增的功能

macOS Monterey 能让你以全然一新的方式与他人沟通联络、共享内容和挥洒创意。尽享 FaceTime 通话新增的音频和视频增强功能&#xff0c;包括空间音频和人像模式。通过功能强大的效率类工具&#xff08;例如专注模式、快速备忘录和 Safari 浏览器中的标签页组&#xff09;完成更…

【unity】【VR】白马VR课堂系列-VR开发核心基础04-主体设置-XR Rig的引入和设置

接下来我们开始引入并构建XR Rig。 你可以将XR Rig理解为玩家在VR世界中的替身。 我们先删除Main Camera&#xff0c;在Hierarchy右键点击删除。 然后再在场景层右键选择XR下的XR Origin。这时一个XR Origin对象就被添加到了Hierarchy。 重设XR Origin的Position和Rotation…