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 也可以自己设置,比较简单,感兴趣的同学可自行动手试验。

目录
相关文章
|
8天前
|
数据处理 开发者 Kotlin
利用Kotlin Flow简化数据流管理
随着移动端应用的复杂化,数据流管理成为一大挑战。Kotlin Flow作为一种基于协程的响应式编程框架,可简化数据流处理并支持背压机制,有效避免应用崩溃。本文通过解答四个常见问题,详细介绍Kotlin Flow的基本概念、创建方法及复杂数据流处理技巧,帮助开发者轻松上手,提升应用性能。
36 16
|
2天前
|
存储 API 数据库
Kotlin协程与Flow的魅力——打造高效数据管道的不二法门!
在现代Android开发中,Kotlin协程与Flow框架助力高效管理异步操作和数据流。协程采用轻量级线程管理,使异步代码保持同步风格,适合I/O密集型任务。Flow则用于处理数据流,支持按需生成数据和自动处理背压。结合两者,可构建复杂数据管道,简化操作流程,提高代码可读性和可维护性。本文通过示例代码详细介绍其应用方法。
11 2
|
9天前
|
数据处理 Kotlin
掌握这项Kotlin技能,让你的数据流管理不再头疼!Flow的秘密你解锁了吗?
【9月更文挑战第12天】随着移动应用发展,数据流管理日益复杂。Kotlin Flow作为一种基于协程的异步数据流处理框架应运而生,它可解耦数据的生产和消费过程,简化数据流管理,并支持背压机制以防应用崩溃。本文通过四个问题解析Kotlin Flow的基础概念、创建方式、复杂数据流处理及背压实现方法,助您轻松掌握这一高效工具,在实际开发中更从容地应对各种数据流挑战,提升应用性能。
28 8
|
10天前
|
数据处理 API 数据库
揭秘Kotlin Flow:迈向响应式编程的黄金钥匙
【9月更文挑战第11天】在现代软件开发中,异步编程与数据处理对于构建高性能应用至关重要。Kotlin Flow作为协程库的一部分,提供了简洁高效的API来处理数据流。本文将通过实例引导你从零开始学习Kotlin Flow,掌握构建响应式应用的方法。Flow是一种冷流,仅在订阅时才开始执行,支持map、filter等操作符,简化数据处理。
25 7
|
8天前
|
存储 数据处理 Kotlin
Kotlin Flow背后的神秘力量:背压、缓冲与合并策略的终极揭秘!
【9月更文挑战第13天】Kotlin Flow 是 Kotlin 协程库中处理异步数据流的强大工具,本文通过对比传统方法,深入探讨 Flow 的背压、缓冲及合并策略。背压通过 `buffer` 函数控制生产者和消费者的速率,避免过载;缓冲则允许数据暂存,使消费者按需消费;合并策略如 `merge`、`combine` 和 `zip` 则帮助处理多数据源的整合。通过这些功能,Flow 能更高效地应对复杂数据处理场景。
22 2
|
8天前
|
移动开发 定位技术 Android开发
「揭秘高效App的秘密武器」:Kotlin Flow携手ViewModel,打造极致响应式UI体验,你不可不知的技术革新!
【9月更文挑战第12天】随着移动开发领域对响应式编程的需求增加,管理应用程序状态变得至关重要。Jetpack Compose 和 Kotlin Flow 的组合提供了一种优雅的方式处理 UI 状态变化,简化了状态管理。本文探讨如何利用 Kotlin Flow 增强 ViewModel 功能,构建简洁强大的响应式 UI。
21 3
|
9天前
|
数据库 Kotlin
Kotlin中的冷流和热流以及如何让Flow停下来
本文介绍了Kotlin中`Flow`的概念及其类型,包括冷流(Cold Flow)、热流`SharedFlow`及具有最新值的`StateFlow`。文中详细描述了每种类型的特性与使用场景,并提供了停止`Flow`的方法,如取消协程、使用操作符过滤及异常处理。通过示例代码展示了如何运用这些概念。
17 2
|
2天前
|
API 数据处理 数据库
掌握 Kotlin Flow 的艺术:让无限数据流处理变得优雅且高效 —— 实战教程揭秘如何在数据洪流中保持代码的健壮与灵活
Kotlin Flow 是一个强大的协程 API,专为处理异步数据流设计。它适合处理网络请求数据、监听数据库变化等场景。本文通过示例代码展示如何使用 Kotlin Flow 管理无限流,如实时数据流。首先定义了一个生成无限整数的流 `infiniteNumbers()`,然后结合多种操作符(如 `buffer`、`onEach`、`scan`、`filter`、`takeWhile` 和 `collectLatest`),实现对无限流的优雅处理,例如计算随机数的平均值并在超过阈值时停止接收新数据。这展示了 Flow 在资源管理和逻辑清晰性方面的优势。
10 0
|
1月前
|
前端开发 编译器 测试技术
Kotlin Multiplatform 跨平台开发的优化策略与实践
本文深入讲解Kotlin Multiplatform(KMP)的优化策略与实践。KMP是由JetBrains推出的开源技术,允许跨平台共享代码同时保持原生优势。文章覆盖KMP核心概念、性能优化技巧(如代码结构优化、利用`expect`/`actual`关键字、Kotlin/Native性能特性等),以及在移动、桌面和Web应用的实际案例分析。此外,还介绍了如何利用KMP生态系统工具进行快速开发,并展望了KMP的未来发展。
42 0
|
8天前
|
Android开发 开发者 Kotlin
告别AsyncTask:一招教你用Kotlin协程重构Android应用,流畅度飙升的秘密武器
【9月更文挑战第13天】随着Android应用复杂度的增加,有效管理异步任务成为关键。Kotlin协程提供了一种优雅的并发操作处理方式,使异步编程更简单直观。本文通过具体示例介绍如何使用Kotlin协程优化Android应用性能,包括网络数据加载和UI更新。首先需在`build.gradle`中添加coroutines依赖。接着,通过定义挂起函数执行网络请求,并在`ViewModel`中使用`viewModelScope`启动协程,结合`Dispatchers.Main`更新UI,避免内存泄漏。使用协程不仅简化代码,还提升了程序健壮性。
21 1