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天前
|
安全 数据处理 Android开发
构建高效Android应用:Kotlin协程的实践之路
【4月更文挑战第26天】 在面对现代Android开发时,性能优化和流畅的用户体验成为了开发者们追求的目标。Kotlin作为一种现代化的编程语言,通过其协程特性为Android应用带来了前所未有的并发处理能力。本文将深入探讨如何利用Kotlin协程提升Android应用的响应性和效率,同时保持代码的简洁性。我们将从协程的基础概念出发,逐步揭示如何在实际应用中运用这些强大的工具,以及它们如何改善应用架构和用户交互体验。
|
12天前
|
移动开发 Java Android开发
构建高效Android应用:探究Kotlin协程的优势与实践
【4月更文挑战第22天】 在移动开发领域,性能优化和响应性是衡量应用程序质量的关键指标。近年来,Kotlin语言因其简洁性和功能性在Android社区中获得了广泛认可。特别是Kotlin协程,作为一种新的并发处理机制,正在改变开发者编写异步和后台任务的方式。本文将探讨Kotlin协程的核心优势,并通过实例展示如何在Android应用中实现它们以提高性能和用户体验。
|
14天前
|
API 数据库 Android开发
构建高效Android应用:探究Kotlin协程的优化实践
【4月更文挑战第20天】 在现代Android开发中,Kotlin协程以其轻量级线程管理和非阻塞I/O操作的优势成为提升应用性能和响应性的重要工具。本文深入分析Kotlin协程的核心原理,探讨其在Android平台上实现高效并发编程的方法,并通过具体实例演示如何利用协程改进应用架构。我们将从协程的基本概念出发,逐步解析其与线程、回调和异步任务的关系,最终展示如何通过协程简化代码结构,提高运行效率,并确保用户界面的流畅性。
21 11
|
2月前
|
调度 数据库 Android开发
构建高效Android应用:Kotlin协程的实践与优化
在Android开发领域,Kotlin以其简洁的语法和平台友好性成为了开发的首选语言。其中,Kotlin协程作为处理异步任务的强大工具,它通过提供轻量级的线程管理机制,使得开发者能够在不阻塞主线程的情况下执行后台任务,从而提升应用性能和用户体验。本文将深入探讨Kotlin协程的核心概念,并通过实例演示如何在实际的Android应用中有效地使用协程进行网络请求、数据库操作以及UI的流畅更新。同时,我们还将讨论协程的调试技巧和常见问题的解决方法,以帮助开发者避免常见的陷阱,构建更加健壮和高效的Android应用。
36 4
|
1月前
|
Java Android开发 开发者
构建高效Android应用:Kotlin协程的实践与优化
在响应式编程范式日益盛行的今天,Kotlin协程作为一种轻量级的线程管理解决方案,为Android开发带来了性能和效率的双重提升。本文旨在探讨Kotlin协程的核心概念、实践方法及其在Android应用中的优化策略,帮助开发者构建更加流畅和高效的应用程序。通过深入分析协程的原理与应用场景,结合实际案例,本文将指导读者如何优雅地解决异步任务处理,避免阻塞UI线程,从而优化用户体验。
|
4天前
|
移动开发 Java Android开发
构建高效Android应用:Kotlin协程的实践之路
【4月更文挑战第30天】在移动开发领域,随着用户需求的不断增长和设备性能的持续提升,实现流畅且高效的用户体验已成为开发者的首要任务。针对Android平台,Kotlin协程作为一种新兴的异步编程解决方案,以其轻量级线程管理和简洁的代码逻辑受到广泛关注。本文将深入探讨Kotlin协程的概念、优势以及在实际Android应用中的运用,通过实例演示如何利用协程提升应用性能和响应能力,为开发者提供一条构建更高效Android应用的实践路径。
|
4天前
|
移动开发 调度 Android开发
构建高效Android应用:Kotlin协程的实践之路
【4月更文挑战第30天】 在移动开发领域,性能优化与流畅的用户体验始终是开发者追求的目标。随着Kotlin语言在Android开发中的普及,其提供的协程特性成为了解决异步编程问题的有力工具。本文将通过深入分析Kotlin协程的原理与实践,展示如何在Android应用中利用协程提升响应速度和处理效率,同时保证代码的简洁性和可维护性。我们将从基本概念出发,逐步深入到协程的高级使用场景,帮助开发者构建更加高效的Android应用。
|
4天前
|
移动开发 调度 Android开发
构建高效Android应用:Kotlin协程的实践与优化
【4月更文挑战第30天】在移动开发领域,性能和响应性是衡量应用质量的关键指标。对于Android平台而言,Kotlin协程作为一种新兴的异步编程解决方案,提供了更为简洁和高效的处理并发任务的能力。本文将深入探讨Kotlin协程的核心原理,以及如何通过它们来提升Android应用的性能。我们将从基础概念出发,逐步介绍协程的创建、管理以及与Android UI线程的交互,并最终展示如何优化现有代码以利用协程的优势。
|
5天前
|
移动开发 数据库 Android开发
构建高效Android应用:探究Kotlin协程的优化实践
【4月更文挑战第29天】在移动开发领域,尤其是Android平台上,性能优化一直是开发者关注的重点。近年来,Kotlin语言凭借其简洁性和功能性成为Android开发的热门选择。其中,Kotlin协程作为一种轻量级的并发处理机制,为编写异步代码、网络请求和数据库操作提供了极大的便利。本文将深入探讨Kotlin协程在Android应用中的性能优化技巧,帮助开发者构建更加高效的应用程序。
|
6天前
|
API 调度 Android开发
构建高效Android应用:Kotlin协程的实践之路
【4月更文挑战第28天】随着移动应用开发的不断深入,开发者们追求更高效的编码方式和性能优化。Kotlin作为一种现代的编程语言,其协程特性提供了异步编程的新范式,使得处理并发任务更加简洁高效。本文将探讨如何在Android应用开发中利用Kotlin协程提升性能和用户体验,同时确保代码的可读性和可维护性。通过实例分析和代码演示,我们将深入了解协程在Android中的实际应用及其带来的优势。
12 1