kotlin Flow 学习指南 (三)最终篇

目录

  • 前言
  • Flow生命周期
  • StateFlow 替代LiveData
  • SharedFlow
  • 其他常见应用场景
    • 处理复杂、耗时逻辑
    • 存在依赖关系的接口请求
    • 组合多个接口的数据
  • Flow使用注意事项
  • 总结

前言

前面两篇文章,介绍了Flow是什么,如何使用,以及相关的操作符进阶,接下来这篇文章,主要介绍Flow在实际项目中使用。

Flow生命周期

在介绍Flow实际应用场景之前,我们先回顾Flow第一篇介绍的计时器例子,我们在ViewModel定义了一个timeFlow数据流:

class MainViewModel : ViewModel() {val timeFlow = flow {var time = 0while (true) {emit(time)delay(1000)time++}
}

然后Activity里面,接收前面定义的数据流。

lifecycleOwner.lifecycleScope.launch {viewModel.timeFlow.collect { time ->times = timeLog.d("ddup", "update UI $times")}}

我运行看下实际效果:

flow1.gif

你们有没有发现,App切换到后台时,日志还在打印,这不是对资源的浪费,我们修改一下接收的地方代码:

lifecycleOwner.lifecycleScope.launchWhenStarted {viewModel.timeFlow.collect { time ->times = timeLog.d("ddup", "update UI $times")}}

我们把协程开启的方法,从launch改成launchWhenStarted,再运行看下效果:

flow2.gif

我们可以看到,当点击HOME键,退回到后台的时候,日志不再打印了,由此可见,改动生效了,但是流取消接收了吗,我们切回到前台看下:

flow3.gif

切换到前台,我们可以看到,计数器并没有从0开始,所以其实它并没有取消接收,只是在后台暂停接收数据了,Flow管道还保留之前的数据,事实上这个launchWhenStarted API已经废弃了,Google更推荐repeatOnLifecycle来代替它,并且它不会存在管道中保留旧数据问题。
我们尝试改造一下对应代码:

lifecycleOwner.lifecycleScope.launch {lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.timeFlow.collect { time ->times = timeLog.d("ddup", "update UI $times")}}
}

重新运行看下效果:

flow4.gif

我们可以看到,从后台切回到前台数据又从0开始了,说明切换到后台,Flow取消工作了,原来的数据全部清空了。

我们在使用Flow,通过repeatOnLifecycle,更能保证我们程序的安全性。

StateFlow 替代LiveData

前面介绍的都是Flow冷流例子,接下来将会介绍一些热流常见的应用场景。
还是前面的计时器的例子,假如横竖屏切换后,又会出现什么情况呢?

flow5.gif

我们可以看到,横竖屏切换后,Activity重新创建,重新创建后,timeFlow会重新collect,冷流被重新collect后重新执行,然后计时器又从0开始计时了,很多时候,我们希望横竖屏切换时,希望页面的状态是保持不变的,至少在一定时间内不被改变的,这里我们冷流修改成热流试下:

val hotFlow =timeFlow.stateIn(viewModelScope,SharingStarted.WhileSubscribed(5000),0)```
lifecycleOwner.lifecycleScope.launch {lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.hotFlow.collect { time ->times = timeLog.d("ddup", "update UI $times")}}
}
```

这里着重说下stateIn里面的三个参数,第一个是协程的作用域,第二个是flow保持工作状态最大有效时间,超过flow就会停止工作,最后一个参数是初始值。

重新运行看下效果:

flow6.gif

这里我们可以看到横竖屏切换后,打印的日志,计时器不会从0开始了。
我们上面介绍了一个冷流如何修改变成热流的,这里还没有介绍stateFlow如何代替LiveData,下面介绍一下,stateFlow替代LiveData用法:

private val _stateFlow = MutableStateFlow(0)
val stateFlow = _stateFlow.asStateFlow()fun startTimer() {val timer = Timer()timer.scheduleAtFixedRate(object :TimerTask() {override fun run() {_stateFlow.value += 1}},0,1000)
}```viewModel.startTimer()lifecycleOwner.lifecycleScope.launch {lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.stateFlow.collect { time ->times = timeLog.d("ddup", "update UI $times")}}
}
```

我们定义了一个StateFlow热流,然后通过一个startTimer()方法改变stateFlow值类似LiveData setData,点击按钮时,开始改变StateFlow值并收集对应流的值类似LiveData Observe方法监听数据变化。
下面看下实际运行效果:

