探索 Kotlin 中的 Flow API:异步编程的艺术

363 阅读5分钟

前言

在这篇博客中,我们将学习 Kotlin 中的 Flow基本 API。Kotlin 提供了许多开箱即用的特性,帮助我们在项目中完成各种任务。 在 Android 开发中,Kotlin 的 Flow API 非常有用。 这篇文章适合对 Kotlin 的 Flow API 感兴趣但不了解的人。我们的目标是让你理解什么是 Kotlin 的 Flow API。 让我们开始学习 Kotlin 的 Flow API。

什么是 Flow

Flow 是一种异步数据流(通常来自一个任务),它向收集器发射值并在完成时伴随或不伴随异常。

通过一个例子,这一点会更清楚。让我们看一个标准的计步器应用示例。

假设我们有一个任务:记录用户的步数,并发射步数的累计值,如 1 步、2 步、3 步,依此类推。任务可以正常完成,也可以因为异常而中断。

因此,这里有一个任务会发射一些值,这些值将被收集器收集。

Flow 的主要组件

Flow 的主要组件如下:

  • Flow Builder:负责构建并发射数据流。
  • Operator:用于转换数据格式。
  • Collector:负责收集并处理数据流。

让我们用一个更贴切的比喻来理解这些组件:

  • Flow Builder -> 水管系统中的水泵
  • Operator -> 水管中的过滤器和调节阀
  • Collector -> 水龙头

这个比喻将 Flow 比作水的流动系统,从水泵(Flow Builder)开始,经过过滤器和调节阀(Operator)处理后,最终通过水龙头(Collector)输出。

Flow Builder

Flow Builder 就像是水管系统中的水泵,负责开始流动并推动数据流。它启动了整个流程,让数据开始流动。

Operator

Operator 就像是水管中的过滤器和调节阀,负责处理和转换流经的数据。它可以对数据进行过滤、转换或调整,以满足特定的需求。

Collector

Collector 类似于水龙头,它负责最终将处理过的数据提供给消费者。当数据流经过了所有的处理和转换后,Collector 将其输出供后续的使用。

流程图

dP1FJuD04CNl-oacz6oYZQH93wRs3rTDr9FniCBORBhCaDb1cnY_ky12XLGlNWXlvlU6zrW5jQ9rwOqwzGYPvmzOs-1os71PCI5fC2OS75LMR0dDKrtkAaiAdmO06Ze_x8LhAZRiMM2soDAhTHftB0LAFx_Ut6RffVaQdtlbu__FjEQaJPnCw77491QgdIzG4h1XU3ydxoeKgnofGPnZJGgF-S2Tb44gsZR6JEZTKL78UjXQ.png

Flow API 源码

public fun interface FlowCollector<in T> {
    public suspend fun emit(value: T)
}

public interface Flow<out T> {
    public suspend fun collect(collector: FlowCollector<T>)
}

在这段代码中,我们定义了两个关键接口:FlowCollectorFlow

  • FlowCollector 是一个函数式接口,用于发射值到流中。它包含一个名为 emit 的挂起函数,该函数接受一个泛型参数 value,用于发射流中的值。由于 emit 函数是挂起函数,因此它可以在协程中被调用。

  • Flow 接口表示一个可以发射值的异步数据流。它是一个泛型接口,接受一个类型参数 T,表示流中的值的类型。Flow 接口定义了一个名为 collect 的挂起函数,该函数接受一个 FlowCollector 对象作为参数,并用于收集流中的值。由于 collect 函数也是挂起函数,因此它也可以在协程中被调用。

这两个接口是 Kotlin 协程库中实现异步数据流的基础,它们提供了一种简洁而灵活的方式来处理异步数据流。通过实现 Flow 接口和使用 FlowCollector 接口的 emit 函数,我们可以轻松地创建和操作异步数据流。

Flow 的基本示例

flow {
    (1..5).forEach {
        emit(it)
    }
}.filter {
    it % 2 == 0
}.map {
    it * it
}.collect {
    println(it)
}
  • flow { } -> Flow Builder
  • filter { } -> Operator
  • map { } -> Operator
  • collect { } -> Collector

让我们逐步分析这段代码:

首先,我们有一个 Flow Builder 发射从 1 到 5 的整数。 接着,我们有一个 filter 操作符,它过滤出偶数。这是一个中间操作符。 然后,我们有一个 map 操作符,它将每个偶数平方。这也是一个中间操作符。 最后,我们有一个收集器,打印出处理后的值:4、16。

Flow Builder 的类型

Flow Builder 有四种类型:

flowOf(): 用于从一组给定的项目创建 Flow。
asFlow(): 一个扩展函数,用于将类型转换为 Flow。
flow{}: 这是我们在 上面示例中使用的。
channelFlow{}: 这个构建器使用内部的 send 方法创建 Flow。

示例:

// 使用 flowOf() 创建一个空的流
flowOf()

