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

news2024/11/13 15:00:23

目录

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

前言

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

Flow生命周期

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

class MainViewModel : ViewModel() {

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

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

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

我运行看下实际效果:

flow1.gif

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

lifecycleOwner.lifecycleScope.launchWhenStarted {
     viewModel.timeFlow.collect { time ->
         times = time
         Log.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 = time
            Log.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 = time
            Log.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 = time
            Log.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是粘性的,在这里看上去没有问题,但是我们看另外一个例子,我们模拟一个点击登陆的场景,点击登陆按钮,实现登陆并登陆:

//MainViewModel
    private 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(
        // 每个新的订阅者订阅时收到的回放的数目,默认0
        replay: Int = 0,

       // 除了replay数目之外,缓存的容量,默认0
        extraBufferCapacity: 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? = null
    var inputReader: InputStreamReader? = null
    var reader: BufferedReader? = null
    try {
        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 = 0f
    val startTime = System.currentTimeMillis()
    //NOTE:注意这里可以多个zip操作符来合并Flow,且多个Flow之间是并行关系
    electricFlow.zip(waterFlow) { electric, water ->
        totalCost = electric.cost + water.cost
        builder.append("${electric.info()},\n").append("${water.info()},\n")
    }.zip(internetFlow) { two, internet ->
        totalCost += internet.cost
        two.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.coloradmin.cn/o/1916344.html

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

相关文章

leetcode 1421 净现值查询(postgresql)

需求 表: NPV ---------------------- | Column Name | Type | ---------------------- | id | int | | year | int | | npv | int | ---------------------- (id, year) 是该表主键. 该表有每一笔存货的年份, id 和对应净现值的信息. 表: Queries ---------------------- …

Nginx -Web服务器/反向代理/负载均衡

文章目录 一、web服务1.1 nginx安装1.2 配置文件1.3 Nginx处理Web机制 二、反向代理三、负载均衡3.1 分类3.2 负载相关配置文件3.3 keepalive 提高吞吐量3.4 配置浏览器缓存 附、JMeter性能测试工具 以赛促学内容,大概率感觉会使用nginx做web服务,特对nginx做总结归纳. Nginx是…

AI in Finance 金融领域AI应用-基于DeepNLP AI App Store 真实用户评论打分和排名

AI在金融领域应用 AI in Finance 金融服务领域的AI应用和传统的金融智能应用不同。传统金融智能应用包括如风险评估 (Risk assessment), 风险管理&#xff08;Risk management), 欺诈检测 (Fraud Detection&#xff09;等等。 通用AI大模型和人工智能应用如ChatGPT&#xff0c…

PyTorch复现PointNet——模型训练+可视化测试显示

因为项目涉及到3D点云项目&#xff0c;故学习下PointNet这个用来处理点云的神经网络 论文的话&#xff0c;大致都看了下&#xff0c;网络结构有了一定的了解&#xff0c;本博文主要为了下载调试PointNet网络源码&#xff0c;训练和测试调通而已。 我是在Anaconda下创建一个新的…

香蕉派BPI-Wifi6迷你路由器公开发售

Banana Pi BPI-Wifi6 Mini 公开发售。 Banana Pi BPI-Wifi6 Mini 开源路由器采用Triductor TR6560 TR5220 wifi SOC设计&#xff0c;是一款迷你尺寸的wifi6路由器解决方案。内置高性能双核ARM Cortec A9处理器用于WIFI报文转发或智能业务处理&#xff0c;内置高性能LSW和硬件N…

最新浪子授权系统网站源码 全开源免授权版本

最新浪子授权系统网站源码 全开源免授权版本 此版本没有任何授权我已经去除授权&#xff0c;随意二开无任何加密。 更新日志 1.修复不能下载 2.修复不能更新 3.修复不能删除用户 4.修复不能删除授权 5.增加代理后台管理 6.重写授权读取文件 7.修复已经知道漏洞 源码下…

pytorch-RNN实战-正弦曲线预测

目录 1. 正弦数据生成2. 构建网络3. 训练4. 预测5. 完整代码6. 结果展示 1. 正弦数据生成 曲线如下图&#xff1a; 代码如下图&#xff1a; 50个点构成一个正弦曲线随机生成一个0~3之间的一个值&#xff08;随机的原因是防止每次都从相同的点开始&#xff0c;50个点的正弦曲…

云手机批量操作使用场景,从Amazon、TK等软件分析

云手机目前所具备的群控&#xff0c;批量操作&#xff0c;自动化等功能&#xff0c;对于电商&#xff0c;软测&#xff0c;办公&#xff0c;直播&#xff0c;营销等行业有很好的减负作用。 针对于具体的海外APP&#xff0c;云手机具体可以做哪些事情来帮助我们减轻压力&#x…

Docker拉取失败,利用github将镜像推送到阿里云

GITHUB配置 fork https://github.com/tech-shrimp/docker_image_pusher 该项目到自己的账户下。 设置环境变量&#xff0c;其路径如下图 在该项目中 .github/workflows/docker.yaml 找到 env 标签 ALIYUN_REGISTRY: "${{ secrets.ALIYUN_REGISTRY }}"ALIYUN_NAME_S…

AC修炼计划(AtCoder Regular Contest 180) A~C

A - ABA and BAB A - ABA and BAB (atcoder.jp) 这道题我一开始想复杂了&#xff0c;一直在想怎么dp&#xff0c;没注意到其实是个很简单的规律题。 我们可以发现我们住需要统计一下类似ABABA这样不同字母相互交替的所有子段的长度&#xff0c;而每个字段的的情况有&#xff…

600Kg大载重起飞重量多旋翼无人机技术详解

600Kg大载重起飞重量的多旋翼无人机是一种高性能的无人驾驶旋翼飞行器&#xff0c;具有出色的载重能力和稳定的飞行特性。该无人机采用先进的飞行控制系统和高效的动力系统&#xff0c;能够满足各种复杂任务的需求&#xff0c;广泛应用于物资运输、应急救援、森林防火等领域。 …

西门子大手笔又买一家公司,2024年“两买”和“两卖”的背后……

导语 大家好&#xff0c;我是社长&#xff0c;老K。专注分享智能制造和智能仓储物流等内容。 新书《智能物流系统构成与技术实践》 更多的海量【智能制造】相关资料&#xff0c;请到智能制造online知识星球自行下载。 今年&#xff0c;这家全球工业巨头不仅精准出击&#xff0c…

MACOS查看硬盘读写量

一、安装Homebrew 按照提示进行安装 /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"二、安装smartmontools brew install smartmontools三、查看硬盘读写量等信息 sudo smartctl -a /dev/disk0

Python8:线程和进程

1.并发和并行 并发&#xff1a;在逻辑上具备同时处理多个任务的能力&#xff08;其实每时刻只有一个任务&#xff09; 并行&#xff1a;物理上在同一时刻执行多个并发任务 2.线程与进程 一个进程管多个线程&#xff0c;一个进程至少有一个线程 python多线程是假的&#xf…

UML-各种图

什么是类图 定义系统中的类&#xff0c;描述类的内部结构&#xff08;属性、方法等&#xff09;&#xff0c;表示类之间的关系&#xff08;泛化、实现、依赖、关联、聚合、组合&#xff09;。 UML表示类图 上图中左侧图形是一个常见的类图&#xff0c; 类名&#xff1a;在顶…

Qt:15.布局管理器(QVBoxLayout-垂直布局、QHBoxLayout-水平布局、QGridLayout-网格布局、拉伸系数,控制控件显示的大小)

目录 一、QVBoxLayout-垂直布局&#xff1a; 1.1QVBoxLayout介绍&#xff1a; 1.2 属性介绍&#xff1a; 1.3细节理解&#xff1a; 二、QHBoxLayout-水平布局&#xff1a; 三、QGridLayout-网格布局&#xff1a; 3.1QGridLayout介绍&#xff1a; 3.2常用方法&#xff1a…

iMazing 3.0.3.1Mac中文破解版下载安装激活

今天&#xff0c;小编要分享的是Mac下一款可以帮助用户管理IOS设备的软件——iMazing&#xff0c;之前&#xff0c;小编也分享的过类似的软件&#xff0c;iMazing却有独特之处。小子这次带来的是3.0.3.1版本。 iMazing 3是一款iOS设备管理软件&#xff0c;该软件支持对基于iOS…

【STM32学习】cubemx配置,串口的使用,串口发送接收函数使用,以及串口重定义、使用printf发送

1、串口的基本配置 选择USART1&#xff0c;选择异步通信&#xff0c;设置波特率 选择后&#xff0c;会在右边点亮串口 串口引脚是用来与其他设备通信的&#xff0c;如在程序中打印发送信息&#xff0c;电脑上打开串口助手&#xff0c;就会收到信息。 串口的发送接收&#xff0…

机器学习筑基篇,容器调用显卡计算资源,Ubuntu 24.04 快速安装 NVIDIA Container Toolkit!...

[ 知识是人生的灯塔,只有不断学习,才能照亮前行的道路 ] Ubuntu 24.04 安装 NVIDIA Container Toolkit 什么是 NVIDIA Container Toolkit? 描述:NVIDIA Container Toolkit(容器工具包)使用户能够构建和运行 GPU 加速的容器,该工具包括一个容器运行时库和实用程序,用于自动…

新能源汽车充电站远程监控系统S275钡铼技术无线RTU

新能源汽车充电站的远程监控系统在现代城市基础设施中扮演着至关重要的角色&#xff0c;而钡铼技术的S275无线RTU作为一款先进的物联网数据监测采集控制短信报警终端&#xff0c;为充电站的安全运行和高效管理提供了强大的技术支持。 技术特点和功能 钡铼S275采用了基于UCOSI…