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;正确的工具能够大幅提高效率与透明度。如何从众多需求池工具中选择最适合团队的…

第一节 SHELL脚本中的常用命令(2)

二,网络管理命令nmcli 1.查看网卡 # 或者先用ip addr或ip a等查看网卡 ip a s 网卡名 ifconfig 网卡名 nmcil device show 网卡名 nmcil device status nmcil connection show 网卡名2.设置网卡 a)当网卡未被设置过时 设置dncp网络工作模式 nmcil connection add con-name…

Rust编程-编写自动化测试

编写单元测试步骤&#xff1a; 1. 准备所需的数据 2. 调用需要测试的代码 3. 断言运行结果与我们所期望的一致 Rust的test元数据&#xff1a; #[cfg(test)]&#xff1a;是一个属性宏&#xff08;attribute macro&#xff09;。用于控制特定的代码段仅在测试环境中编译…

自定义类型:联合体

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

size_t 数据类型的好处

什么是size_t size_t 类型在不同的平台上对应不同的底层整数类型&#xff0c;具体取决于平台的指针大小。size_t 主要用于表示大小和长度&#xff0c;如数组的元素数量、缓冲区的大小等&#xff0c;它的设计目的是为了匹配指针的大小&#xff0c;以避免类型不匹配引起的错误。…

代码随想录算法训练营DAY58|101.孤岛的总面积、102.沉没孤岛、103. 水流问题、104.建造最大岛屿

忙。。。写了好久。。。。慢慢补吧。 101.孤岛的总面积 先把周边的岛屿变成水dfs def dfs(x, y, graph, s):if x<0 or x>len(graph) or y<0 or y>len(graph[0]) or graph[x][y]0:return sgraph[x][y]0s1s dfs(x1, y, graph, s)s dfs(x-1, y, graph, s)s dfs(…

【爬虫入门知识讲解:xpath】

3.3、xpath xpath在Python的爬虫学习中&#xff0c;起着举足轻重的地位&#xff0c;对比正则表达式 re两者可以完成同样的工作&#xff0c;实现的功能也差不多&#xff0c;但xpath明显比re具有优势&#xff0c;在网页分析上使re退居二线。 xpath 全称为XML Path Language 一种…

软考高级第四版备考--第16天(规划沟通管理)Plan Communication Management

定义&#xff1a;基于每个干系人或干系人群体的信息需求、可用的组织资产以及具体的项目的需求&#xff0c;为项目沟通活动制定恰当的方法和计划的过程。 作用&#xff1a; 及时向干系人提供相关信息&#xff1b;引导干系人有效参与项目&#xff1b;编制书面沟通计划&#xf…

【基于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;估计值来…

vue项目中常见的一些preset及其关系

Babel的作用 Babel主要用途是用来做js代码转换的&#xff0c;将最新的js语法或者api转换成低版本浏览器可兼容执行的代码。 语法兼容是指一些浏览器新特性增加的js写法&#xff0c;例如箭头函数 ()>{}&#xff1b;低版本的浏览器无法识别这些&#xff0c;会导致一些语法解…

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的原子性可以实现什么效果&…

GESP C++ 三级真题(2023年9月)T2 进制判断

进制判断 问题描述 N进制数指的是逢N进一的计数制。例如&#xff0c;人们日常生活中大多使用十进制计数&#xff0c; 而计算机底层则一般使用二进制。除此之外&#xff0c;八进制和十六进制在一些场合也是 常用的计数制(十六进制中&#xff0c;一般使用字母A至F表示十至十五…

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

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

uniapp上传文件并获取上传进度

1. 上传普通文件 uni.chooseMessageFile({count: 1,success: (res) > {console.log(res)console.log("res123456", res.tempFiles[0].path)const uploadTask uni.uploadFile({url: http://localhost:8000/demo,filePath: res.tempFiles[0].path,name: file,form…

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

各种音频处理器

在HiFi&#xff08;高保真&#xff09;音频系统中&#xff0c;通常需要使用一些特定类型的音频处理器&#xff0c;以确保音频信号的高保真和优质输出。以下是一些常见的音频处理器类型及其在HiFi系统中的应用&#xff1a; DAC&#xff08;数模转换器&#xff09;&#xff1a; …