// 使用 flowOf() 创建一个包含特定元素的流,并收集和打印每个元素
flowOf(4, 2, 5, 1, 7)
    .collect {
        Log.d(TAG, it.toString())
    }

// 使用 asFlow() 将 Iterable 转换为流,并收集和打印每个元素
(1..5).asFlow()
    .collect {
        Log.d(TAG, it.toString())
    }

// 使用 flow {} 创建一个流,并收集和打印每个元素
flow {
    (0..10).forEach {
        emit(it)
    }
}.collect {
    Log.d(TAG, it.toString())
}

// 使用 channelFlow {} 创建一个通道流,并收集和打印每个元素
channelFlow {
    (0..10).forEach {
        send(it)
    }
}.collect {
    Log.d(TAG, it.toString())
}

这些示例展示了不同方法创建流以及如何收集和处理其中的元素。 现在我们需要了解 flowOn 操作符。

flowOn 操作符

使用 flowOn 操作符模拟获取网络数据

在实际的 Android 应用程序中,我们经常需要在后台线程中执行网络请求,然后在 UI 线程上显示结果。让我们使用 flowOn 操作符来模拟这个过程。

val networkFlow = flow {
    // 模拟网络请求,在后台线程执行
    val result = fetchDataFromNetwork()
    emit(result)
}
.flowOn(Dispatchers.IO)

lifecycleScope.launch {
    // 在 UI 线程收集和处理数据
    networkFlow.collect { result ->
        // 更新 UI 或执行其他操作
        Log.d(TAG, "Received network data: $result")
    }
}

在这个示例中,fetchDataFromNetwork() 函数模拟了从网络获取数据的过程。该函数应该在后台线程中执行以避免阻塞主线程。

然后,我们将这个过程包装在一个 Flow 中,并使用 flowOn(Dispatchers.IO) 将其放置在后台线程上执行。

最后,在 lifecycleScope.launch 中,我们通过 collect 方法在 UI 线程上收集和处理从网络请求中发出的数据。

这个例子展示了如何使用 Flow 和 flowOn 操作符来管理网络请求和 UI 线程之间的线程切换,以确保良好的用户体验和应用性能。

在这个例子中,flow builder 内的任务将在后台线程(Dispatchers.Default)上执行。

现在,我们需要切换到 UI 线程。为此,我们需要将 collect 方法包装在 lifecycleScope.launch 中。

这就是如何使用 flowOn 操作符控制线程的方式。

flowOn() 类似于 RxJava 中的 subscribeOn(),它们都用于指定流的生产者运行在哪个线程上。在实际应用中,它们通常用于确保耗时操作不会阻塞主线程,从而保持应用的响应性。

关于 Dispatchers,它们帮助决定任务在哪个线程上执行。主要有三种类型的 Dispatchers:

IO dispatcher

用于处理涉及网络请求、磁盘读写等 IO 操作的任务。由于这些操作可能会阻塞线程,因此将它们放在单独的 IO 线程上执行可以提高应用的性能和响应性。

Default dispatcher

适用于 CPU 密集型的计算任务。这包括大量的数据处理、计算和算法执行等操作。

Main dispatcher

Android 应用的 UI 线程,用于处理用户界面相关的操作。在这个线程上执行任务时,必须确保它们不会阻塞 UI 线程,以免导致应用的 ANR(Application Not Responding)错误。

通过选择适当的 Dispatchers,我们可以更好地管理任务的执行,并确保应用程序的流畅运行。

使用 Flow Builder 创建 Flow

让我们通过例子来学习。

  1. 读取本地持久化文件

在这个示例中,我们使用 Flow Builder 创建了一个 Flow,用于从本地持久化文件中读取数据,并在主线程上接收数据。

val readLocalFileFlow = flow {
    // 模拟从本地持久化文件中读取数据的过程
    val data = fileManager.readFile()
    emit(data)
}.flowOn(Dispatchers.IO)

lifecycleScope.launch {
    readLocalFileFlow.collect { data ->
        // 在这里处理从本地文件中读取的数据
        Log.d(TAG, "Data from local file: $data")
    }
}
  1. 发送邮件

在这个示例中,我们使用 Flow Builder 创建了一个 Flow,用于模拟发送邮件的过程。它在后台线程中发送邮件,并在主线程上接收发送结果。

val sendEmailFlow = flow {
    // 模拟发送邮件的过程
    val result = emailService.sendEmail()
    emit(result)
}.flowOn(Dispatchers.IO)

lifecycleScope.launch {
    sendEmailFlow.collect { result ->
        // 在这里处理发送邮件的结果
        Log.d(TAG, "Email sending result: $result")
    }
}

这些例子展示了如何使用 Flow Builder 创建异步流,并在 Android 中处理各种异步任务。

总结

通过本文,我们学习了 Kotlin 的 基本 FlowAPI。我们了解了 Flow 的基本概念、主要组件以及如何使用 Flow Builder 创建流。同时,我们通过实际示例演示了如何在 Android 项目中使用 Flow API 处理异步任务。