flow7.gif

到这里,我们介绍完了StateFlow基本用法,下面来介绍SharedFlow。

SharedFlow

要理解SharedFlow,我们先知道个概念,粘性事件,按字面理解就是,观察者订阅数据源时,如果数据源已经有最新的数据,那么这些数据会立即推送给观察者。从上面的解释来看,LiveData是符合这个粘性特性的,同样的StateFlow呢?我们写个简单的demo验证一下:


class MainViewModel : ViewModel() {private val _clickCountFlow = MutableStateFlow(0)val clickCountFlow = _clickCountFlow.asStateFlow()fun increaseClickCount() {_clickCountFlow.value += 1
}
}
//MainActivity
```
val tv = findViewById<TextView>(R.id.tv_content)
val btn = findViewById<Button>(R.id.btn)
btn.setOnClickListener {viewModel.increaseClickCount()
}lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.clickCountFlow.collect { time ->tv.text = time.toString()Log.d("ddup", "update UI $time")}}
}
```

我们首先在MainViewModel,定义了一个clickCountFlow,然后在Activity,通过Button点击对clickCountFlow数据改变,然后接收clickCountFlow并把数据显示在文本上。
下面看下运行效果:

flow8.gif

我们可以看到横竖屏切换的时候,Activity重新创建,clickCountFlow重新收集后,数据还是从之前的4开始的,说明StateFlow是粘性的,在这里看上去没有问题,但是我们看另外一个例子,我们模拟一个点击登陆的场景,点击登陆按钮,实现登陆并登陆:

