Kotlin 学习笔记(五)—— Flow 数据流学习实践指北(一)(上)

简介: Kotlin 学习笔记(五)—— Flow 数据流学习实践指北(一)(上)

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

最近马斯克收购了推特之后,马上就裁掉了 50% 的推特员工,这不禁让我想起了灭霸的响指... 还有苹果、亚马逊冻结招聘,英特尔、Lyft开启裁员计划,国内外都不好过啊,大家都开始勒紧裤腰带了···那么,我们打工人是不是也该刷刷题了···(笑Cry.jpg)

Kotlin 学习笔记艰难地来到了第五篇~ 在这一篇主要会说 Flow 的基本知识和实例。由于 Flow 内容较多,所以会分几个小节来讲解,这是第一小节,文章后面会结合一个实例介绍 Flow 在实际开发中的应用。

首先回想一下,在协程中处理某个操作,我们只能返回单个结果;而 Flow 可以按顺序返回多个结果,在官方文档中,Flow 被翻译为 数据流,这也说明了 Flow 适用于多值返回的场景。

Flow 是以协程为基础构建的,所以它可通过异步的方式处理一组数据,所要处理的数据类型必须相同,比如:Flow<Int>是处理整型数据的数据流。

Flow 一般包含三个部分:

1)提供方:负责生成数据并添加到 Flow 中,得益于协程,Flow 可以异步生成数据;

2)中介(可选):可对 Flow 中的值进行操作、修改;也可修改 Flow 本身的一些属性,如所在线程等;

3)使用方:接收并使用 Flow 中的值。

提供方:生产者,使用方:消费者,典型的生产者消费者模式。


1. Flow 概述


Flow 是一个异步数据流,它可以顺序地发出数据,通过流上的一些中间操作得出结果;若出错可抛出异常。这些 “流上的中间操作” 包括但不限于 mapfiltertakezip 等等方法。这些中间操作是链式的,可以在后面再次添加其他操作方法,并且也不是挂起函数,它们只是构建了一条链式的操作并实时返回结果给后面的操作步骤。

流上的终端操作符要么是挂起函数,例如 collectsingletoList 等等,要么是在给定作用域内开始收集流的 launchIn 操作符。前半句好理解,后半句啥意思?这就得看一下 launchIn 这个终端操作符的作用了。它里面是这样的:

//code 1
public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {
    collect() // tail-call
}

原来 launchIn 方法可以传入一个 CoroutineScope 协程作用域,然后在这个作用域里面调用 collect 方法。lifecycleScopeMainScope() 这些都是协程作用域,所以 launchIn  方法只不过是 scope.launch { flow.collect() } 的一种简写。

流的执行也被称之为收集流,并且是以挂起的方式,不是阻塞的。流最终的执行成功与否取决于流上的操作是否全部执行成功。collect 函数就是最常见的收集流函数。


1.1 冷流与热流


冷流(Cold Flow):在数据被使用方订阅后,即调用 collect 方法之后,提供方才开始执行发送数据流的代码,通常是调用 emit 方法。即不消费,不生产,多次消费才会多次生产。使用方和提供方是一对一的关系。

热流(Hot Flow):无论有无使用方,提供方都可以执行发送数据流的操作,提供方和使用方是一对多的关系。热流就是不管有无消费,都可生产。

SharedFlow 就是热流的一种,任何流也可以通过 stateInshareIn 操作转化为热流,或者通过 produceIn 操作将流转化为一个热通道也能达到目的。本篇只介绍冷流相关知识,热流会在后面小节讲解~


2. Flow 构建方法


Flow 的构造方法有如下几种:

1、 flowOf() 方法。用于快速创建流,类似于 listOf() 方法,下面是它的源码:

//code 2
public fun <T> flowOf(vararg elements: T): Flow<T> = flow {
    for (element in elements) {
        emit(element)
    }
}

所以用法也比较简单:

//code 3
val testFlow = flowOf(65,66,67)
lifecycleScope.launch {
    testFlow.collect {
        println("输出:$it")
    }
}
//打印结果:
//输出:65
//输出:66
//输出:67

注意到 Flow 初始化的时候跟其他对象一样,作用域在哪儿都可以,但 collect 收集的时候就需要放在协程里了,因为 collect 是个挂起函数。

2、asFlow() 方法。是集合的扩展方法,可将其他数据转换成 Flow,例如 Array 的扩展方法:

//code 4
public fun <T> Array<T>.asFlow(): Flow<T> = flow {
    forEach { value ->
        emit(value)
    }
}

