Flow深入浅出系列之使用Kotlin Flow自动刷新Android数据的策略

news2025/3/10 5:54:20

  • 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? = null
        flow {
            nextEmissionTimeMark?.let { delay(-it.elapsedNow()) }
            while (true) {
                emit(Unit)
                nextEmissionTimeMark = timeSource.markNow() + period
                delay(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.coloradmin.cn/o/1102170.html

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

相关文章

Linux——生产者消费者模型

目录 一.为何要使用生产者消费者模型 二.生产者消费者模型优点 三.基于BlockingQueue的生产者消费者模型 1.BlockingQueue——阻塞队列 2.实现代码 四.POSIX信号量 五.基于环形队列的生产消费模型 一.为何要使用生产者消费者模型 生产者消费者模式就是通过一个容器来解决生…

面试官:做过性能优化?我:任务切片!

给大家推荐一个实用面试题库 1、前端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;web前端面试题库 代码背景 本次分享基于一次线上环境的卡顿事故&#xff0c;客户数据体量过大导致的页面卡顿甚至页面直接崩溃的问题…

pdf压缩文件怎么压缩最小?

pdf压缩文件怎么压缩最小&#xff1f;我们很多项目介绍或是学术的报告都是采用的这个pdf格式&#xff0c;那么我们在存储或是需要进行分享的时候&#xff0c;可能就会因为文件过大而导致无法打开或是发送了。那么就需要将其进行压缩。PDF文件压缩方法很多&#xff0c;pdf压缩文…

【前端】JS - WebAPI

目 录 一.WebAPI 背景知识什么是 WebAPI什么是 APIAPI 参考文档 二.DOM 基本概念什么是 DOMDOM 树 三.获取元素querySelectorquerySelectorAll 四.事件初识基本概念事件三要素 五.操作元素获取/修改元素内容&#xff08;innerHTML&#xff09;获取/修改元素属性获取/修改样式属…

为什么全链路压测如此重要?

在今天的数字化世界中&#xff0c;软件系统的稳健性和性能至关重要。用户对于应用程序的高可用性和快速响应时间有着越来越高的期望&#xff0c;因此&#xff0c;全链路压测变得至关重要。本文将深入探讨什么是全链路压测&#xff0c;为什么它如此重要以及如何进行全链路压测。…

【ESP32】C语言映射表在嵌入式串口解析中的应用

本文章主要以ESP32开发环境为例记录&#xff0c;C语言映射表在嵌入式串口解析中的应用 【ESP32】C语言映射表在嵌入式串口解析中的应用 一、C语言映射表在串口数据解析中的应用1、数据结构2、指令、函数映射表3、串口解析函数实现 二、实验现象三、实验代码 一、C语言映射表在串…

Python超入门(4)__迅速上手操作掌握Python

# 15.while循环 rows 0 while rows < 5:print(* * rows)rows 1* ** *** ****# 16.使用while循环制作猜灯谜游戏secret_num 12 guess_count 0 guess_limit 100while guess_count < guess_limit:guess_count int(input("猜测:"))if guess_count secret_n…

turn搭建测试

安装 安装环境 cat /etc/redhat-release CentOS Linux release 7.6.1810 (Core)相关系统环境安装 sudo yum install -y make gcc cc gcc-c wgetsudo yum install -y openssl-devel libevent libevent-devel安装libEvent组件 # 如果链接不可用直接去官网找对应release包下载…

面试算法27:回文链表

问题 如何判断一个链表是不是回文&#xff1f;要求解法的时间复杂度是O&#xff08;n&#xff09;&#xff0c;并且不得使用超过O&#xff08;1&#xff09;的辅助空间。如果一个链表是回文&#xff0c;那么链表的节点序列从前往后看和从后往前看是相同的。 分析 如果不考虑…

使用两个goroutine交替、顺序打印一段字符串的字符

1、使用两个goroutine交替、顺序打印一段字符串的字符 输入&#xff1a;hello world 输出&#xff1a;hello world 关键点&#xff1a;控制goroutine的执行先后循序 golang语言版本&#xff1a; package mainimport ("fmt""sync" )func main() {conte…

数据结构——线性表作业

目录 选择题和填空题 编程题 1. 输出单链表倒数第K个结点值 单链表 双指针 2. 数组元素移动 3. 多项式相加 4. 数组的循环左移 选择题和填空题 编程题 1. 输出单链表倒数第K个结点值 【问题描述】 输入一个单向链表&#xff0c;输出该链表中倒数第k个结点&#xff0…

外贸知识:谈好订单的客户临时却要求价格优惠怎么办?

距离订单成交只差一步的时候&#xff0c;客户却要求价格一降再降&#xff0c;是为什么&#xff1f; 真的只是为了降价吗&#xff1f; Carl是一个苦苦挣扎的业务员&#xff0c;每次在挖掘客户需求的过程中&#xff0c;客户都是一副“对对对&#xff0c;是是是&#xff0c;好好好…

那些年,我们追过的Java BUG

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

基于springboot实现银行OA系统的设计与实现平台项目【项目源码+论文说明】计算机毕业设计

摘要 在互联网信息技术时代中&#xff0c;企业管理更多的是使用管理系统进行智能化控制&#xff0c;提高单位的核心竞争力&#xff0c;适应快节奏的生产活动。银行OA系统是为企业提供的一整套便于企业管理的应用软件&#xff0c;是目前企业管理的必备系统。通过走访了解&#…

几个非常实用的 Chrome Devtools 技巧

原文链接&#xff1a;[几个非常实用的 Chrome Devtools 技巧](https://fe32.top/articles/skill001/ 关于 Chrome 浏览器&#xff0c;如果你是一名前端开发者&#xff0c;相信对此并不陌生&#xff0c;我们可以用它来查看 网络请求、分析网页性能、调试 JavaScript 功能 等。 …

山海鲸报表系统:数据洞察的利器

在信息时代&#xff0c;数据是每个组织的核心资产。然而&#xff0c;拥有大量数据并不足够&#xff0c;我们需要将这些数据转化为有用的信息&#xff0c;以指导决策和行动。这就是山海鲸报表系统的使命。 解析数据的力量 山海鲸报表系统是一款强大的工具&#xff0c;旨在帮助企…

代码随想录打卡第四十二天| ● 62.不同路径 ● 63. 不同路径 II

62 不同路径 **题目&#xff1a;**一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish” &#xff09;。 问总共有多少条…

收银系统哪个好 2023年十大收银软件品牌排行榜

根据2023年的最新数据和市场调研&#xff0c;以下是收银系统十大排名&#xff08;2023年十大收银软件品牌排行榜&#xff09;&#xff1a; 商人宝 一款集成了收银系统、会员管理、进销存和数据分析等多个功能的服装店收银软件&#xff0c;适用于服装卖场、中小型连锁超市、便利…

RunnerGo UI自动化使用体验

RunnerGo怎么做UI自动化 首先需要进入官网&#xff0c;RunnerGo支持开源&#xff0c;可以自行下载安装&#xff0c;也可以点击右上角体验企业版按钮快速体验 点击体验企业版进入工作台后可以点击页面上方的UI自动化 进入到测试页面 创建元素 我们可以在元素管理中创建我们测试…

企业文件防泄密软件!好用的文件加密系统推荐

由于众多企业内部都有大量的机密数据以电子文档的形式存储着&#xff0c;且传播手段多样&#xff0c;很容易造成文件泄密的问题发生。若是员工通过网络泄密重要文件&#xff0c;或是有黑客入侵窃取机密数据等&#xff0c;造成重要文件被非法查看盗取&#xff0c;都会给企业业务…