//MainViewModelprivate val _loginFlow = MutableStateFlow("")val loginFlow = _loginFlow.asStateFlow()fun startLogin() {// Handle login logic here._loginFlow.value = "Login Success"}
//MainActivity```
val tv = findViewById<TextView>(R.id.tv_content)
val btn = findViewById<Button>(R.id.btn)
btn.setOnClickListener {viewModel.startLogin()
}lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.loginFlow.collect {if (it.isNotBlank()) {Toast.makeText(this@MainActivity2, it, Toast.LENGTH_LONG).show()}}}
}
```

上述代码实际就是模拟一个点击登陆,然后会提示登陆成功,我们看下实际运行效果:

flow9.gif

看到没有,横竖屏切换后,登陆成功的提示重新弹出一遍,我们并没有走重新登陆流程,这就是粘性事件带来的数据重复接收的问题,上面代码,我们改成SharedFlow试下:

    private val _loginFlow = MutableSharedFlow<String>()val loginFlow = _loginFlow.asSharedFlow()fun startLogin() {// Handle login logic here.viewModelScope.launch {_loginFlow.emit("Login Success")}}

我们StateFlow改成SharedFlow,我们可以看到SharedFlow不需要初始值,登陆的地方增加了emit方法发送数据,接收数据的地方不变,重新运行下看下效果:

flow10.gif

这里我们可以看到使用SharedFlow不会出现这个粘性问题,其实SharedFlow还有很多参数可以配置的:

    public fun <T> MutableSharedFlow(// 每个新的订阅者订阅时收到的回放的数目,默认0replay: Int = 0,// 除了replay数目之外,缓存的容量,默认0extraBufferCapacity: Int = 0,// 缓存区溢出时的策略,默认为挂起。只有当至少有一个订阅者时,onBufferOverflow才会生效。当无订阅者时,只有最近replay数目的值会保存,并且onBufferOverflow无效。onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND)

SharedFlow更多用法,有待大家去发掘啊,这里不过赘述了。

其他常见应用场景

前面介绍了从基本冷流到热流,以及StateFlow、SharedFlow常见用法,适用场景,接下来,我们围绕几个实际例子,看看flow其他常见应用场景。

处理复杂、耗时逻辑

我们一般做一些复杂的耗时逻辑,放在子线程处理,然后切换到主线程展示UI,同样的Flow也支持线程切换,flowOn可以让之前的操作放到对应的子线程处理。
我们实现一个读取本地Assets目录下的person.json文件,并将其解析出来,json文件中的内容:

{"name": "ddup","age": 101,"interest": "earn money..."
}

然后解析文件:

fun getAssetJsonInfo(context: Context, fileName: String): String {val strBuilder = StringBuilder()var input: InputStream? = nullvar inputReader: InputStreamReader? = nullvar reader: BufferedReader? = nulltry {input = context.assets.open(fileName, AssetManager.ACCESS_BUFFER)inputReader = InputStreamReader(input, StandardCharsets.UTF_8)reader = BufferedReader(inputReader)var line: String?while ((reader.readLine().also { line = it }) != null) {strBuilder.append(line)}} catch (ex: Exception) {ex.printStackTrace()} finally {try {input?.close()inputReader?.close()reader?.close()} catch (e: IOException) {e.printStackTrace()}}return strBuilder.toString()
}

Flow读取文件:

/*** 通过Flow方式,获取本地文件*/
private fun getFileInfo() {lifecycleScope.launch {flow {//解析本地json文件,并生成对应字符串val configStr = getAssetJsonInfo(this@MainActivity2, "person.json")//最后将得到的实体类发送到下游emit(configStr)}.map { json ->Gson().fromJson(json, PersonModel::class.java) //通过Gson将字符串转为实体类}.flowOn(Dispatchers.IO) //在flowOn之上的所有操作都是在IO线程中进行的.onStart { Log.d("ddup", "onStart") }.filterNotNull().onCompletion { Log.d("ddup", "onCompletion") }.catch { ex -> Log.d("ddup", "catch:${ex.message}") }.collect {Log.d("ddup", "collect parse result:$it")}}
}

最终打印日志:

2024-07-09 22:00:34.006 12251-12251 ddup com.ddup.flowtest D onStart 2024-07-09 22:00:34.018 12251-12251 ddup com.ddup.flowtest D collect parse result:PersonModel(name=ddup, age=101, interest=earn money...) 2024-07-09 22:00:34.019 12251-12251 ddup com.ddup.flowtest D onCompletion

存在依赖关系的接口请求

我们经常会遇到接口请求依赖另外一个请求的结果,也就是所谓的嵌套请求,嵌套过多的就会出现回调地狱,我们通过FLow来实现一个类似的需求:

lifecycleScope.launch {lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {//将两个flow串联起来 先搜索目的地,然后到达目的地viewModel.getTokenFlows().flatMapConcat {//第二个flow依赖第一个的结果viewModel.getUserFlows(it)}.collect {tv.text = it ?: "error"}}
}

组合多个接口的数据

组合多个接口的数据是一个什么样的场景呢,比如说,我们存在请求多个接口,然后把它们的结果合并起来统一展示或者作为另外一个接口的请求参数,试问一下,该如何实现呢:
第一种,一个一个请求,然后合并;
第二种,并发请求,然后全部请求完了合并。
显然,第二种效果比较高效,下面看下代码:

//分别请求电费、水费、网费,Flow之间是并行关系
suspend fun requestElectricCost(): Flow<SpendModel> =flow {delay(500)emit(SpendModel("电费", 10f, 500))}.flowOn(Dispatchers.IO)suspend fun requestWaterCost(): Flow<SpendModel> =flow {delay(1000)emit(SpendModel("水费", 20f, 1000))}.flowOn(Dispatchers.IO)suspend fun requestInternetCost(): Flow<SpendModel> =flow {delay(2000)emit(SpendModel("网费", 30f, 2000))}.flowOn(Dispatchers.IO)

首先,我们在ViewModel模拟定义了,几个网络请求,接下来合并请求:

lifecycleScope.launch {val electricFlow = viewModel.requestElectricCost()val waterFlow = viewModel.requestWaterCost()val internetFlow = viewModel.requestInternetCost()val builder = StringBuilder()var totalCost = 0fval startTime = System.currentTimeMillis()//NOTE:注意这里可以多个zip操作符来合并Flow,且多个Flow之间是并行关系electricFlow.zip(waterFlow) { electric, water ->totalCost = electric.cost + water.costbuilder.append("${electric.info()},\n").append("${water.info()},\n")}.zip(internetFlow) { two, internet ->totalCost += internet.costtwo.append(internet.info()).append(",\n\n总花费:$totalCost")}.collect {tv.text = it.append(",总耗时:${System.currentTimeMillis() - startTime} ms")Log.d("ddup","${it.append(",总耗时:${System.currentTimeMillis() - startTime} ms")}")}
}

运行结果:
flow11.png
我们看到总花费时间,跟最长请求的时间基本一致。

Flow使用注意事项

多个Flow不能放到一个lifecycleScope.launch里去collect{},因为进入collect{}相当于一个死循环,下一行代码永远不会执行;如果就想写到一个lifecycleScope.launch{}里去,可以在内部再开启launch{}子协程去执行。
错误示范:

lifecycleScope.launch {flow1.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect {}flow2.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect {}
}

正确写法:

lifecycleScope.launch {launch {flow1.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect {}}launch {flow2.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect {}}
}

总结

我们从Flow的生命周期,介绍了flow正确使用姿势,避免资源的浪费,到普通的冷流转换成热流,再到StateFlow代替LiveData,以及它的粘性问题,然后通过SharedFlow解决粘性问题,再到常见应用场景,最后到Flow使用注意事项,基本涵盖了Flow大部分特性、应用场景,这也是Flow学习的最终篇。
创作不易,喜欢的麻烦点赞、收藏、评论,以资鼓励
参考文章
Kotlin Flow响应式编程,StateFlow和SharedFlow

Kotlin | Flow数据流的几种使用场景

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

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

相关文章

如何挑选适合的需求池管理系统?10款优质工具分享

本文将分享10款优质需求池管理工具&#xff1a;PingCode、Worktile、Teambition、Epicor Kinetic、TAPD、SAP IBP、Logility、RELEX Solutions、JIRA、明道云。 在管理项目和产品需求时&#xff0c;正确的工具能够大幅提高效率与透明度。如何从众多需求池工具中选择最适合团队的…

自定义类型:联合体

像结构体一样&#xff0c;联合体也是由一个或者多个成员组成&#xff0c;这些成员可以是不同的类型。 联合体类型的声明 编译器只为最⼤的成员分配⾜够的内存空间。联合体的特点是所有成员共⽤同⼀块内存空间。所以联合体也叫&#xff1a;共⽤体。 输出结果&#xff1a; 联合体…

【基于R语言群体遗传学】-16-中性检验Tajima‘s D及连锁不平衡 linkage disequilibrium (LD)

Tajimas D Test 已经开发了几种中性检验&#xff0c;用于识别模型假设的潜在偏差。在这里&#xff0c;我们将说明一种有影响力的中性检验&#xff0c;即Tajimas D&#xff08;Tajima 1989&#xff09;。Tajimas D通过比较数据集中的两个&#x1d703; 4N&#x1d707;估计值来…

spark shuffle写操作——UnsafeShuffleWriter

PackedRecordPointer 使用long类型packedRecordPointer存储数据。 数据结构为&#xff1a;[24 bit partition number][13 bit memory page number][27 bit offset in page] LongArray LongArray不同于java中long数组。LongArray可以使用堆内内存也可以使用堆外内存。 Memor…

秋招突击——7/9——字节面经

文章目录 引言正文八股MySQL熟悉吗&#xff1f;讲一下MySQL索引的结构&#xff1f;追问&#xff1a;MySQL为什么要使用B树&#xff1f;在使用MySQL的时候&#xff0c;如何避免索引失效&#xff1f;讲一下MySQL的事物有哪几种特征&#xff1f;MySQL的原子性可以实现什么效果&…

【区块链+跨境服务】粤澳健康码跨境互认系统 | FISCO BCOS应用案例

2020 年突如其来的新冠肺炎疫情&#xff0c;让社会治理体系面临前所未见的考验&#xff0c;如何兼顾疫情防控与复工复产成为社会 各界共同努力的目标。区块链技术作为传递信任的新一代信息基础设施&#xff0c;善于在多方协同的场景中发挥所长&#xff0c;从 而为粤澳两地的疫情…

CSS关于居中的问题

文章目录 1. 行内和块级元素自身相对父控件居中1.1. 块级元素相对父控件居中1.2. 行内元素相对于父控件居中 2. 实现单行文字垂直居中3. 子绝父相实现子元素的水平垂直居中3.1. 方案一3.1.1. 示例 3.2. 方案二3.2.1. 示例 3.3. 方案三(推荐)3.3.1. 示例 3.4. 方案四(了解一下) …

AI大模型知识点大梳理_ai大模型的精度以下哪项描述的准确

AI大模型是什么 AI大模型是指具有巨大参数量的深度学习模型&#xff0c;通常**包含数十亿甚至数万亿个参数。**这些模型可以通过学习大量的数据来提高预测能力&#xff0c;从而在自然语言处理、计算机视觉、自主驾驶等领域取得重要突破。 AI大模型的定义具体可以根据参数规模…

短信验证码研究:公开的短信验证码接口、不需要注册的短信验证码接口

短信验证码研究&#xff1a;公开的短信验证码接口、不需要注册的短信验证码接口 0 说明 本文提供了一个短信验证码接口&#xff0c;主要用于以下场景&#xff1a; 1、用于开发调试 2、用于申请验证码困难的企业和个人 3、用于短信验证码认证还没有通过&#xff0c;但是着急…

DBeaver操作MySQL无法同时执行多条语句的解决方法

DBeaver选择数据库连接&#xff0c;在【驱动属性】中将allowMultiQueries允许执行多条语句置为True

泰迪智能科技大数据实验室产品-实训管理平台介绍

高校大数据实验室通常配备有先进的计算机硬件和软件工具&#xff0c;以及专门的数据库和分析平台&#xff0c;以便研究人员和学生能够进行复杂的数据处理、分析和解释。主要利用大数据技术进行科学研究、技术开发和人才培养。 泰迪智能科技实训管理平台作为教学核心&#xff0c…

JS进阶-构造函数

学习目标&#xff1a; 掌握构造函数 学习内容&#xff1a; 构造函数 构造函数&#xff1a; 封装是面向对象思想中比较重要的一部分&#xff0c;js面向对象可以通过构造函数实现的封装。 同样的将变量和函数组合到了一起并能通过this实现数据的共享&#xff0c;所不同的是借助…

小程序需要进行软件测试吗?小程序测试有哪些测试内容?

在如今移动互联网快速发展的时代&#xff0c;小程序已成为人们生活中不可或缺的一部分。然而&#xff0c;面对日益增长的小程序数量和用户需求&#xff0c;小程序的稳定性和质量问题日益突显。因此&#xff0c;对小程序进行软件测试显得尤为重要。 近期的一项调查显示&#xf…

【架构】分布式与微服务架构解析

分布式与微服务架构解析 一、分布式1、什么是分布式架构2、为什么需要分布式架构3、分布式架构有哪些优势&#xff1f;4、分布式架构有什么劣势&#xff1f;5、分布式架构有哪些关键技术&#xff1f;6、基于分布式架构如何提高其高性能&#xff1f;7、如何基于架构提高系统的稳…

【工具】咸鱼小助手,一款咸鱼之王辅助工具

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ Github&#xff1a;咸鱼之王的自动化脚本&#xff0c;自动答题、爬塔、领资源等 下载&#xff1a;(密码:9u22) 咸鱼小助手 文档&#xff1a;腾讯文档 视…

软考《信息系统运行管理员》-3.2信息系统设施运维的环境管理

3.2信息系统设施运维的环境管理 1 计算机机房的选址要求 电子计算机机房地理位置 选择水源充足&#xff0c;电子比较稳定可靠&#xff0c;交通通信方便&#xff0c;自然环境清洁的地点要远离产生粉尘、油烟、有害气体以及生产或存储具有腐蚀性、易燃、易爆物品的工厂、仓库、…

3d模型选不中任何东西是什么原因?---模大狮模型网

在进行3D模型设计过程中&#xff0c;有时会遇到无法选择模型中的任何元素的问题。这种情况可能会影响设计师的工作效率和体验&#xff0c;因此了解问题的原因以及如何解决是至关重要的。本文将探讨在3D建模中遇到无法选中模型元素的原因及解决方法。 一、问题原因分析 无法选中…

07浅谈大语言模型可调节参数tempreture

浅谈temperature 什么是temperature&#xff1f; temperature是大预言模型生成文本时常用的两个重要参数。它的作用体现在控制模型输出的确定性和多样性&#xff1a; 控制确定性&#xff1a; temperature参数可以控制模型生成文本的确定性&#xff0c;大部分模型中temperatur…

医疗器械网络安全 | 漏洞扫描、渗透测试没有发现问题,是否说明我的设备是安全的?

尽管漏洞扫描、模糊测试和渗透测试在评估系统安全性方面是非常重要和有效的工具&#xff0c;但即使这些测试没有发现任何问题&#xff0c;也不能完全保证您的医疗器械是绝对安全的。这是因为安全性的评估是一个多维度、复杂且持续的过程&#xff0c;涉及多个方面和因素。以下是…

数模打怪(一)之层次分析法

一、什么是层次分析法 层次分析法&#xff08;AHP&#xff09;主要用于解决评价类问题&#xff08;可打分&#xff09; 比如哪种方案更好、哪位运动员更优秀等 二、层次分析法的三个步骤 1、建立层次结构 分析题目&#xff0c;找出评价类问题的三要素&#xff1a; &#x…