不仅 Array 扩展了此方法,各种其他数据类型的数组都扩展了此方法。所以集合可以很方便地构造一个 Flow。

3、flow {···} 方法。这个方法可以在其内部顺序调用 emit 方法或 emitAll 方法从而构造一个顺序执行的 Flow。emit 是发射单个值;emitAll 是发射一个流,这两个方法分别类似于 list.add(item)list.addAll(list2) 方法。flow {···} 方法的源码如下:

//code 5
public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T> = SafeFlow(block)

需要额外注意的是,flow 后面的 lambda 表达式是一个挂起函数,里面不能使用不同的 CoroutineContext 来调用 emit 方法去发射值。因此,在 flow{...} 中不要通过创建新协程或使用 withContext 代码块在另外的 CoroutineContext 中调用 emit 方法,否则会报错。如果确实有这种需求,可以使用 channelFlow 操作符。

//code 6
val testFlow = flow {
    emit(23)
//    withContext(Dispatchers.Main) { // error
//        emit(24)
//    }
    delay(3000)
    emitAll(flowOf(25,26))
}

4、channelFlow {···} 方法。这个方法就可以在内部使用不同的 CoroutineContext 来调用 send 方法去发射值,而且这种构造方法保证了线程安全也保证了上下文的一致性,源码如下:

//code 7
public fun <T> channelFlow(@BuilderInference block: suspend ProducerScope<T>.() -> Unit): Flow<T> =
    ChannelFlowBuilder(block)

一个简单的使用例子:

//code 8
val testFlow1 = channelFlow {
    send(20)
    withContext(Dispatchers.IO) { //可切换线程
        send(22)
    }
}
lifecycleScope.launch {
    testFlow1.collect {
        println("输出 = $it")
    }
}

5、MutableStateFlowMutableSharedFlow 方法:都可以定义相应的构造函数去创建一个可以直接更新的热流。由于篇幅有限,有关热流的知识后面小节会再说明。


3. Flow 常用的操作符


Flow 的使用依赖于众多的操作符,这些操作符可以大致地分为 中间操作符末端操作符 两大类。中间操作符是流上的中间操作,可以针对流上的数据做一些修改,是链式调用。中间操作符与末端操作符的区别是:中间操作符是用来执行一些操作,不会立即执行,返回值还是个 Flow;末端操作符就会触发流的执行,返回值不是 Flow。

一个完整的 Flow 是由 Flow 构建器Flow 中间操作符Flow 末端操作符 组成,如下示意图所示:

image.png


3.1 collect 末端操作符


最常见的当然是 collect 操作符。它是个挂起函数,需要在协程作用域中调用;并且它是一个末端操作符,末端操作符就是实际启动 Flow 执行的操作符,这一点跟 RxJava 中的 Observable 对象的执行很像。

熟悉 RxJava 的同学知道,在 RxJava 中,Observable 对象的执行开始时机是在被一个订阅者(subscriber) 订阅(subscribe) 的时候,即在 subscribe 方法调用之前,Observable 对象的主体是不会执行的。

Flow 也是相同的工作原理,Flow 在调用 collect 操作符收集流之前,Flow 构建器和中间操作符都不会执行。举个栗子:

//code 9
val testFlow2 = flow {
    println("++++ 开始")
    emit(40)
    println("++++ 发出了40")
    emit(50)
    println("++++ 发出了50")
}
lifecycleScope.launch {
    testFlow2.collect{
        println("++++ 收集 = $it")
    }
}
// 输出结果:
//com.example.myapplication I/System.out: ++++ 开始
//com.example.myapplication I/System.out: ++++ 收集 = 40
//com.example.myapplication I/System.out: ++++ 发出了40
//com.example.myapplication I/System.out: ++++ 收集 = 50
//com.example.myapplication I/System.out: ++++ 发出了50

从输出结果可以看出,每次到 collect 方法调用时,才会去执行 emit 方法,而在此之前,emit 方法是不会被调用的。这种 Flow 就是冷流。


3.2 reduce 末端操作符


reduce 也是一个末端操作符,它的作用就是将 Flow 中的数据两两组合接连进行处理,跟 Kotlin 集合中的 reduce 操作符作用相同。举个栗子:

//code 10
private fun reduceOperator() {
    val testFlow = listOf("w","i","f","i").asFlow()
    CoroutineScope(Dispatchers.Default).launch {
        val result = testFlow.reduce { accumulator, value ->
            println("+++accumulator = $accumulator  value = $value")
            "$accumulator$value"
        }
        println("+++final result = $result")
    }
}
//输出结果:
//com.example.myapplication I/System.out: +++accumulator = w  value = i
//com.example.myapplication I/System.out: +++accumulator = wi  value = f
//com.example.myapplication I/System.out: +++accumulator = wif  value = i
//com.example.myapplication I/System.out: +++final result = wifi

看结果就知道,reduce 操作符的处理逻辑了,两个值处理后得到的新值作为下一轮中的输入值之一,这就是两两接连进行处理的意思。

图1 中出现的 toList 操作符也是一种末端操作符,可以将 Flow 返回的多个值放进一个 List 中返回,返回的 List 也可以自己设置,比较简单,感兴趣的同学可自行动手试验。

目录
相关文章
|
28天前
|
Java Kotlin
Kotlin学习教程(七)
《Kotlin学习教程(七)》主要介绍了Lambda表达式,这是一种匿名函数,广泛用于简化代码。文章通过与Java 8 Lambda表达式的对比,展示了Kotlin中Lambda的基本语法、参数声明、函数体定义及如何作为参数传递。示例包括按钮事件处理和字符串比较,突出了Lambda表达式的简洁性和实用性。
35 4
|
30天前
|
Java Kotlin 索引
Kotlin学习教程(三)
Kotlin学习教程(三)
17 4
|
30天前
|
Java Kotlin
Kotlin学习教程(二)
Kotlin学习教程(二)
35 4
|
30天前
|
安全 Java 编译器
Kotlin学习教程(一)
Kotlin学习教程(一)
36 4
|
29天前
|
存储 Java API
Kotlin学习教程(六)
《Kotlin学习教程(六)》介绍了Kotlin中的注解、反射、扩展函数及属性等内容。注解用于添加元数据,反射支持运行时自省,扩展则允许为现有类添加新功能,无需修改原类。本文还详细解释了静态扩展的使用方法,展示了如何通过companion object定义静态部分,并对其进行扩展。
16 2
|
29天前
|
存储 设计模式 JSON
Kotlin学习教程(五)
《Kotlin学习教程(五)》介绍了Kotlin中的泛型、嵌套类、内部类、匿名内部类、枚举、密封类、异常处理、对象、单例、对象表达式、伴生对象、委托等高级特性。具体内容包括泛型的定义和类型擦除、嵌套类和内部类的区别、匿名内部类的创建、枚举类的使用、密封类的声明和用途、异常处理机制、对象和单例的实现、对象表达式的应用、伴生对象的作用以及类委托和属性委托的使用方法。通过这些内容,读者可以深入理解Kotlin的高级特性和设计模式。
16 1
|
1月前
|
Java 开发者 Kotlin
Kotlin学习笔记- 类与构造器
本篇笔记详细介绍了Kotlin中的类与构造器,包括类的基本概念、主构造器与次构造器的区别、构造器中参数的使用规则、类的继承以及构造器在继承中的应用等。通过具体示例,解释了如何在类中定义属性、实现构造逻辑,并探讨了Kotlin类的继承机制和Any类的作用。此外,还简要介绍了包的概念及其在组织代码中的作用。适合初学者深入理解Kotlin面向对象编程的核心概念。
30 3
|
1月前
|
安全 IDE Java
Kotlin 学习笔记- 空类型和智能类型转换
Kotlin 学习笔记聚焦于空类型和智能类型转换,深入解析非空与可空类型、安全调用操作符、Elvis 运算符、非空断言运算符及智能类型转换等内容,助你高效掌握 Kotlin 语言特性,避免 NullPointException 异常,提升代码质量。
29 2
|
1月前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
25 1
|
2月前
|
Android开发 开发者 Kotlin
告别AsyncTask:一招教你用Kotlin协程重构Android应用,流畅度飙升的秘密武器
【9月更文挑战第13天】随着Android应用复杂度的增加,有效管理异步任务成为关键。Kotlin协程提供了一种优雅的并发操作处理方式,使异步编程更简单直观。本文通过具体示例介绍如何使用Kotlin协程优化Android应用性能,包括网络数据加载和UI更新。首先需在`build.gradle`中添加coroutines依赖。接着,通过定义挂起函数执行网络请求,并在`ViewModel`中使用`viewModelScope`启动协程,结合`Dispatchers.Main`更新UI,避免内存泄漏。使用协程不仅简化代码,还提升了程序健壮性。
84 1
下一篇
无影云桌面