Kotlin-反应式编程-全-
Kotlin 反应式编程(全)
原文:
zh.annas-archive.org/md5/e59035cf55f50a348ab77c7077b663bd译者:飞龙
前言
我们的世界仅仅是状态的集合吗?不。那么,为什么所有的编程范式都将我们的世界视为一系列的状态?我们难道不能在编程中反映那些真实、运动且持续变化状态的物体吗?这些问题自从我第一次学习编程以来就一直吸引着我。
当我开始作为 Android 开发者工作时,这些问题仍然困扰着我,但也结识了一些朋友。为什么应用程序中需要这么多循环?难道没有可以替代迭代器的东西吗?此外,对于 Android 应用程序,我们必须考虑很多因素,因为移动设备的处理器和 RAM 没有像你的 PC 那么强大。如果你没有很好地组织项目,经常会遇到内存不足异常。所以,如果我们能在程序中减少迭代器的使用,用户体验将显著提升,但,我们该如何做到?我们如何替换迭代器,用什么呢?
有一天,我读了一篇关于反应式编程和 ReactiveX 框架的博客文章(很可能是托马斯·尼尔德写的,感谢他),它让我看到了所有问题的答案。于是,我开始学习反应式编程。
我发现反应式编程的学习曲线非常复杂,许多开发者都在中途放弃。在大多数地方,反应式编程被认为是一个高级话题。然而,我继续我的反应式编程学习之旅,作为耐心和一致性的回报,我得到了我的问题的答案。RxJava(以及所有其他的 ReactiveX 库)代表的是类似于我们现实世界的模型,并且,与状态不同,它们通过运动和持续变化的状态来模拟行为。与迭代器模式不同,它依赖于推送机制,即数据/事件到来时就会推送给订阅者/观察者,这使得编程变得更加容易,更接近人类世界。
另一方面,大约两年前(2015 年 12 月),当我读到一篇关于将在 JVM 上运行的新语言的 Jetbrains 博客(是的,我读了很多,也写了很多)时,我的第一个想法是,为什么需要一种新语言?于是,我开始探索 Kotlin,并爱上了它。该语言唯一的目的就是让编程变得更加容易。每当有人谈论 Kotlin 的好处时,他们都会提到轻松处理空指针异常,但还有更多优势;这个列表永远没有尽头,并且还在不断增长。
对于程序员来说,最好的事情莫过于将 Kotlin 和 ReactiveX 框架结合起来;马里奥·阿里亚斯为了开发者社区的利益,出色地完成了这项工作,并在 2013 年 10 月启动了 RxKotlin。
RxKotlin 缺少的只有文档;我个人认为,ReactiveX 库复杂的学习曲线背后的主要原因是缺乏文档,以及,很大程度上,缺乏意识。我见过很多开发者,甚至有 6-8 年以上经验的,都没有听说过反应式编程;我相信这本书将在改变这种状况中扮演更大的角色。这本书也是我使命的一部分(也是 Kotlin Kolkata 用户组的使命),尽可能广泛地传播 Kotlin 的使用和知识。
根据我的知识,这是第一本帮助你学习 Kotlin 反应式编程的书籍,涵盖了 RxKotlin(确切地说,是 RxKotlin 2.0)和 Reactor-Kotlin 框架。这是一本逐步指南,教你如何学习 RxKotlin 和 Reactor-Kotlin,并增加了对 Spring 和 Android 的覆盖。我希望这本书能帮助你发现 Kotlin 和反应式编程的全部好处,并且,借助这本书,你将能够成功地将反应式编程应用到所有的 Kotlin 项目中。
如果你有任何疑问、反馈或评论,可以通过我的网站 www.rivuchk.com 联系我,或者发送电子邮件到 rivu@rivuchk.com。请确保在电子邮件的主题中提及“Book Query - Reactive Programming in Kotlin”。
本书涵盖内容
第一章,反应式编程简短介绍,帮助你理解反应式编程的背景、思维模式和原则。
第二章,使用 Kotlin 和 RxKotlin 进行函数式编程,这一章将带你了解函数式编程范式的基本概念及其在 Kotlin 上的可能实现,以便你能够轻松理解函数式反应式编程。
第三章,观察者、观察者和主题,使你能够掌握 RxKotlin 的基础——观察者、观察者和主题是 RxKotlin 的核心。
第四章,背压和 Flowables 简介,介绍了 Flowables,这使你能够使用背压——RxKotlin 中一种防止生产者超过消费者速度的技术。
第五章,异步数据算子和转换,介绍了 RxKotlin 中的算子。
第六章,关于算子和错误处理的更多内容,帮助你更牢固地掌握算子,并介绍如何组合生产者以及如何使用算子过滤它们。这一章还将帮助你更有效地在 RxKotlin 中处理错误。
第七章,使用调度器在 RxKotlin 中实现并发和并行处理,使你能够利用调度器的优势来实现并发编程。
第八章,测试 RxKotlin 应用程序,带你了解应用程序开发中最关键的部分——测试,在 RxKotlin 中测试略有不同,因为反应式编程定义的是行为而不是状态。本章从测试的基础开始,使你能够从头开始学习测试。
第九章,资源管理和扩展 RxKotlin,帮助你学习如何在 Kotlin 中管理资源——资源可以是数据库实例、文件、HTTP 访问或任何需要关闭的东西。你还将学习如何在第九章中创建自己的自定义操作符。
第十章,Spring for Kotlin 开发者入门 Web 编程,让你开始使用 Spring 和 Hibernate,以便你在用 Kotlin 编写 API 时能够利用其优势。
第十一章,使用 Spring JPA 和 Hibernate 的 REST API,介绍了 Reactor 框架和 reactor-kotlin 扩展,这样你就可以在 Kotlin 中使用反应式编程。
第十二章,反应式 Kotlin 和 Android,本书的最后一章,让你开始使用 Kotlin 在 Android 中进行反应式编程。
你需要这本书的内容
我们将在本书的程序中使用 Java 8 和 Kotlin 1.1.50,因此需要 Oracle 的 JDK 1.8 以及 Kotlin 1.1.50(如果你使用 IntelliJ IDEA,则可以跳过下载)。你需要一个环境来编写和编译你的 Kotlin 代码(我强烈推荐 IntelliJ IDEA,但你也可以使用任何你喜欢的工具),并且最好有一个构建自动化系统,如 Gradle 或 Maven。本书后面的内容中,我们将使用 Android Studio(2.3.3 或 3.0)。本书中你需要的一切都应该免费使用,并且不需要商业或个人许可(我们使用的是 IntelliJ IDEA 社区版)。
这本书面向的对象
这本书是为希望构建容错、可扩展和分布式系统的 Kotlin 开发者编写的。需要具备 Kotlin 的基本知识;然而,假设没有反应式编程的先验知识。
术语约定
在这本书中,你会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“让我们首先看看ReactiveCalculator类的init块”
代码块设置如下:
async(CommonPool) {
Observable.range(1, 10)
.subscribeOn(Schedulers.trampoline())//(1)
.subscribe {
runBlocking { delay(200) }
println("Observable1 Item Received $it")
}
当我们希望将你的注意力引向代码块的一个特定部分时,相关的行或项目将以粗体显示:
abstract class BaseActivity : AppCompatActivity() {
final override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onCreateBaseActivity(savedInstanceState)
}
abstract fun onCreateBaseActivity(savedInstanceState: Bundle?)
}
任何命令行输入或输出都应如下所示。输入命令可能被分成多行以提高可读性,但需要在提示符中作为一条连续的行输入:
$ git clone https://github.com/ReactiveX/RxKotlin.git
$ cd RxKotlin/
$ ./gradlew build
新术语和重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“转到 Android Studio | 设置 | 插件。”
警告或重要注意事项看起来像这样。
小贴士和技巧看起来像这样。
读者反馈
我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。
要向我们发送一般反馈,只需发送电子邮件至 feedback@packtpub.com,并在邮件主题中提及书的标题。
如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您已经是 Packt 图书的骄傲拥有者,我们有一些东西可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的 SUPPORT 标签上。
-
点击“代码下载 & 错误更正”。
-
在搜索框中输入书的名称。
-
选择您想要下载代码文件的书籍。
-
从下拉菜单中选择您购买此书的来源。
-
点击“代码下载”。
文件下载完成后,请确保您使用最新版本的以下软件解压缩或提取文件夹:
-
适用于 Windows 的 WinRAR / 7-Zip
-
适用于 Mac 的 Zipeg / iZip / UnRarX
-
适用于 Linux 的 7-Zip / PeaZip
本书代码包也托管在 GitHub 上github.com/PacktPublishing/Reactive-Programming-in-Kotlin。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
下载本书的彩色图像
我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。这些彩色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/ReactiveProgramminginKotlin_ColorImages.pdf下载此文件。
勘误
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
侵权
互联网上版权材料的侵权是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的非法复制我们的作品,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 copyright@packtpub.com 与我们联系,并提供涉嫌侵权材料的链接。
我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。
问答
如果您对本书的任何方面有问题,您可以通过 questions@packtpub.com 联系我们,我们将尽力解决问题。
第一章:响应式编程简述
响应式 这个术语最近变得非常流行。它不仅成为趋势,而且每天都有新的博客文章、演讲、新兴的框架和库出现,开始统治软件开发领域。甚至那些经常被称为市场巨头的知名 IT 公司,如 Google、Facebook、Amazon、Microsoft 和 Netflix,不仅支持和使用响应式编程,甚至开始发布针对同一目的的新框架。
因此,作为一个程序员,我们在思考响应式编程。为什么每个人都对它如此疯狂?响应式编程究竟是什么意思?响应式编程有哪些好处?最后,我们应该学习它吗?如果是的话,那么应该如何学习?
另一方面,Kotlin 也是你最近听说的新编程语言(我们猜测你已经听说过 Kotlin,因为这本书假设你对这种语言有一定的了解)。作为一门语言,Kotlin 解决了 Java 中许多重要的问题。最好的部分是它与 Java 的互操作性。如果你仔细观察趋势,你就会知道 Kotlin 不仅创造了一股强大的风,还引发了一场风暴,影响了周围的一切。甚至 Google 在 Google IO/17 上也宣布了对 Kotlin 的官方支持,将其作为 Android 应用开发的官方编程语言,并指出这是自 Android 框架出现以来,Google 首次向 Android 家族添加了除 Java 之外的语言。不久之后,Spring 也表达了他们对 Kotlin 的支持。
用简单的话来说,Kotlin 已经足够强大,可以创建出色的应用程序,但如果你将响应式编程风格与 Kotlin 结合起来,构建出色的应用程序将会变得超级简单。
本书将使用 RxKotlin 和 Reactor 在 Kotlin 中介绍响应式编程,并展示它们在 Spring、Hibernate 和 Android 中的实现。
在本章中,我们将涵盖以下主题:
-
什么是响应式编程?
-
采用函数式响应式编程的原因
-
响应式宣言
-
observer(响应式)模式与熟悉模式的比较 -
开始使用 RxKotlin
什么是响应式编程?
响应式编程是一种围绕数据流和变化传播的异步编程范式。用更简单的话来说,那些将影响其数据/数据流的所有变化传播给所有相关方(如最终用户、组件和子组件,以及其他以某种方式相关的程序)的程序被称为 响应式程序。
例如,拿任何电子表格(比如说 Google Sheet),在 A1 单元格中输入任何数字,在 B1 单元格中写入 =ISEVEN(A1) 函数;它会显示 TRUE 或 FALSE,取决于你输入的是偶数还是奇数。现在,如果你修改 A1 中的数字,B1 的值也会自动改变;这种行为被称为 响应式。
如果还不够清楚?让我们看看一个编码示例,然后再尝试再次理解它。以下是一个用于确定数字是偶数还是奇数的正常 Kotlin 代码块:
fun main(args: Array<String>) {
var number = 4
var isEven = isEven(number)
println("The number is " + (if (isEven) "Even" else "Odd"))
number = 9
println("The number is " + (if (isEven) "Even" else "Odd"))
}
fun isEven(n:Int):Boolean = ((n % 2) == 0)
如果你检查程序的输出,那么你会看到,尽管数字被分配了新的值,isEven仍然是 true;然而,如果isEven被用来跟踪数字的变化,那么它将自动变为 false。响应式程序会做同样的事情。
采用函数式响应式编程的原因
那么,让我们首先讨论采用函数式响应式编程的原因。除非它带来一些真正显著的好处,否则改变你整个编码方式是没有意义的,对吧?是的,函数式响应式编程为你带来了一系列令人震惊的好处,如下所示:
-
摆脱回调地狱:
回调是在预定义事件发生时被调用的方法。传递带有回调方法接口的机制称为回调机制。这种机制涉及大量的代码,包括接口、它们的实现等等。因此,它被称为回调地狱。
-
标准错误处理机制:
通常,在处理复杂任务和 HTTP 调用时,错误处理是一个主要关注点,尤其是在没有标准机制的情况下,这会变成一个头疼的问题。
-
比常规多线程简单得多:
虽然与 Java 相比,Kotlin 使线程处理变得更加容易,但它仍然足够复杂。响应式编程有助于使其更容易。
-
异步操作的直接方法:
线程和异步操作是相互关联的。随着线程变得更容易,异步操作也是如此。
-
一应俱全,每个操作都使用相同的 API:
响应式编程,尤其是 RxKotlin,为你提供了一个简单直接的 API。你可以用它做任何事情,无论是网络调用、数据库访问、计算还是 UI 操作。
-
函数式方法:
响应式编程引导你编写可读的声明式代码,因为在这里,事情更加函数化。
-
可维护和可测试的代码:
最重要的要点——通过正确地遵循响应式编程,你的程序将变得更加易于维护和测试。
响应式宣言
那么,什么是响应式宣言?响应式宣言(www.reactivemanifesto.org)是一份定义四个响应式原则的文档。你可以将其视为响应式编程宝藏的地图,或者像响应式编程宗教的圣经一样。
每个开始学习响应式编程的人都应该阅读这份宣言,以了解响应式编程的实质及其原则。
因此,以下就是响应式宣言定义的四个原则的精髓:
-
响应式:
系统能够及时响应。响应式系统专注于提供快速和一致的反应时间,因此它们提供一致的服务质量。
-
弹性:
如果系统遇到任何故障,它仍然保持响应。通过复制、遏制、隔离和委派实现弹性。故障被限制在每个组件内部,隔离组件彼此之间,因此当某个组件发生故障时,它不会影响其他组件或整个系统。
-
弹性:
反应式系统可以响应变化,并在不同的工作负载下保持响应性。它们在商品硬件和软件平台上以成本效益的方式实现弹性。
-
消息驱动:
为了建立弹性原则,反应式系统需要通过依赖异步消息传递在组件之间建立边界。
通过实施前四个原则,系统变得可靠和响应,因此是反应式的。
反应式流标准规范
除了反应式宣言之外,我们还有关于反应式流的标准化规范。在反应式世界的每一件事都是通过反应式流完成的。2013 年,Netflix、Pivotal 和 Lightbend(之前称为 Typesafe)感觉到需要为反应式流制定标准规范,因为反应式编程开始传播,更多的反应式编程框架开始出现,因此他们启动了导致反应式流标准规范倡议的工作,现在它正在各种框架和平台上得到实施。
您可以查看反应式流标准规范——www.reactive-streams.org/。
Kotlin 的反应式框架
要编写反应式程序,我们需要一个库或特定的编程语言;我们不能将 Kotlin 称为反应式语言(基本上,我不知道有任何这样的语言是本身反应式的),因为它是一种强大的灵活的编程语言,适用于现代多平台应用程序,完全与 Java 和 Android 互操作。然而,有一些反应式库可以帮助我们实现这些。因此,让我们看看可用的列表:
-
RxKotlin
-
Reactor-Kotlin
-
Redux-Kotlin
-
FunKTionale
-
RxJava 和其他反应式 Java 框架也可以与 Kotlin 一起使用(因为 Kotlin 与 Java 完全互操作)
在这本书中,我们将重点关注 RxJava 和 Reactor-kotlin(在后面的章节中,关于 Spring)。
开始使用 RxKotlin
RxKotlin 是针对 Kotlin 的反应式编程的具体实现,它受到函数式编程的影响。它倾向于函数组合、避免全局状态和副作用。它依赖于生产者/消费者模式的observer模式,有许多操作符允许组合、调度、节流、转换、错误处理和生命周期管理。
而 Reactor-Kotlin 也基于函数式编程,并且得到了 Spring 框架的广泛认可和支持。
下载和设置 RxKotlin
你可以从 GitHub 下载和构建 RxKotlin (github.com/ReactiveX/RxKotlin)。我不需要任何其他依赖项。GitHub wiki 页面上的文档结构良好。以下是您如何从 GitHub 检出项目并运行构建的说明:
$ git clone https://github.com/ReactiveX/RxKotlin.git
$ cd RxKotlin/
$ ./gradlew build
你也可以按照页面上的说明使用 Maven 和 Gradle。
对于 Gradle,使用以下编译依赖项:
compile 'io.reactivex.rxjava2:rxkotlin:2.x.y'
对于 Maven,使用以下依赖项:
<dependency>
<groupId>io.reactivex.rxjava2</groupId>
<artifactId>rxkotlin</artifactId>
<version>2.x.y</version>
</dependency>
本书针对 RxKotlin 2.x,所以请记住使用io.reactive.rxjava2而不是io.reactivex.rxkotlin,因为后者是针对 RxKotlin 1.x 的。
注意,我们在这本书中使用的是 RxKotlin 版本 2.1.0。
现在,让我们看看 RxKotlin 是什么。我们将从一个众所周知的东西开始,然后逐渐深入了解库的秘密。
比较拉取机制与 RxJava 推送机制
RxKotlin 围绕表示用于推送机制(而不是传统程序中iterator模式的拉取机制)的数据/事件系统的可观察类型,因此它是懒加载的,可以同步和异步使用。
如果我们从与数据列表一起工作的简单示例开始,我们会更容易理解。所以,这里是代码:
fun main(args: Array<String>) {
var list:List<Any> = listOf("One", 2, "Three", "Four", 4.5,
"Five", 6.0f) // 1
var iterator = list.iterator() // 2
while (iterator.hasNext()) { // 3
println(iterator.next()) // Prints each element 4
}
}
以下截图是输出结果:

那么,让我们逐行分析程序,了解它是如何工作的。
在注释1中,我们创建了一个包含七个项目的列表(列表通过任何类帮助包含混合数据类型的数据)。在注释2中,我们是从列表中创建iterator,这样我们就可以遍历数据。在注释3中,我们创建了一个while循环,通过iterator从列表中提取数据,然后在注释4中打印它。
需要注意的是,我们在当前线程阻塞直到收到数据并准备就绪的同时,从列表中提取数据。例如,想象一下从网络调用/数据库查询中获取数据,而不是仅仅从List中获取,在这种情况下,线程将被阻塞多长时间。显然,你可以为这些操作创建一个单独的线程,但这样也会增加复杂性。
仅仅思考一下;哪种方法更好?让程序等待数据,还是当数据可用时将数据推送到程序?
ReactiveX 框架(无论是 RxKotlin 还是 RxJava)的构建块是可观察的。observable类与iterator接口正好相反。它有一个底层集合或计算,产生可以被消费者消费的值。然而,区别在于消费者不像在iterator模式中那样从生产者拉取这些值;相反,生产者将值作为通知推送给消费者。
那么,让我们再次以相同的例子为例,这次使用observable:
fun main(args: Array<String>) {
var list:List<Any> = listOf("One", 2, "Three",
"Four", 4.5, "Five", 6.0f) // 1
var observable: Observable<Any> = list.toObservable();
observable.subscribeBy( // named arguments for
lambda Subscribers
onNext = { println(it) },
onError = { it.printStackTrace() },
onComplete = { println("Done!") }
)
}
这个程序输出与上一个相同——它打印出列表中的所有项。不同之处在于方法。那么,让我们看看它实际上是如何工作的:
-
创建一个列表(与上一个相同)。
-
使用该列表创建了一个
observable实例。 -
我们正在订阅
observer实例(我们使用命名参数为lambda,稍后详细说明)。
当我们订阅observable时,每个数据都会被推送到onNext,当所有数据都推送完毕时,它将调用onComplete,如果发生任何错误,则调用onError。
所以,你学会了如何使用observable实例,它们与非常熟悉的iterator实例非常相似。我们可以使用这些observable实例来构建异步流并将数据更新推送到它们的订阅者(甚至是多个订阅者)。这是一个简单的响应式编程范式实现。数据正在传播到所有感兴趣的各方——订阅者。
ReactiveEvenOdd 程序
现在,我们多少熟悉了observables,让我们以响应式的方式修改偶数奇数程序。以下是实现这一点的代码:
fun main(args: Array<String>) {
var subject:Subject<Int> = PublishSubject.create()
subject.map({ isEven(it) }).subscribe({println
("The number is ${(if (it) "Even" else "Odd")}" )})
subject.onNext(4)
subject.onNext(9)
}
这里是输出:

在这个程序中,我们使用了subject和map,我们将在后面的章节中介绍。在这里,它只是展示了在响应式编程中通知变化是多么容易。如果你仔细查看程序,你也会发现代码是模块化和函数式的。当我们用数字通知subject时,它调用map中的方法,然后调用subscribe方法中的方法,该方法是map方法的返回值。map方法检查数字是否为偶数,并相应地返回 true 或 false;在subscribe方法中,我们接收该值并相应地打印偶数或奇数。subject.onNext方法是向主题发送新值的方式,以便它可以处理它。
ReactiveCalculator 项目
那么,让我们从一个用户输入的事件开始。通过以下示例进行操作:
fun main(args: Array<String>) {
println("Initial Out put with a = 15, b = 10")
var calculator:ReactiveCalculator = ReactiveCalculator(15,10)
println("Enter a = <number> or b = <number> in separate
lines\nexit to exit the program")
var line:String?
do {
line = readLine();
calculator.handleInput(line)
} while (line!= null && !line.toLowerCase().contains("exit"))
}
如果你运行代码,你会得到以下输出:

在main方法中,我们并没有做很多操作,除了监听输入并将其传递给ReactiveCalculator类,所有其他操作都在类内部完成,因此它是模块化的。在后面的章节中,我们将为输入过程创建一个单独的observable,并将所有用户输入处理在那里。为了简化,我们遵循了用户输入的拉机制,你将在下一章学习如何移除它。现在,让我们看一下下面的ReactiveCalculator类:
class ReactiveCalculator(a:Int, b:Int) {
internal val subjectAdd: Subject<Pair<Int,Int>> =
PublishSubject.create()
internal val subjectSub: Subject<Pair<Int,Int>> =
PublishSubject.create()
internal val subjectMult: Subject<Pair<Int,Int>> =
PublishSubject.create()
internal val subjectDiv: Subject<Pair<Int,Int>> =
PublishSubject.create()
internal val subjectCalc:Subject<ReactiveCalculator> =
PublishSubject.create()
internal var nums:Pair<Int,Int> = Pair(0,0)
init{
nums = Pair(a,b)
subjectAdd.map({ it.first+it.second }).subscribe
({println("Add = $it")} )
subjectSub.map({ it.first-it.second }).subscribe
({println("Substract = $it")} )
subjectMult.map({ it.first*it.second }).subscribe
({println("Multiply = $it")} )
subjectDiv.map({ it.first/(it.second*1.0) }).subscribe
({println("Divide = $it")} )
subjectCalc.subscribe({
with(it) {
calculateAddition()
calculateSubstraction()
calculateMultiplication()
calculateDivision()
}
})
subjectCalc.onNext(this)
}
fun calculateAddition() {
subjectAdd.onNext(nums)
}
fun calculateSubstraction() {
subjectSub.onNext(nums)
}
fun calculateMultiplication() {
subjectMult.onNext(nums)
}
fun calculateDivision() {
subjectDiv.onNext(nums)
}
fun modifyNumbers (a:Int = nums.first, b: Int = nums.second) {
nums = Pair(a,b)
subjectCalc.onNext(this)
}
fun handleInput(inputLine:String?) {
if(!inputLine.equals("exit")) {
val pattern: Pattern = Pattern.compile
("([a|b])(?:\\s)?=(?:\\s)?(\\d*)");
var a: Int? = null
var b: Int? = null
val matcher: Matcher = pattern.matcher(inputLine)
if (matcher.matches() && matcher.group(1) != null
&& matcher.group(2) != null) {
if(matcher.group(1).toLowerCase().equals("a")){
a = matcher.group(2).toInt()
} else if(matcher.group(1).toLowerCase().equals("b")){
b = matcher.group(2).toInt()
}
}
when {
a != null && b != null -> modifyNumbers(a, b)
a != null -> modifyNumbers(a = a)
b != null -> modifyNumbers(b = b)
else -> println("Invalid Input")
}
}
}
}
在这个程序中,我们只有对数据(而不是事件,即用户输入)有推送机制(observable模式)。虽然本书的初始章节将向你展示如何观察数据变化;RxJava 还允许你观察事件(如用户输入),我们将在本书末尾讨论 RxJava 在 Android 上的应用时涵盖这一点。所以,现在,让我们了解这个代码是如何工作的。
首先,我们创建了一个ReactiveCalculator类,它观察其数据甚至自身;因此,每当其属性被修改时,它都会调用所有的calculate方法。
我们使用Pair来配对两个变量,并在Pair上创建了四个subject来观察其变化并对其进行处理;我们需要四个subject因为存在四个独立的操作。在后面的章节中,你还将学习如何仅用一个方法来优化它。
在calculate方法上,我们只是通知主题处理Pair并打印新的结果。
如果你关注这两个程序中的map方法,那么你会了解到map方法接受我们通过onNext传递的值,并将其处理成结果值;这个结果值可以是任何数据类型,并且这个结果值会被传递给订阅者以进一步处理和/或显示输出。
摘要
在本章中,我们学习了什么是响应式编程以及我们应该学习它的原因。我们还开始了编码。响应式编程模式可能看起来很新或者有些不常见,但它并不难;在使用它时,你只需要声明一些额外的事情。
我们学习了observable及其用法。我们还介绍了subject和map,我们将在后面的章节中深入学习。
在后面的章节中,我们将继续使用ReactiveCalculator示例,并看看我们如何可以优化和增强这个程序。
本章中展示的三个示例可能一开始看起来有些混乱和复杂,但它们实际上非常简单,随着你继续阅读本书,它们将变得熟悉起来。
在下一章中,我们将学习更多关于函数式编程和 RxKotlin 中的函数式接口。
第二章:使用 Kotlin 和 RxKotlin 进行函数式编程
函数式编程范式与面向对象编程(OOP)略有不同。它侧重于使用声明性和表达性的程序以及不可变数据,而不是语句。函数式编程的定义指出:“函数式编程是一种编程系统,它依赖于将程序结构化为不可变数据的数学函数的评估,并避免状态改变。”它是一种声明性编程范式,建议使用小型、可重用的声明性函数。
我们已经看到了函数式编程的定义;现在,你难道不想深入了解它的定义,看看它究竟意味着什么吗?所有语言都支持函数式编程吗?如果不是,那么哪些语言支持,Kotlin 又如何呢?响应式编程与函数式编程究竟有什么关系?最后,我们需要学习什么,才能进行函数式编程?
在本章中,我们将涵盖以下主题:
-
函数式编程入门
-
函数式编程与响应式编程的关系
-
Kotlin 的突破性特性——协程
介绍函数式编程
因此,函数式编程希望您将编程逻辑分布到可重用的声明性小函数中。将您的逻辑分布到小块代码中会使代码模块化且不复杂,因此您可以在任何给定点重构/更改任何模块/代码的一部分,而不会对其他模块产生影响。
函数式编程需要一些接口和语言的支持,因此我们不能说任何语言都是函数式的,除非它提供某种支持来实现函数式编程。然而,函数式编程并不是什么新事物;实际上,它是一个相当古老的概念,并且有几种语言支持它。我们称这些语言为函数式编程语言,以下是一些最受欢迎的函数式编程语言的列表:
-
Lisp
-
Clojure
-
Wolfram
-
Erlang
-
OCaml
-
Haskell
-
Scala
-
F#
Lisp 和 Haskell 是一些最古老的语言,并且至今仍在学术界和工业界中使用。当谈到 Kotlin 时,它从第一个稳定版本开始就提供了对函数式编程的出色支持,与 Java 相比,Java 在 Java 8 之前没有任何对函数式编程的支持。你可以在面向对象和函数式编程风格之间,甚至两种风格的混合中使用 Kotlin,这对我们来说真的是一个巨大的好处。Kotlin 对诸如高阶函数、函数类型和 lambda 等特性提供了第一级支持,如果你正在做或探索函数式编程,Kotlin 是一个不错的选择。
函数式响应式编程(FRP)的概念实际上是将响应式编程与函数式编程相结合的产物。编写函数式编程的主要目的是实现模块化编程;这种模块化编程对于实现响应式编程或更确切地说,实现响应式宣言的四个原则非常有帮助,有时甚至是必需的。
函数式编程基础
函数式编程包含一些新的概念,如 Lambda、纯函数、高阶函数、函数类型和内联函数,我们将学习这些内容。非常有趣,不是吗?
注意,尽管在许多程序员的词汇中,纯函数和 Lambda 是相同的,但实际上它们并不相同。在本章的后续部分,我们将了解更多关于它们的内容。
Lambda 表达式
Lambda 或 Lambda 表达式通常意味着 匿名函数,即没有名称的函数。你也可以说 Lambda 表达式是一个函数,但并非每个函数都是 Lambda 表达式。并非每种编程语言都支持 Lambda 表达式,例如,Java 直到 Java 8 才有这个功能。Lambda 表达式的实现也因语言而异。Kotlin 对 Lambda 表达式有很好的支持,在 Kotlin 中实现它们既简单又自然。现在让我们看看 Kotlin 中 Lambda 表达式是如何工作的:
fun main(args: Array<String>) {
val sum = { x: Int, y: Int -> x + y } // (1)
println("Sum ${sum(12,14)}")// (2)
val anonymousMult = {x: Int -> (Random().nextInt(15)+1) * x}
// (3)
println("random output ${anonymousMult(2)}")// (4)
}
在前面的程序中,在注释(1)中,我们声明了一个 Lambda 表达式,该表达式将两个数字相加并返回结果 sum;在注释(2)中,我们调用该函数并打印它;在注释(3)中,我们声明了另一个 Lambda,它将随机数与 15 绑定并乘以传递给它的值 x,然后返回结果;在注释(4)中,我们再次打印它。这两个 Lambda 表达式实际上都是函数,但没有函数名;因此,它们也被称为匿名函数。如果你与 Java 比较,Java 有匿名类的功能,但直到 Java 8 才包含 lambda/匿名函数。
如果你好奇输出结果,请参考以下截图:

纯函数
纯函数的定义是,如果函数的返回值完全依赖于其参数/输入,那么这个函数可以被称为纯函数。所以,如果我们声明一个函数为 fun func1(x:Int):Int,那么它的返回值将严格依赖于其参数 x;比如说,如果你两次调用 func1 并传入值 3,那么两次的返回值将是相同的。纯函数可以是 Lambda 或命名函数。在前面的例子中,第一个 Lambda 表达式是一个纯函数,但第二个不是,因为对于第二个来说,即使传入相同的值,其返回值也可能在不同时间不同。让我们看看以下例子来更好地理解它:
fun square(n:Int):Int {//(1)
return n*n
}
fun main(args: Array<String>) {
println("named pure func square = ${square(3)}")
val qube = {n:Int -> n*n*n}//(2)
println("lambda pure func qube = ${qube(3)}")
}
这里的两个函数,(1)和(2),都是纯函数——一个是命名的,而另一个是 lambda。如果你将值3传递给任一函数n次,它们的返回值在每次都会相同。纯函数没有副作用。
副作用:如果一个函数或表达式修改了其作用域之外的状态,或者除了返回值之外还有可观察的与调用函数或外部世界的交互,那么它就被说成有副作用。
来源-Wikipedia en.wikipedia.org/wiki/Side_effect_(computer_science)。
需要注意的是,正如我们之前所说的,纯函数与 lambda 表达式无关,它们的定义完全不同。
以下是输出:
named pure func square = 9
lambda pure func qube = 27
高阶函数
那些接受另一个函数作为参数或返回函数作为结果的函数被称为高阶函数。考虑以下示例以更好地理解它:
fun highOrderFunc(a:Int, validityCheckFunc:(a:Int)->Boolean) {//(1)
if(validityCheckFunc(a)) {//(2)
println("a $a is Valid")
} else {
println("a $a is Invalid")
}
}
fun main(args: Array<String>) {
highOrderFun(12,{ a:Int -> a.isEven()})//(3)
highOrderFunc(19,{ a:Int -> a.isEven()})
}
在这个程序中,我们声明了一个highOrderFunc函数,它将接受一个Int和一个validityCheckFunc(Int)函数。我们在highOrderFunc函数内部调用validityCheckFunc函数,以检查值是否有效。然而,我们在调用main函数内部的highOrderFunc函数时定义了validityCheckFunc函数。
注意,在这个程序中,isEven函数是一个扩展函数,它是在你与书一起获得的project文件内部定义的。
这里是输出:
a 12 is Valid
a 19 is Invalid
内联函数
虽然函数是编写模块化代码的绝佳方式,但它有时可能会因为函数栈维护和开销而增加程序执行时间并减少内存优化。内联函数是避免函数式编程中这些障碍的绝佳方法。例如,请参见以下代码片段:
fun doSomeStuff(a:Int = 0) = a+(a*a)
fun main(args: Array<String>) {
for (i in 1..10) {
println("$i Output ${doSomeStuff(i)}")
}
}
让我们回顾一下内联函数的定义;它说,内联函数是提高程序性能和内存优化的增强功能。函数可以被指示给编译器,使其成为内联函数,这样编译器就可以在函数被调用的任何地方替换这些函数定义。编译器在编译时替换内联函数的定义,而不是在运行时引用函数定义;因此,不需要为函数调用、栈维护等额外内存,同时还能获得函数的好处。
上述程序声明了一个函数,用于添加两个数字并返回结果,我们将在循环中调用这个函数。而不是为这个目的声明一个函数,我们可以在将要调用函数的地方直接编写加法代码,但声明一个函数让我们可以在不影响剩余代码的情况下随时修改加法逻辑,例如,如果我们想修改加法为乘法或其他操作。如果我们将函数声明为内联的,那么该函数内部的代码将替换所有的函数调用,从而在保持自由度的同时提高性能。以下代码片段作为例子:
inline fun doSomeStuff(a:Int = 0) = a+(a*a)
fun main(args: Array<String>) {
for (i in 1..10) {
println("$i Output ${doSomeStuff(i)}")
}
}
这是程序的输出结果:

Kotlin 通过内联函数提供的一个额外功能是,如果你将一个高阶函数声明为inline,那么inline关键字将影响该函数本身以及传递给它的 lambda。让我们用inline修改高阶函数的代码:
inline fun highOrderFuncInline(a:Int, validityCheckFunc:(a:Int)-
>Boolean) {
if(validityCheckFunc(a)) {
println("a $a is Valid")
} else {
println("a $a is Invalid")
}
}
fun main(args: Array<String>) {
highOrderFuncInline(12,{ a:Int -> a.isEven()})
highOrderFuncInline(19,{ a:Int -> a.isEven()})
}
编译器将用其 lambda 替换所有对validityCheckFunc的调用,就像它会对具有定义的highOrderFuncInline做的那样。正如你所见,代码的修改并不多,只是在函数声明前添加了inline关键字,就能提高性能。
将函数式编程应用于ReactiveCalculator类
因此,现在,在尝试理解上一章中的ReactiveCalculator类之后,我们将尝试优化代码。让我们首先看看ReactiveCalculator类的init块:
init{
nums = Pair(a,b)
subjectAdd.map({ it.first+it.second }).subscribe({println
("Add = $it")} )//1
subjectSub.map({ it.first-it.second }).subscribe({println
("Substract = $it")} )
subjectMult.map({ it.first*it.second }).subscribe
({println("Multiply = $it")} )
subjectDiv.map({ it.first/(it.second*1.0) }).subscribe
({println("Divide = $it")} )
subjectCalc.subscribe({
with(it) {
calculateAddition()
calculateSubstraction()
calculateMultiplication()
calculateDivision()
}
})
subjectCalc.onNext(this)
}
因此,现在,有了函数式编程的知识,我们可以轻松地说,map和subscribe方法是接受函数作为参数的高阶函数。然而,你真的认为需要很多subject和subscriber吗?类上的subscriber不就足够完成这项工作了吗?让我们尝试修改和优化以下代码片段:
class ReactiveCalculator(a:Int, b:Int) {
val subjectCalc: io.reactivex.subjects.Subject
<ReactiveCalculator> =
io.reactivex.subjects.PublishSubject.create()
var nums:Pair<Int,Int> = Pair(0,0)
init{
nums = Pair(a,b)
subjectCalc.subscribe({
with(it) {
calculateAddition()
calculateSubstraction()
calculateMultiplication()
calculateDivision()
}
})
subjectCalc.onNext(this)
}
inline fun calculateAddition():Int {
val result = nums.first + nums.second
println("Add = $result")
return result
}
inline fun calculateSubstraction():Int {
val result = nums.first - nums.second
println("Substract = $result")
return result
}
inline fun calculateMultiplication():Int {
val result = nums.first * nums.second
println("Multiply = $result")
return result
}
inline fun calculateDivision():Double {
val result = (nums.first*1.0) / (nums.second*1.0)
println("Multiply = $result")
return result
}
inline fun modifyNumbers (a:Int = nums.first, b:
Int = nums.second) {
nums = Pair(a,b)
subjectCalc.onNext(this)
}
fun handleInput(inputLine:String?) {
if(!inputLine.equals("exit")) {
val pattern: java.util.regex.Pattern =
java.util.regex.Pattern.compile
("([a|b])(?:\\s)?=(?:\\s)?(\\d*)");
var a: Int? = null
var b: Int? = null
val matcher: java.util.regex.Matcher =
pattern.matcher(inputLine)
if (matcher.matches() && matcher.group(1) != null &&
matcher.group(2) != null) {
if(matcher.group(1).toLowerCase().equals("a")){
a = matcher.group(2).toInt()
} else if(matcher.group(1).toLowerCase().equals("b")){
b = matcher.group(2).toInt()
}
}
when {
a != null && b != null -> modifyNumbers(a, b)
a != null -> modifyNumbers(a = a)
b != null -> modifyNumbers(b = b)
else -> println("Invalid Input")
}
}
}
}
因此,我们移除了所有其他的subscriber,只用一个来完成工作。以下是输出结果:
Initial Output with a = 15, b = 10
Add = 25
Substract = 5
Multiply = 150
Multiply = 1.5
Enter a = <number> or b = <number> in separate lines
exit to exit the program
a = 6
Add = 16
Substract = -4
Multiply = 60
Multiply = 0.6
b=4
Add = 10
Substract = 2
Multiply = 24
Multiply = 1.5
exit
我们订阅了类本身;因此,每当其变量发生变化时,我们会收到通知,并在subscribe方法中直接执行所有任务。此外,由于我们已经将函数声明为内联的,它们还将有助于性能优化。
协程
路径突破,可能是 Kotlin 中最令人兴奋的功能是协程。它们是编写异步、非阻塞代码的新方法,类似于线程,但更简单、更高效、更轻量级。协程是在 Kotlin 1.1 中添加的,并且仍然是实验性的,所以在生产环境中使用之前请三思。
在本书的后续章节中,你将了解 RxKotlin 中的调度器,它封装了线程的复杂性,但你只能在 RxKotlin 链中使用它,而协程可以在任何地方和任何时候使用。这确实是 Kotlin 的一个突破性特性。它们在线程上提供了很好的抽象,使得上下文变化和并发变得更容易。
请记住,RxKotlin 目前还没有使用协程;原因很简单——协程和 RxKotlin 中的调度器几乎具有相同的内部架构;虽然协程是新的,但调度器在 RxJava、RxJs、RxSwift 以及更多中已经存在很长时间了。
协程是开发者在不使用/无法使用 RxKotlin Schedulers 时实现并发的最佳选择。
因此,让我们先将其添加到我们的项目中。如果你正在使用 Gradle,请按照以下步骤操作(apply plugin 可以是 'kotlin' 或 'kotlin-android',具体取决于你是否用于 JVM 或 Android):
apply plugin: 'kotlin'
kotlin {
experimental {
coroutines 'enable'
}
}
然后,我们必须添加以下依赖项:
repositories {
...
jcenter()
}
dependencies {
...
compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.16"
}
如果你正在使用 Maven,那么请在 pom.xml 文件中添加以下代码块:
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
...
<configuration>
<args>
<arg>-Xcoroutines=enable</arg>
</args>
</configuration>
</plugin>
<repositories>
...
<repository>
<id>central</id>
<url>http://jcenter.bintray.com</url>
</repository>
</repositories>
<dependencies>
...
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>0.16</version>
</dependency>
</dependencies>
Apache Maven 是一个软件项目管理与理解工具。基于项目对象模型(POM)的概念,Maven 可以从中央信息点管理项目的构建、报告和文档。请参考以下网址获取更多信息–maven.apache.org/。
那么,协程究竟是什么?在开发应用程序时,我们经常遇到需要执行长时间运行或耗时操作的情况,例如网络调用、数据库操作或一些复杂的计算。Java 中处理这种情况的唯一选项是使用线程,而这本身就是一个非常复杂的过程。每当面对这些情况时,我们都感到需要一种简单而强大的 API 来处理这些情况。来自 .NET 领域的开发者,尤其是那些之前使用过 C# 的开发者,对 async/await 操作符很熟悉;这某种程度上与 Kotlin 协程最为接近。
开始使用协程
因此,让我们考虑以下示例:
suspend fun longRunningTsk():Long {//(1)
val time = measureTimeMillis {//(2)
println("Please wait")
delay(2,TimeUnit.SECONDS)//(3)
println("Delay Over")
}
return time
}
fun main(args: Array<String>) {
runBlocking {//(4)
val exeTime = longRunningTsk()//(5)
println("Execution Time is $exeTime")
}
}
我们将检查代码,但首先让我们看看输出结果:
Please wait
Delay Over
Execution Time is 2018
因此,现在让我们来理解代码。在注释(1)中,当我们声明函数时,我们使用suspend关键字标记函数,这个关键字用来标记一个函数为挂起状态,即当执行函数时,程序应该等待其结果;因此,不允许在主线程中挂起函数(这为你提供了主线程和挂起函数之间清晰的障碍)。在注释(2)中,我们使用measureTimeMillis启动了一个代码块,并将其值赋给了val类型的time变量。measureInMillis的功能相当简单——它执行传递给它的代码块,同时测量其执行时间,并返回相同的值。我们将在注释(3)中使用delay函数故意延迟程序执行 2 秒钟。在注释(4)中的main函数中的runBlocking代码块使程序等待直到注释(5)中调用的longRunningTsk函数完成。所以,这是一个相当简单的例子;然而,我们在这里使主线程等待。有时,你可能不希望这样;相反,你可能想要进行异步操作。所以,让我们也尝试实现这一点:
fun main(args: Array<String>) {
val time = async(CommonPool) { longRunningTsk() }//(1)
println("Print after async ")
runBlocking { println("printing time ${time.await()}") }//(2)
}
在这里,我们保持longRunningTsk不变,只是修改了main函数。在注释(1)中,我们将time变量赋值给async代码块内部的longRunningTsk。async代码块非常有趣;它在传递给它的协程上下文中异步执行其代码块内的代码。
基本上有三种协程上下文类型。Unconfined表示它将在主线程上运行,CommonPool在公共线程池上运行,或者你也可以创建一个新的协程上下文。
在注释(2)中,我们运行了一个阻塞代码,这将使main函数等待直到time变量的值可用;await函数帮助我们完成这个任务——它告诉runBlocking代码块等待直到async代码块完成执行,以便time变量的值可用。
构建序列
如我之前提到的,Kotlin 协程不仅仅是 Java 中的线程和 C#中的async/await。这是一个在学习后你会感到愤怒的功能,因为它在你学习编码时并不存在。为了锦上添花,这个功能是应用级别的,它甚至包含在kotlin-stdlib中,所以你可以在那里直接使用,无需做任何事情,甚至无需显式使用协程。
在学习我所说的内容之前,让我们做一些老式的代码,比如说斐波那契数列?以下代码块作为例子:
fun main(args: Array<String>) {
var a = 0
var b = 1
print("$a, ")
print("$b, ")
for(i in 2..9) {
val c = a+b
print("$c, ")
a=b
b=c
}
}
所以,这是用 Kotlin 编写的老式斐波那契数列程序。当你计划让用户输入要打印的数字数量时,这段代码会变得更有问题。如果我告诉你 Kotlin 有一个buildSequence函数可以为你完成这个任务,而且做得非常自然,方式也更简单?所以,现在让我们修改代码:
fun main(args: Array<String>) {
val fibonacciSeries = buildSequence {//(1)
var a = 0
var b = 1
yield(a)//(2)
yield(b)
while (true) {
val c = a+b
yield(c)//(3)
a=b
b=c
}
}
println(fibonacciSeries.take(10) join "," )//(4)
}
以下是对两个程序输出的描述:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34
现在,让我们理解这个程序。在注释(1)中,我们声明val fibonacciSeries由buildSequence块填充。每当我们在序列/数列中计算出一个要输出的值时,我们将输出那个值(在注释2和3中)。在注释4中,我们调用fibonacciSeries来计算到第10个变量,并用逗号(,)连接序列的元素。
因此,你学习了协程;现在,让我们将其实现到我们的程序中。
带有协程的ReactiveCalculator类
到目前为止,在ReactiveCalculator程序中,我们都在同一个线程上执行所有操作;你不认为我们应该异步地做这些事情吗?所以,让我们这样做:
class ReactiveCalculator(a:Int, b:Int) {
val subjectCalc:
io.reactivex.subjects.Subject<ReactiveCalculator> =
io.reactivex.subjects.PublishSubject.create()
var nums:Pair<Int,Int> = Pair(0,0)
init{
nums = Pair(a,b)
subjectCalc.subscribe({
with(it) {
calculateAddition()
calculateSubstraction()
calculateMultiplication()
calculateDivision()
}
})
subjectCalc.onNext(this)
}
inline fun calculateAddition():Int {
val result = nums.first + nums.second
println("Add = $result")
return result
}
inline fun calculateSubstraction():Int {
val result = nums.first - nums.second
println("Substract = $result")
return result
}
inline fun calculateMultiplication():Int {
val result = nums.first * nums.second
println("Multiply = $result")
return result
}
inline fun calculateDivision():Double {
val result = (nums.first*1.0) / (nums.second*1.0)
println("Division = $result")
return result
}
inline fun modifyNumbers (a:Int = nums.first, b:
Int = nums.second) {
nums = Pair(a,b)
subjectCalc.onNext(this)
}
suspend fun handleInput(inputLine:String?) {//1
if(!inputLine.equals("exit")) {
val pattern: java.util.regex.Pattern =
java.util.regex.Pattern.compile
("([a|b])(?:\\s)?=(?:\\s)?(\\d*)");
var a: Int? = null
var b: Int? = null
val matcher: java.util.regex.Matcher =
pattern.matcher(inputLine)
if (matcher.matches() && matcher.group(1) != null &&
matcher.group(2) != null) {
if(matcher.group(1).toLowerCase().equals("a")){
a = matcher.group(2).toInt()
} else if(matcher.group(1).toLowerCase().equals("b")){
b = matcher.group(2).toInt()
}
}
when {
a != null && b != null -> modifyNumbers(a, b)
a != null -> modifyNumbers(a = a)
b != null -> modifyNumbers(b = b)
else -> println("Invalid Input")
}
}
}
}
fun main(args: Array<String>) {
println("Initial Out put with a = 15, b = 10")
var calculator: ReactiveCalculator = ReactiveCalculator(15, 10)
println("Enter a = <number> or b = <number> in separate lines\nexit
to exit the program")
var line:String?
do {
line = readLine();
async(CommonPool) {//2
calculator.handleInput(line)
}
} while (line!= null && !line.toLowerCase().contains("exit"))
}
在注释(1)中,我们将handleInput函数声明为挂起,这告诉 JVM 这个函数应该需要更长的时间,调用此函数的上下文执行应该等待它完成。正如我之前已经提到的,挂起函数不能在主上下文中调用;所以,在注释(2)中,我们创建了一个async块来调用这个函数。
函数式编程 – 单子
函数式编程没有单子是不完整的。如果你对函数式编程感兴趣,那么你非常了解它;否则,你可能是第一次听说。那么,什么是单子呢?让我们来了解一下。单子的概念相当抽象;定义说“单子是一个结构,通过封装一个值并添加一些额外的功能来创建一个新的类型”。所以,让我们先使用单子;看看下面的程序:
fun main(args: Array<String>) {
val maybeValue: Maybe<Int> = Maybe.just(14)//1
maybeValue.subscribeBy(//2
onComplete = {println("Completed Empty")},
onError = {println("Error $it")},
onSuccess = { println("Completed with value $it")}
)
val maybeEmpty:Maybe<Int> = Maybe.empty()//3
maybeEmpty.subscribeBy(
onComplete = {println("Completed Empty")},
onError = {println("Error $it")},
onSuccess = { println("Completed with value $it")}
)
}
在这里,Maybe是一个封装了Int值并添加了一些额外功能的单子。Maybe单子表示它可能包含或不包含值,并且可以带或不带值或错误完成。所以,如果有错误,那么它显然会调用onError;如果没有错误,并且它有一个值,它将使用该值调用onSuccess;如果没有值也没有错误,它将调用onComplete。需要注意的是,这里的三个方法onError、onComplete和onSuccess都是终止方法,这意味着这三个中的任何一个都可能被Maybe单子调用,而其他的方法将永远不会被调用。
让我们逐步分析程序,以便更好地理解单子。在注释(1)中,我们将声明一个Maybe单子并将其赋值为14。在注释(2)中,我们将订阅这个单子。在注释(3)中,我们再次声明一个Maybe单子,这次赋予一个空值。订阅需要三个 lambda 表达式作为参数——当单子包含一个值时,onSuccess会被调用;当它不包含任何值时,onComplete会被调用;如果发生任何错误,则onError会被调用。现在让我们看看输出结果:
Completed with value 14
Completed Empty
因此,正如我们所看到的,对于maybeValue,onSuccess被调用,但对于maybeEmpty,onComplete方法被调用。
单一单子
Maybe 只不过是另一种类型的单子,还有很多;我们将在后面的章节中介绍其中一些最重要的,并将它们与响应式编程结合起来。
摘要
在本章中,我们学习了函数式编程。如果你对函数式编程的概念掌握得足够好,响应式编程的谜题将自动为你解决。我们还学习了函数式响应式编程的含义。
通过学习函数式编程,我们也对前一章中的约束有了清晰的认识。
我们还获得了对协程的介绍,这是 Kotlin 语言的一个突破性新特性。
我们已经用协程和几个新的函数式编程概念修改了我们的 ReactiveCalculator 类,并对其进行了优化。
第三章:可观察的、观察者和主题
可观察的、订阅者和响应式编程的基础。我们可以这样说,它们是响应式编程的构建块。在前两章中,你已经对Observables和subject有了一定的了解;我们通过observable/subject实例观察数据;但这并不是我们想要的全部;相反,我们希望将所有操作和数据更改以响应式的方式收集到observable实例中,使应用程序完全响应式。此外,在阅读前几章时,你可能想知道它究竟是如何操作的?在这一章中,让我们建立响应式编程支柱的基础——Observables、Observers和subjects:
-
我们将深入了解将各种数据源转换为
observable实例的细节 -
你将了解各种类型的
Observables。 -
如何使用
Observer实例和订阅,最后是subjects及其各种实现
我们还将学习Observable的各种工厂方法。
在这一章中有很多东西需要理解,所以让我们首先了解Observables。
可观察的
正如我们之前讨论的,在响应式编程中,Observable有一个底层计算,它产生可以被消费者(Observer)消费的值。这里最重要的一点是,消费者(Observer)在这里不是拉取值;相反,Observable将值推送到消费者。因此,我们可以说,Observable是一个基于推送、可组合的迭代器,它通过一系列操作符将项目发射到最终的Observer,最终消费这些项目。现在让我们按顺序逐一分析,以便更好地理解:
-
Observer订阅Observable -
Observable开始发射它内部拥有的项目 -
Observer对Observable发射的任何项目做出反应
因此,让我们深入了解一个Observable是如何通过其事件/方法工作的,即onNext、onComplete和onError。
可观察是如何工作的
正如我们之前所述,Observable有三个最重要的事件/方法;让我们逐一讨论它们:
-
onNext:Observable逐个将所有项目传递给此方法。 -
onComplete:当所有项目都通过onNext方法后,Observable调用onComplete方法。 -
onError:当Observable遇到任何错误时,它会调用onError方法来处理错误,如果已定义。请注意,onError和onComplete都是终止事件,如果调用onError,则永远不会调用onComplete,反之亦然。
这里需要注意的是,我们谈论的Observable中的项目可以是任何东西;它定义为Observable<T>,其中T可以是任何类;甚至可以将array/list分配为Observable。
让我们看看下面的图片:

让我们通过以下代码示例来更好地理解它:
fun main(args: Array<String>) {
val observer:Observer<Any> = object :Observer<Any>{//1
override fun onComplete() {//2
println("All Completed")
}
override fun onNext(item: Any) {//3
println("Next $item")
}
override fun onError(e: Throwable) {//4
println("Error Occured $e")
}
override fun onSubscribe(d: Disposable) {//5
println("Subscribed to $d")
}
}
val observable: Observable<Any> = listOf
("One", 2, "Three", "Four", 4.5, "Five", 6.0f).toObservable() //6
observable.subscribe(observer)//7
val observableOnList: Observable<List<Any>> =
Observable.just(listOf("One", 2, "Three", "Four",
4.5, "Five", 6.0f),
listOf("List with Single Item"),
listOf(1,2,3,4,5,6))//8
observableOnList.subscribe(observer)//9
}
在前面的例子中,我们在注释(1)处声明了observer实例的Any数据类型。
在这里,我们正在利用 Any 数据类型的好处。在 Kotlin 中,每个类都是 Any 的子类。此外,在 Kotlin 中,一切都是类和对象;没有单独的原始数据类型。
observer 接口中有四个声明的方法。注释 2 中的 onComplete() 方法在 Observable 完成所有项目且没有任何错误时被调用。在注释 3 中,我们定义了 onNext(item: Any) 函数,该函数将由 observable 为其必须发出的每个项目调用。在该方法中,我们将数据打印到控制台。在注释 4 中,我们定义了 onError(e: Throwable) 方法,在 Observable 遇到任何错误时将被调用。在注释 5 中,onSubscribe(d: Disposable) 方法将在 Observer 订阅 Observable 时被调用。在注释 6 中,我们将从列表创建 Observable(val observable)并使用 observer 在注释 7 中订阅 observable。在注释 8 中,我们再次创建一个可观察的(val observableOnList),它包含列表作为项目。
程序的输出如下:

如您在输出中看到的,对于第一次订阅(注释 7),当我们订阅 Observable 时,它调用 onSubscribe 方法,然后 Observable 开始发出项目,当 Observer 在 onNext 方法中开始接收它们并打印它们时。当 Observable 发出所有项目后,它调用 onComplete 方法来表示所有项目都已成功发出。第二个也是一样,只是这里每个项目都是一个列表。
因此,随着我们在 Observables 方面获得了一些基础知识,让我们学习创建 Observable 的各种方法——Observable 的工厂方法。
理解 Observable.create 方法
您可以在任何时间使用 Observable.create 方法创建自己的 Observable。此方法接受 ObservableEmitter<T> 接口的一个实例作为观察的源。因此,让我们考虑以下示例:
fun main(args: Array<String>) {
val observer: Observer<String> = object : Observer<String> {
override fun onComplete() {
println("All Completed")
}
override fun onNext(item: String) {
println("Next $item")
}
override fun onError(e: Throwable) {
println("Error Occured ${e.message}")
}
override fun onSubscribe(d: Disposable) {
println("New Subscription ")
}
}//Create Observer
val observable:Observable<String> = Observable.create<String> {//1
it.onNext("Emit 1")
it.onNext("Emit 2")
it.onNext("Emit 3")
it.onNext("Emit 4")
it.onComplete()
}
observable.subscribe(observer)
val observable2:Observable<String> = Observable.create<String> {//2
it.onNext("Emit 1")
it.onNext("Emit 2")
it.onNext("Emit 3")
it.onNext("Emit 4")
it.onError(Exception("My Custom Exception"))
}
observable2.subscribe(observer)
}
首先,我们创建了一个 Observer 接口的实例,就像之前的例子一样。我不会详细说明 observer,因为我们已经在之前的例子中看到了概述,我们将在本章后面详细讨论。
在注释 1 中,我们使用 Observable.create 方法创建了 Observable;我们通过 onNext 方法从 Observable 中发出四个 string,然后使用 onComplete 方法通知其完成。
在注释 2 中,我们做了几乎同样的事情,只是这里我们没有调用 onComplete,而是使用自定义 Exception 调用了 onError。
这是程序的输出:

Observable.create 方法很有用,尤其是在您使用自定义数据结构并希望控制要发出的值时。您还可以从不同的线程向 Observer 发送值。
注意,Observable契约(reactivex.io/documentation/contract.html)规定Observable必须按顺序(不是并行)向观察者发出通知。它们可以从不同的线程发出这些通知,但通知之间必须存在正式的“发生之前”关系。
理解Observable.from方法
Observable.from方法相对于Observable.create方法来说比较简单。你可以使用from方法从几乎任何 Kotlin 结构创建Observable实例。
注意,在 RxKotlin 1 中,你将有一个Observale.from方法;然而,从 RxKotlin 2.0(与 RxJava2.0 一样),操作符重载已被重命名为后缀,例如fromArray、fromIterable、fromFuture等等。
因此,让我们看看这段代码:
fun main(args: Array<String>) {
val observer: Observer<String> = object : Observer<String> {
override fun onComplete() {
println("All Completed")
}
override fun onNext(item: String) {
println("Next $item")
}
override fun onError(e: Throwable) {
println("Error Occured ${e.message}")
}
override fun onSubscribe(d: Disposable) {
println("New Subscription ")
}
}//Create Observer
val list = listOf("String 1","String 2","String 3","String 4")
val observableFromIterable: Observable<String> =
Observable.fromIterable(list)//1
observableFromIterable.subscribe(observer)
val callable = object : Callable<String> {
override fun call(): String {
return "From Callable"
}
}
val observableFromCallable:Observable<String> =
Observable.fromCallable(callable)//2
observableFromCallable.subscribe(observer)
val future:Future<String> = object :Future<String> {
override fun get(): String = "Hello From Future"
override fun get(timeout: Long, unit: TimeUnit?): String =
"Hello From Future"
override fun isDone(): Boolean = true
override fun isCancelled(): Boolean = false
override fun cancel(mayInterruptIfRunning: Boolean):
Boolean = false
}
val observableFromFuture:Observable<String> =
Observable.fromFuture(future)//3
observableFromFuture.subscribe(observer)
}
在注释1中,我使用了Observable.fromIterable方法从Iterable实例(这里,List)创建Observable。在注释2中,我调用了Observable.fromCallable方法从Callable实例创建Observable,同样,在注释3中,我调用了Observable.fromFuture方法从Future实例派生出Observable。
这是输出:

理解toObservable扩展函数
多亏了 Kotlin 的扩展函数,你可以轻松地将任何Iterable实例,如List,转换为Observable;我们已经在第一章,《反应式编程简介》中使用了这个方法,然而,看看这个:
fun main(args: Array<String>) {
val observer: Observer<String> = object : Observer<String> {
override fun onComplete() {
println("All Completed")
}
override fun onNext(item: String) {
println("Next $item")
}
override fun onError(e: Throwable) {
println("Error Occured ${e.message}")
}
override fun onSubscribe(d: Disposable) {
println("New Subscription ")
}
}//Create Observer
val list:List<String> = listOf
("String 1","String 2","String 3","String 4")
val observable:Observable<String> = list.toObservable()
observable.subscribe(observer)
}
以下是其输出:

那么,你难道不好奇想看看toObservable方法吗?让我们来看看。你可以在RxKotlin包提供的observable.kt文件中找到这个方法:
fun <T : Any> Iterator<T>.toObservable(): Observable<T> =
toIterable().toObservable()
fun <T : Any> Iterable<T>.toObservable(): Observable<T> =
Observable.fromIterable(this)
fun <T : Any> Sequence<T>.toObservable(): Observable<T> =
asIterable().toObservable()
fun <T : Any> Iterable<Observable<out T>>.merge(): Observable<T> =
Observable.merge(this.toObservable())
fun <T : Any> Iterable<Observable<out T>>.mergeDelayError():
Observable<T> = Observable.mergeDelayError(this.toObservable())
因此,它基本上内部使用了Observable.from方法;再次感谢 Kotlin 的扩展函数。
理解Observable.just方法
另一个有趣的工厂方法是Observable.just;此方法创建Observable并将传递给它的参数作为Observable的唯一项目。请注意,如果您将Iterable实例作为单个参数传递给Observable.just,它将整个list作为一个项目,这与Observable.from不同,后者将从Iterable中的每个项目创建Observable的项目。
当你调用Observable.just时,这是会发生的情况:
-
你用参数调用
Observable.just -
Observable.just将创建Observable -
它将每个参数作为
onNext通知发出 -
当所有参数成功发出后,它将发出
onComplete通知
让我们通过这个代码示例来更好地理解它:
fun main(args: Array<String>) {
val observer: Observer<Any> = object : Observer<Any> {
override fun onComplete() {
println("All Completed")
}
override fun onNext(item: Any) {
println("Next $item")
}
override fun onError(e: Throwable) {
println("Error Occured ${e.message}")
}
override fun onSubscribe(d: Disposable) {
println("New Subscription ")
}
}//Create Observer
Observable.just("A String").subscribe(observer)
Observable.just(54).subscribe(observer)
Observable.just(listOf("String 1","String 2","String 3",
"String 4")).subscribe(observer)
Observable.just(mapOf(Pair("Key 1","Value 1"),Pair
("Key 2","Value 2"),Pair("Key 3","Value
3"))).subscribe(observer)
Observable.just(arrayListOf(1,2,3,4,5,6)).subscribe(observer)
Observable.just("String 1","String 2",
"String 3").subscribe(observer)//1
}
以下是输出:

如您在输出中看到的,列表和映射也被视为单个项目,但请看代码中的注释 1,我在 Observable.just 方法的参数中传递了三个字符串。Observable.just 将每个参数作为单独的项目处理并相应地发出(参见输出)。
其他 Observable 工厂方法
在继续 Observer、订阅、取消订阅和 Subjects 之前,让我们尝试一些其他的 Observable 工厂方法。
因此,让我们首先看看这段代码,然后我们将逐行尝试学习它:
fun main(args: Array<String>) {
val observer: Observer<Any> = object : Observer<Any> {
override fun onComplete() {
println("All Completed")
}
override fun onNext(item: Any) {
println("Next $item")
}
override fun onError(e: Throwable) {
println("Error Occured ${e.message}")
}
override fun onSubscribe(d: Disposable) {
println("New Subscription ")
}
}//Create Observer
Observable.range(1,10).subscribe(observer)//(1)
Observable.empty<String>().subscribe(observer)//(2)
runBlocking {
Observable.interval(300,TimeUnit.MILLISECONDS).
subscribe(observer)//(3)
delay(900)
Observable.timer(400,TimeUnit.MILLISECONDS).
subscribe(observer)//(4)
delay(450)
}
}
在注释 (1) 中,我们使用 Observable.range() 工厂方法创建了 Observable。该方法创建一个 Observable 并发出带有 start 参数的整数,直到根据 count 参数发出指定数量的整数。
在注释 (2) 中,我们使用 Observable.empty() 方法创建了 Observable。该方法创建 Observable 并立即发出 onComplete(),而不使用 onNext() 发出任何项目。
在注释 (3) 和注释 (4) 中,我们使用了两个有趣的 Observable 工厂方法。注释 (3) 中的方法 Observable.interval(),从 0 开始按顺序发出数字,在每次指定的间隔后继续发出,直到你取消订阅和程序运行。而注释 (4) 中的方法 Observable.timer(),在指定时间过后只会发出一次 0。
如果您好奇的话,以下是输出:

订阅者 - Observer 接口
来自 RxKotlin 1.x 的 Subscriber 在 RxKotlin 2.x 中本质上变成了 Observer。在 RxKotlin 1.x 中有一个 Observer 接口,但 Subscriber 是传递给 subscribe() 方法的,它实现了 Observer。然而,在 RxJava 2.x 中,Subscriber 只在谈论 Flowables 时存在,我们将在第四章 介绍背压和 Flowables 中介绍。
如您在本章前面的示例中所见,Observer 是一个包含四个方法的接口——onNext(item:T)、onError(error:Throwable)、onComplete() 和 onSubscribe(d:Disposable)。如前所述,当我们连接 Observable 到 Observer 时,它会在 Observer 中寻找这四个方法并调用它们。因此,以下是对这四个方法的简要描述:
-
onNext:Observable调用Observer的此方法来逐个传递每个项目。 -
onComplete: 当Observable想要表示完成时,它通过传递项目到onNext方法来完成,然后调用Observer的onComplete方法。 -
onError: 当Observable遇到任何错误时,如果Observer中定义了onError方法,它会调用该方法来处理错误,否则,它会抛出异常。 -
onSubscribe: 每当新的Observable订阅到Observer时,都会调用此方法。
订阅和取消订阅
因此,我们有Observable(需要观察的事物)和Observer(需要观察者);现在怎么办?如何将它们连接起来?Observable和Observer就像输入设备(无论是键盘还是鼠标)和计算机一样,我们需要某种东西来连接它们(即使是无线输入设备也有一些连接通道,无论是蓝牙还是 Wi-Fi)。
subscribe操作符的作用是将Observable连接到Observer,就像媒体的作用一样。我们可以向subscribe操作符传递一到三个方法(onNext、onComplete、onError),或者我们可以向subscribe操作符传递Observer接口的实例,以获取与Observer连接的Observable。
现在,让我们看看以下示例:
fun main(args: Array<String>) {
val observable:Observable<Int> = Observable.range(1,5)//1
observable.subscribe({//2
//onNext method
println("Next $it")
},{
//onError Method
println("Error ${it.message}")
},{
//onComplete Method
println("Done")
})
val observer: Observer<Int> = object : Observer<Int> {//3
override fun onComplete() {
println("All Completed")
}
override fun onNext(item: Int) {
println("Next $item")
}
override fun onError(e: Throwable) {
println("Error Occurred ${e.message}")
}
override fun onSubscribe(d: Disposable) {
println("New Subscription ")
}
}
observable.subscribe(observer)
}
在这个例子中,我们创建了Observable实例(在注释1处),并且两次使用了不同的重载subscribe操作符。在注释2处,我们将三个方法作为参数传递给了subscribe方法。第一个参数是onNext方法,第二个是onError方法,最后是onComplete。在注释2处,我们传递了一个Observer接口的实例。
输出可以很容易地预测如下:

因此,我们已经了解了订阅的概念,现在我们可以进行订阅了。如果你想在订阅一段时间后停止发射,那必须有一种方法,对吧?所以让我们来检查一下。
记得Observer的onSubscribe方法吗?在那个方法中有一个参数我们还没有讨论过。当你subscribe时,如果你传递方法而不是Observer实例,那么subscribe操作符将返回一个Disposable实例,或者如果你使用Observer的实例,那么你将在onSubscribe方法的参数中得到Disposable实例。
你可以使用Disposable接口的实例在任何给定时间停止发射。让我们看看这个示例:
fun main(args: Array<String>) {
runBlocking {
val observale:Observable<Long> =
Observable.interval(100,TimeUnit.MILLISECONDS)//1
val observer:Observer<Long> = object : Observer<Long> {
lateinit var disposable:Disposable//2
override fun onSubscribe(d: Disposable) {
disposable = d//3
}
override fun onNext(item: Long) {
println("Received $item")
if(item>=10 && !disposable.isDisposed) {//4
disposable.dispose()//5
println("Disposed")
}
}
override fun onError(e: Throwable) {
println("Error ${e.message}")
}
override fun onComplete() {
println("Complete")
}
}
observale.subscribe(observer)
delay(1500)//6
}
}
我希望你能记得几页前的这个章节中提到的Observable.interval工厂方法。这个方法接受两个参数,描述了间隔期间和时间单位,然后,它按顺序打印整数,从0开始。使用间隔创建的Observable永远不会完成,也永远不会停止,除非你停止它们或者程序停止执行。我认为它非常适合这个场景,因为我们在这里想要在中间停止Observable。
所以,在这个例子中,在注释1处,我们使用Observable.interval工厂方法创建了一个Observable,该Observable将在每个100毫秒的间隔后发射一个整数。
在注释2处,我声明了一个lateinit var disposable类型的Disposable变量(lateinit意味着该变量将在稍后的时间点初始化)。在注释3处,在onSubscribe方法内部,我们将接收到的参数值赋给disposable变量。
我们打算在序列达到10后停止执行,也就是说,在10被发射后,应立即停止发射。为了实现这一点,我们在onNext方法内部放置了一个检查,检查发射项的值是否等于或大于10,并且如果发射尚未停止(已释放),则我们将释放发射(注释5)。
这里是输出结果:
Received 0
Received 1
Received 2
Received 3
Received 4
Received 5
Received 6
Received 7
Received 8
Received 9
Received 10
Disposed
从输出中,我们可以看到在调用disposable.dispose()方法后没有发射任何整数,尽管执行等待了 500 毫秒更多(100*10=1000 毫秒来打印序列直到10,我们使用 1500 毫秒调用delay方法,因此发射10后 500 毫秒)。
如果你好奇想了解Disposable接口,那么以下是其定义:
interface Disposable {
/**
* Dispose the resource, the operation should be idempotent.
*/
fun dispose()
/**
* Returns true if this resource has been disposed.
* @return true if this resource has been disposed
*/
val isDisposed:Boolean
}
它有一个属性表示是否已经通知停止发射(已释放)以及一个方法来通知停止发射(释放)。
热和冷可观测对象
因此,既然我们已经掌握了Observables和Observers的基本概念,让我们转向更有趣和高级的内容。我们一直在谈论的Observables可以根据其行为分为两类。正如标题所暗示的,这两类是Hot Observables和Cold Observables。我可以打赌,到现在为止,你一定渴望了解更多关于Hot和Cold Observables的信息,不是吗?那么,让我们深入探讨吧。
冷可观测对象
仔细查看所有之前的示例。在所有示例中,如果你多次订阅相同的Observable,你将获得所有订阅的开始处的发射。不相信?看看下面的示例:
fun main(args: Array<String>) {
val observable: Observable<String> = listOf
("String 1","String 2","String 3","String 4").toObservable()//1
observable.subscribe({//2
println("Received $it")
},{
println("Error ${it.message}")
},{
println("Done")
})
observable.subscribe({//3
println("Received $it")
},{
println("Error ${it.message}")
},{
println("Done")
})
}
这里是它的输出结果:

程序相当简单。在注释1处声明了一个Observable,在注释2和3处两次订阅了Observable。现在,看看输出结果。对于两次订阅调用,你从第一个到最后的发射都是完全相同的。
这些具有这种特定行为的Observables,即对每个订阅从开始处发射项目,被称为Cold Observable。更具体地说,Cold Observables在订阅时开始运行,Cold Observable在调用subscribe后开始推送项目,并在每个订阅上推送相同的项目序列。
我们在本章中使用的所有Observable工厂方法都返回Cold Observables。Cold Observables类似于数据。当我们处理数据时,例如,在 Android 中使用 SQLite 或 Room 数据库时,我们更依赖于Cold Observables而不是Hot Observables。
热可观测对象
Cold Observables 是被动的,它们在调用 subscribe 之前不会发出任何内容。Hot Observables 与 Cold Observables 相反;它不需要订阅就可以开始发射。虽然你可以将 Cold Observables 比喻为 CD/DVD 录音,但 Hot Observables 更像电视频道——它们继续广播(发射)其内容,无论是否有观众(观察者)观看。
Hot Observables 更像事件而不是数据。事件可能携带数据,但存在一个时间敏感的组件,其中最近订阅的 Observers 可能会错过之前发出的数据。它们在处理 Android/JavaFX/Swing 中的 UI 事件时特别有用。它们在模拟服务器请求时也非常有用。
介绍 ConnectableObservable 对象
ConnectableObservable 是 Hot Observables 的一个很好的例子。它是最有帮助的 Hot Observables 形式之一。它可以将任何 Observable,甚至是一个 Cold Observable,转换为 Hot Observable。它不会在 subscribe 调用时开始发射;相反,它在调用 connect 方法后才会激活。您必须在调用 connect 之前进行 subscribe 调用;任何在调用 connect 之后进行的 subscribe 调用都会错过之前发出的发射。
让我们考虑以下代码片段:
fun main(args: Array<String>) {
val connectableObservable = listOf
("String 1","String 2","String 3","String 4","String
5").toObservable()
.publish()//1
connectableObservable.subscribe({ println
("Subscription 1: $it") })//2
connectableObservable.map(String::reversed)//3
.subscribe({ println("Subscription 2 $it")})//4
connectableObservable.connect()//5
connectableObservable.subscribe({ println
("Subscription 3: $it") })//6 //Will not receive emissions
}
ConnectableObservable 的主要目的是为了具有多个订阅的 Observables 将一个 Observable 的所有订阅连接起来,以便它们可以响应单个推送;与重复操作以执行推送并针对每个订阅分别推送的 Cold Observables 相反,从而重复循环。ConnectableObservable 连接在 connect 方法之前调用的所有 subscriptions(Observers),并将单个推送传递给所有 Observers,然后 Observers 对该推送做出反应/处理。
在前面的示例中,我们使用 toObservable() 方法创建了 Observable,然后在注释 1 上,我们使用了 publish 操作符将 Cold Observable 转换为 ConnectableObservable。
在注释 2 上,我们订阅了 connectableObservable。在注释 3 上,我们使用了 map 操作符来反转 String,在注释 4 上,我们订阅了映射后的 connectableObservable。
在注释 5 上,我们调用了 connect 方法,并且发射开始对两个 Observers 进行。
注意,我们在示例中的注释 3 上使用了 map 操作符。我们将在第五章异步数据操作符和转换中详细讨论 map 操作符。然而,如果你好奇,这里就是定义:map 操作符将你选择的函数应用于源 Observable 发出的每个项目,并返回一个发出这些函数应用结果的 Observable。
这里是输出:

注意,正如输出所示,每次发射都会同时发送到每个Observer,并且它们以交错的方式处理数据。
这种从Observable一次发射然后将其发射传递给所有Subscriptions/Observers的机制被称为多播。
还要注意,在connect之后的注释6上的subscribe调用没有收到任何发射,因为ConnectableObservable是热的,并且任何在连接之后发生的新订阅都会错过之前发射的发射(记住,在connect方法的调用和新订阅之间,计算机可以在几毫秒内完成很多任务);在这种情况下,它错过了所有的发射。
以下代码片段是另一个例子,以使你更好地理解它:
fun main(args: Array<String>) {
val connectableObservable =
Observable.interval(100,TimeUnit.MILLISECONDS)
.publish()//1
connectableObservable.
subscribe({ println("Subscription 1: $it") })//2
connectableObservable
.subscribe({ println("Subscription 2 $it")})//3
connectableObservable.connect()//4
runBlocking { delay(500) }//5
connectableObservable.
subscribe({ println("Subscription 3: $it") })//6
runBlocking { delay(500) }//7
}
这个例子几乎和上一个例子一样,只是做了一些小的调整。
在这里,我们使用了Observable.interval方法来创建Observable;好处是,由于它在每次发射之前都有一个间隔,因此它会给订阅者在连接后提供一些空间来获取一些发射。
在注释1上,我们将Cold Observable转换为ConnectableObservable,就像上一个例子一样,然后进行了两次订阅,然后连接,就像上一个例子中的注释2、3、4一样。
我们在注释5上直接调用延迟,然后在注释6上再次订阅,然后在注释7上再次延迟,以允许第3次订阅打印一些数据。
以下输出将使我们更好地理解:

仔细查看输出,注意第3次订阅收到了序列5的发射,并且错过了所有之前的发射(在3次订阅之前有5次发射——500 毫秒延迟/100 毫秒间隔)。
主题
实现Hot Observables的另一种伟大方式是Subject。基本上,它是由Observable和Observer组合而成的,因为它具有许多与两者都共同的行为。就像Hot Observables一样,它维护一个内部的Observer列表,并在发射时将单个推送传递给当时订阅它的每个Observer。
那么,让我们来看看Subject能为我们提供什么。为什么它被称为Observables和Observers的组合?请参考以下要点:
-
它拥有
Observable应该拥有的所有操作符。 -
就像
Observer一样,它可以监听发送给它的任何值。 -
在
Subject完成/出错/取消订阅后,它不能被重用。 -
最有趣的一点是它通过自己传递值。作为解释,如果你通过
onNext将一个值传递给Subject(Observer)一侧,它将从中的一侧Observable出来。
因此,Subject是Observable和Observer的组合。您已经在之前的章节中看到了Subject的使用,但为了使事情更清晰,让我们举一个新的例子:
fun main(args: Array<String>) {
val observable = Observable.interval(100,
TimeUnit.MILLISECONDS)//1
val subject = PublishSubject.create<Long>()//2
observable.subscribe(subject)//3
subject.subscribe({//4
println("Received $it")
})
runBlocking { delay(1100) }//5
}
让我们先检查输出,然后我们将解释代码:

现在,让我们理解代码。在这个程序中,我们使用了古老的Observable.interval方法。所以,在注释1中,我们再次使用Observable.interval创建了一个Observable实例,间隔为 100 毫秒。
在注释2中,我们使用PublishSubject.create()创建了Subject。
可用的Subject类型有很多。PublishSubject就是其中之一。PublishSubject只会向observer发出那些在订阅时间之后由Observable源发出的项目。
我们将在本章下一节详细讨论Subject的各种类型。
在注释3中,我们像使用Observer一样使用了Subject实例,来订阅Observable实例的发射。在注释4中,我们像使用Observable一样使用了Subject实例,并用 lambda 表达式订阅以监听Subject实例的发射。
你可能已经习惯了注释5中的代码;如果没有,那么我们使用它来使程序等待1100毫秒,以便我们可以看到由间隔程序产生的输出。你可以将delay方法视为类似于 Java 中的sleep方法,唯一的区别在于在这里你必须在一个Coroutine context中使用delay,因此,为了使用delay方法,你必须指定并启动一个Coroutine context;这并不总是可能的。runBlocking方法就是为了帮助你在那种情况下;它在调用线程内部模拟一个Coroutine context,同时阻塞该线程直到runBlocking完成所有代码的执行。
Subject实例监听Observable实例的发射,然后将这些发射广播给它的Observers,很可能就像一个正在播放电影(从 CD/DVD 录制)的电视频道。
你可能想知道这有什么好处?当我可以直接将subscribe和Observer订阅到Observable上时,为什么要在中间使用PublishSubject?为了找到答案,让我们稍微修改一下这段代码,以便更好地理解它:
fun main(args: Array<String>) {
val observable = Observable.interval(100,
TimeUnit.MILLISECONDS)//1
val subject = PublishSubject.create<Long>()//2
observable.subscribe(subject)//3
subject.subscribe({//4
println("Subscription 1 Received $it")
})
runBlocking { delay(1100) }//5
subject.subscribe({//6
println("Subscription 2 Received $it")
})
runBlocking { delay(1100) }//7
}
这里,代码直到注释5几乎相同(除了注释3中的Subscribe,我在字符串输出前添加了Subscription 1)。
在注释6中,我们又订阅了subject。由于我们在1100毫秒后订阅,它应该会在前 11 次发射之后接收发射。在注释7中,我们再次通过1100毫秒使程序等待。
让我们看看输出:
Subscription 1 Received 0
Subscription 1 Received 1
Subscription 1 Received 2
Subscription 1 Received 3
Subscription 1 Received 4
Subscription 1 Received 5
Subscription 1 Received 6
Subscription 1 Received 7
Subscription 1 Received 8
Subscription 1 Received 9
Subscription 1 Received 10
Subscription 1 Received 11
Subscription 2 Received 11
Subscription 1 Received 12
Subscription 2 Received 12
Subscription 1 Received 13
Subscription 2 Received 13
Subscription 1 Received 14
Subscription 2 Received 14
Subscription 1 Received 15
Subscription 2 Received 15
Subscription 1 Received 16
Subscription 2 Received 16
Subscription 1 Received 17
Subscription 2 Received 17
Subscription 1 Received 18
Subscription 2 Received 18 Subscription 1 Received 19
Subscription 2 Received 19
Subscription 1 Received 20
Subscription 2 Received 20
Subscription 1 Received 21
Subscription 2 Received 21
在输出中,它正在打印从第12次发射(序列11)开始的第二次订阅。所以,Subject不会重放像Cold Observables这样的操作,它只是将发射转发给所有Observers,将Cold Observable转换为Hot Observers。
Subject的多样性
如我们之前提到的,Subject有很多种可用。既然我们在Subject上已经有所掌握,现在让我们深入了解Subject的各种类型,以更好地理解它。所以,这里是一些最有用和最重要的Subject类型,我们将在下面讨论:
-
AsyncSubject -
PublishSubject -
BehaviorSubject -
ReplaySubject
理解AsyncSubject
AsyncSubject只发射源Observable(它监听的Observable)的最后一个值,并且只发射最后一个发射。为了更清楚地说明,AsyncSubject将发射它得到的最后一个值,并且只发射一次。
这是一个AsyncSubject的弹珠图,它来自 ReactiveX 文档(reactivex.io/documentation/subject.html):

让我们考虑以下代码示例:
fun main(args: Array<String>) {
val observable = Observable.just(1,2,3,4)//1
val subject = AsyncSubject.create<Int>()//2
observable.subscribe(subject)//3
subject.subscribe({//4
//onNext
println("Received $it")
},{
//onError
it.printStackTrace()
},{
//onComplete
println("Complete")
})
subject.onComplete()//5
}
这里是输出:
Received 4
Complete
在这个例子中,我们使用Observable.just创建了一个示例,包含4个整数(在注释1处)。然后,在注释2处,我们创建了一个AsyncSubject示例。之后,在注释3和4处,就像前面的例子一样,我们用subject订阅了observable instance,然后用 lambda 订阅了Subject实例;只是这次,我们传递了所有三个方法——onNext、onError和onComplete。
在注释6处,我们调用了onComplete。
如输出所示,Subject只发射了它得到的最后一个值,即4。
在Subject实例上,你可以直接通过onNext方法传递值,而不需要订阅任何Observable。回想一下前几章中的例子,我们使用了Subject(PublishSubject);在那里,我们只使用了onNext来传递值。你可以用Subject订阅另一个Observable,或者用onNext传递值。基本上,当你用Subject订阅Observable时,Subject会在Observable值发射时内部调用其onNext。
有疑问吗?让我们稍微调整一下代码。我们不会订阅Observable,而只会调用onNext来传递值,并将有另一个订阅。以下是代码,以这样做:
fun main(args: Array<String>) {
val subject = AsyncSubject.create<Int>()
subject.onNext(1)
subject.onNext(2)
subject.onNext(3)
subject.onNext(4)
subject.subscribe({
//onNext
println("S1 Received $it")
},{
//onError
it.printStackTrace()
},{
//onComplete
println("S1 Complete")
})
subject.onNext(5)
subject.subscribe({
//onNext
println("S2 Received $it")
},{
//onError
it.printStackTrace()
},{
//onComplete
println("S2 Complete")
})
subject.onComplete()
}
这里是输出:

在这里,我们通过onNext传递了所有值;它只将得到的最后一个值(5)发射给两个订阅。仔细观察,第一个订阅是在传递最后一个值之前。由于ConnectableObservable在调用connect时开始发射,AsyncSubject只在调用onComplete时发射其唯一值。
注意,正如输出所示,AsyncSubject不是以交错方式发射的,也就是说,它会多次重放其操作以将值发射给多个Observers,尽管它只发射一个值。
理解PublishSubject
PublishSubject在订阅时发出它获取的所有后续值,无论它是通过onNext方法还是通过另一个订阅获取的。我们已经看到了PublishSubject的应用,它是最常见的Subject变体。
这里是PublishSubject的图形表示,它取自 ReactiveX 文档(reactivex.io/documentation/subject.html):

理解BehaviorSubject
如果我们将AsyncSubject和PublishSubject结合起来呢?或者混合两者的优点?在多播工作时,BehaviorSubject会发出在订阅之前获取的最后一个项目以及订阅时的所有后续项目,即它保持一个内部的观察者列表并将相同的发出传递给所有其观察者,而不进行重放。
这里是图形表示,取自 ReactiveX 文档(reactivex.io/documentation/subject.html):

让我们用BehaviorSubject修改最后一个例子,看看会发生什么:
fun main(args: Array<String>) {
val subject = BehaviorSubject.create<Int>()
subject.onNext(1)
subject.onNext(2)
subject.onNext(3)
subject.onNext(4)
subject.subscribe({
//onNext
println("S1 Received $it")
},{
//onError
it.printStackTrace()
},{
//onComplete
println("S1 Complete")
})
subject.onNext(5)
subject.subscribe({
//onNext
println("S2 Received $it")
},{
//onError
it.printStackTrace()
},{
//onComplete
println("S2 Complete")
})
subject.onComplete()
}
在这里,我选取了上一个例子,其中我们使用了AsyncSubject,并用BehaviorSubject进行了修改。那么,让我们看看输出并理解BehaviorSubject:
S1 Received 4
S1 Received 5
S2 Received 5
S1 Complete
S2 Complete
当第一个订阅获得4和5时;4是在其订阅之前发出的,而5是在之后。对于第二个订阅,它只获得了5,这是在其订阅之前发出的。
理解ReplaySubject
它更像是冷观察者;无论观察者何时订阅,它都会重新播放它获取的所有项目。
这里是图形表示:

图片来源:reactivex.io/documentation/subject.html
让我们用ReplaySubject修改之前的程序:
fun main(args: Array<String>) {
val subject = ReplaySubject.create<Int>()
subject.onNext(1)
subject.onNext(2)
subject.onNext(3)
subject.onNext(4)
subject.subscribe({
//onNext
println("S1 Received $it")
},{
//onError
it.printStackTrace()
},{
//onComplete
println("S1 Complete")
})
subject.onNext(5)
subject.subscribe({
//onNext
println("S2 Received $it")
},{
//onError
it.printStackTrace()
},{
//onComplete
println("S2 Complete")
})
subject.onComplete()
}
此外,这里是输出:
S1 Received 1
S1 Received 2
S1 Received 3
S1 Received 4
S1 Received 5
S2 Received 1
S2 Received 2
S2 Received 3
S2 Received 4
S2 Received 5
S1 Complete
S2 Complete
它为两个订阅都发出了所有项目。
摘要
在本章中,我们学习了Observables和Observers以及如何使用它们。我们通过几个例子来加强我们对它们的掌握。我们了解到Observables有两种类型——热 Observables和冷 Observables。我们还学习了几个Subject及其变体。几个Subject基本上是Observables和许多Observer的组合。
虽然Observables为我们提供了极大的灵活性和功能,但它也有一些缺点,比如背压。对此好奇吗?想了解更多关于Observables的缺点以及如何克服它们的信息?那么就赶快翻到第四章吧。
第四章:背压和 Flowables 简介
到目前为止,我们一直在尝试理解基于推的响应式编程架构。到目前为止,我们已经对 Observables 有了一个很好的理解。我们现在明白,Observable 会发出项目供 Observer 消费以进行进一步处理。然而,在阅读前面的章节时,你是否曾经想过这样一个情况:Observable 发出项目的速度比 Observer 消费它们的速度快?整章都致力于这个问题。我们将首先尝试理解这个问题何时以及如何发生,然后我们将尝试解决这个问题。
因此,在本章中,我们将重点关注以下主题,并在本章结束时,我们应该有解决前面提到的问题的解决方案:
-
理解背压
-
Flowables 和 Subscriber
-
使用
Flowable.create()创建 Flowables -
一起使用 Observable 和 Flowables
-
背压操作符
-
Flowable.generate()操作符
现在,让我们从背压——Observables 的问题开始。
理解背压
Observable 的唯一问题是当 Observer 无法跟上 Observable 的节奏时。默认情况下,Observable 通过将项目同步推送到 Observer,一次一个,来链式工作。然而,如果 observer 必须执行一些耗时的计算,这可能会比 Observable 每个项目发射间隔更长。困惑吗?让我们考虑这个例子:
fun main(args: Array<String>) {
val observable = Observable.just(1,2,3,4,5,6,7,8,9)//(1)
val subject = BehaviorSubject.create<Int>()
subject.observeOn(Schedulers.computation())//(2)
.subscribe({//(3)
println("Subs 1 Received $it")
runBlocking { delay(200) }//(4)
})
subject.observeOn(Schedulers.computation())//(5)
.subscribe({//(6)
println("Subs 2 Received $it")
})
observable.subscribe(subject)//(7)
runBlocking { delay(2000) }//(8)
}
代码相当简单。我们在注释 (1) 上创建了 Observable,然后,我们创建了 BehaviorSubject,然后在注释 (3) 和 (6) 上,我们订阅了 BehaviorSubject。在注释 (7) 上,在订阅 BehaviorSubject 之后,我们将使用 BehaviorSubject 来订阅 Observable,这样 BehaviorSubject 的 Observers 就应该接收到所有的发射。在注释 (4) 的第一个订阅中,我们使用了 delay 方法来模拟一个耗时订阅者。在注释 (2) 和 (6) 上有新的代码,subject.observeOn(Schedulers.computation());我们将在后面的章节中详细讨论这个方法,但现在,只需记住这个 observeOn 方法帮助我们指定运行订阅的线程,而 Scheduler.computation() 提供了一个用于执行计算的线程。在注释 (8) 上,我们使用了 delay 方法来等待执行,因为执行将在后台进行。
基于我们从前面章节中获得的知识,我们可以说订阅应该以交错的方式打印出 1 到 9 的所有数字,对吗?让我们先看看输出:

对输出感到震惊吗?订阅2并没有像预期的那样交错工作,它在订阅1打印第二个数字之前就已经完成了所有数字的打印,尽管它先开始打印。那么,为什么它打破了Hot Observables的行为?为什么两个Observer没有以交错的方式工作?让我们来检查一下。实际上,程序并没有打破Hot Observables的行为,subject实际上为两个observer各发射了一次;然而,对于第一个observer来说,每次计算都很长,排放被排队;这显然不是什么好事,因为这可能导致很多问题,包括OutOfMemoryError异常。
仍然有疑问?让我们看看另一个例子:
fun main(args: Array<String>) {
val observable = Observable.just(1,2,3,4,5,6,7,8,9)//(1)
observable
.map { MyItem(it) }//(2)
.observeOn(Schedulers.computation())//(3)
.subscribe({//(4)
println("Received $it")
runBlocking { delay(200) }//(5)
})
runBlocking { delay(2000) }//(6)
}
data class MyItem (val id:Int) {
init {
println("MyItem Created $id")//(7)
}
}
在这个例子中,我们消除了Subject和多个Subscribers,使程序更简单、更容易理解。我们已经在上一章中介绍了map操作符,我们使用它来在注释(2)中将Int项目转换为MyItem对象。
如果你忘记了上一章中的map操作符,它接受一个源可观察对象,在运行时处理它们发出的项目,并创建另一个可观察对象来观察。简单来说,map操作符位于subscribe之前,用于在将新生成的项目传递给observer之前处理observable发出的每个项目。我们将在后面的章节中更详细地了解map操作符。
在这里,我们使用它来跟踪每次排放。每当发生排放时,它将立即传递给map操作符,在那里我们创建MyItem类的对象。在MyItem类的init块中,我们打印传递给它的值;因此,一旦有项目被排放,它就会被MyItem类打印出来。
在这里,MyItem类是一个data class,即它将默认拥有val id和toString方法的 getter。
程序的剩余部分几乎相同;让我们看看输出,然后我们继续讨论:

如我们在输出中看到的那样,许多MyItem(即排放)的创建非常快,甚至在Observer(即消费者)开始打印之前就已经完成了。
因此,问题是排放被消费者排队,而消费者正忙于处理生产者之前处理的排放。
解决这个问题的方法之一是消费者到生产者的反馈通道,通过这个通道,消费者可以告诉生产者等待它完成处理之前的排放。这样,消费者或消息中间件就不会在高负载下变得饱和和无响应;相反,它们可能会请求更少的消息,让生产者决定如何减速。这个反馈通道被称为背压。在Observables和Observers中不支持背压,解决方案可能是使用Flowables和Subscribers。让我们学习这些是什么。
Flowable
我们可以将 Flowables 称为 Observables 的带背压版本。可能 Flowables 和 Observables 之间唯一的区别就是 Flowable 考虑了背压,而 Observable 没有。就是这样。Flowable 为操作符提供了默认的 128 个元素的缓冲区大小,因此,当消费者花费时间时,发出的项目可能会在缓冲区中等待。
注意,Flowables 是在 ReactiveX 2.x(RxKotlin 2.X)中添加的,而之前的版本不包括它们。相反,在之前的版本中,Observables 被修改以支持背压,这导致了许多意外的MissingBackpressureException。
如果你对发布说明感兴趣,请看以下内容:
github.com/ReactiveX/RxJava/wiki/What%27s-different-in-2.0#observable-and-flowable
我们已经进行了长时间的讨论;现在让我们动手写代码。首先,我们将尝试使用 Observable 的代码,然后我们将使用 Flowables 来做同样的事情,以观察和理解它们之间的区别:
fun main(args: Array<String>) {
Observable.range(1,1000)//(1)
.map { MyItem3(it) }//(2)
.observeOn(Schedulers.computation())
.subscribe({//(3)
print("Received $it;\t")
runBlocking { delay(50) }//(4)
},{it.printStackTrace()})
runBlocking { delay(60000) }//(5)
}
data class MyItem3 (val id:Int) {
init {
print("MyItem Created $id;\t")
}
}
一个简单的使用Observable.range()操作符的代码示例,该操作符应该从1到1000发出数字。在注释(2)中,我们使用了map操作符将Int转换为MyItem3对象。在注释(3)中,我们订阅了Observable。在注释(4)中,我们运行了一个阻塞延迟来模拟长时间运行的订阅代码。在注释(5)中,我们再次运行了一个阻塞延迟代码,等待消费者完成所有项目的处理后再停止程序执行。
整个输出将占用一些空间,因此我们将部分输出作为截图展示:

如果你仔细观察输出(截图),你会注意到 Observable(生产者)继续发出项目,尽管观察者(消费者)根本跟不上它的节奏。直到观察者(生产者)完成所有项目的发出,观察者(消费者)才处理了第一个项目(项目 1)。如前所述,这可能导致很多问题,包括OutOfMemory错误。现在,让我们将代码中的Observable替换为Flowable:
fun main(args: Array<String>) {
Flowable.range(1,1000)//(1)
.map { MyItem4(it) }//(2)
.observeOn(Schedulers.io())
.subscribe({//(3)
println("Received $it")
runBlocking { delay(50) }//(4)
},{it.printStackTrace()})
runBlocking { delay(60000) }//(5)
}
data class MyItem4 (val id:Int) {
init {
println("MyItem Created $id")
}
}
代码与上一个完全相同,唯一的区别是我们将Observable写成了Flowable。现在,让我们看看输出并注意区别:

你注意到区别了吗?Flowable 不是一次性发出所有项目,而是分批发出少量项目,等待消费者处理完毕后再继续,并以交错的方式完成。这本身就能减少很多问题。
何时使用 Flowables 和 Observables
到目前为止,你可能认为 Flowable 是一个方便的工具,所以你可以在任何地方替换 Observable。然而,这并不总是如此。尽管 Flowable 为我们提供了背压策略,但 Observables 的存在是有原因的,它们各自都有优势和劣势。那么,何时使用哪一个?让我们看看。
何时使用 Flowables?
以下是你应该考虑使用 Flowables 的情况。记住,Flowables 比 Observables 慢:
-
Flowables 和背压旨在帮助处理大量数据。所以,如果你的源可能排放 10,000+项,请使用 Flowable。特别是当源是异步的,这样消费者链可以在需要时要求生产者限制/调节排放。
-
如果你正在读取/解析文件或数据库。
-
当你想从支持阻塞并返回结果的网络 IO 操作/流式 API 中发射时,这是许多 IO 源的工作方式。
何时使用 Observables?
现在你已经知道了何时使用 Flowables,让我们看看你应该优先考虑 Observables 的条件:
-
当你处理的数据量较小(少于 10,000 个排放量)时
-
当你执行严格同步操作或有限并发操作时
-
当你正在发射 UI 事件(在处理 Android、JavaFX 或 Swing 时)
此外,请记住,与 Observables 相比,Flowables 较慢。
流式处理和订阅者
相比于 Observer,Flowable 使用的是支持背压的 Subscriber。然而,如果你使用 lambda 表达式,那么你将不会注意到任何区别。那么,为什么使用 Subscriber 而不是 Observer 呢?因为 Subscriber 支持一些额外的操作和背压。例如,它可以向上游传达它希望接收多少项作为消息。或者,更确切地说,当我们使用 Subscriber 时;你必须指定你想要从上游接收(请求)多少项;如果你不指定它,你将不会收到任何排放。
正如我们之前提到的,使用Subscriber的 lambda 表达式与 Observe 类似;这种实现将自动从上游请求无界数量的排放。就像我们最后的代码一样,我们没有指定我们想要多少排放,但它内部请求了无界数量的排放,这就是为什么我们收到了所有排放的项目。
因此,让我们尝试用Subscriber实例替换之前的程序:
fun main(args: Array<String>) {
Flowable.range(1, 1000)//(1)
.map { MyItem5(it) }//(2)
.observeOn(Schedulers.io())
.subscribe(object : Subscriber<MyItem5> {//(3)
override fun onSubscribe(subscription: Subscription) {
subscription.request(Long.MAX_VALUE)//(4)
}
override fun onNext(s: MyItem5?) {
runBlocking { delay(50) }
println("Subscriber received " + s!!)
}
override fun onError(e: Throwable) {
e.printStackTrace()
}
override fun onComplete() {
println("Done!")
}
})
runBlocking { delay(60000) }
}
data class MyItem5 (val id:Int) {
init {
println("MyItem Created $id")
}
}
前一个程序输出的结果将与之前的相同,所以我们在这里省略输出。相反,让我们理解代码。程序几乎与上一个相同,直到注释(3)处,我们创建了一个Subscriber实例。Subscriber的方法与Observer相同;然而,正如我之前提到的,在subscribe方法上,你必须请求你想要初始的排放数量。我们在注释(4)处也做了同样的事情;然而,因为我们想要接收所有排放,所以我们用Long.MAX_VALUE请求。
那么,request 方法是如何工作的?request() 方法将请求 Subscriber 应该监听的上游发射的数量,从方法调用后开始计数。Subscriber 将忽略请求的发射之后的任何发射,直到你请求更多。
因此,让我们修改这个程序以更好地理解 request 方法:
fun main(args: Array<String>) {
Flowable.range(1, 15)
.map { MyItem6(it) }
.observeOn(Schedulers.io())
.subscribe(object : Subscriber<MyItem6> {
lateinit var subscription: Subscription//(1)
override fun onSubscribe(subscription: Subscription) {
this.subscription = subscription
subscription.request(5)//(2)
}
override fun onNext(s: MyItem6?) {
runBlocking { delay(50) }
println("Subscriber received " + s!!)
if(s.id == 5) {//(3)
println("Requesting two more")
subscription.request(2)//(4)
}
}
override fun onError(e: Throwable) {
e.printStackTrace()
}
override fun onComplete() {
println("Done!")
}
})
runBlocking { delay(10000) }
}
data class MyItem6 (val id:Int) {
init {
println("MyItem Created $id")
}
}
那么,我们在程序中做了哪些调整?让我们逐个检查。在注释 (1) 中,我们声明了一个 lateinit 类型的 Subscription 变量,并在注释 (2) 之前在 onSubscribe 方法中初始化了这个订阅。在注释 (2) 中,我们使用 subscription.request(5) 请求了 5 个项目。然后,在 onNext 中,注释 (3),我们检查接收到的项目是否是第 5 个(因为我们使用的是范围,第 5 个项目的值将是 5);如果项目是第 5 个,那么我们再次请求 2 个。所以,程序应该打印出七个项目,而不是 1 到 15 的范围。让我们检查以下输出:

因此,尽管 Flowable 发射了该范围内的所有项目,但它们在 7 之后从未传递给 Subscriber。
注意,request() 方法并不是直接向上游传递,它只是将信息传递给最近的先前的操作符,该操作符随后决定是否以及如何将信息进一步传递到上游。
因此,我们对 Flowable 和 Subscriber 有了一些了解。现在,是时候深入探索它们了。我们将从从头创建一个 Flowable 实例开始。
从头创建 Flowable
在上一章中,我们学习了 Observable.create 方法,但为了使事情更简单,让我们快速回顾一下,然后我们可以继续使用 Flowable.create。看看以下代码片段:
fun main(args: Array<String>) {
val observer: Observer<Int> = object : Observer<Int> {
override fun onComplete() {
println("All Completed")
}
override fun onNext(item: Int) {
println("Next $item")
}
override fun onError(e: Throwable) {
println("Error Occured ${e.message}")
}
override fun onSubscribe(d: Disposable) {
println("New Subscription ")
}
}//Create Observer
val observable: Observable<Int> = Observable.create<Int> {//1
for(i in 1..10) {
it.onNext(i)
}
it.onComplete()
}
observable.subscribe(observer)
}
因此,在这个程序中,我们使用 Observable.create 操作符创建了 Observable。这个操作符允许我们定义自己的自定义 Observable。我们可以为 Observable 编写自己的规则来发射项目。它提供了极大的自由度,但 Observable 的问题也在这里。它不支持背压。如果我们可以创建一个具有背压支持的类似版本,那岂不是很好?我们将这样做,但让我们先看看输出:

因此,正如预期的那样,它打印了从 1 到 10 的所有数字。现在,如前所述,让我们尝试使用 Flowable:
fun main(args: Array<String>) {
val subscriber: Subscriber<Int> = object : Subscriber<Int> {
override fun onComplete() {
println("All Completed")
}
override fun onNext(item: Int) {
println("Next $item")
}
override fun onError(e: Throwable) {
println("Error Occured ${e.message}")
}
override fun onSubscribe(subscription: Subscription) {
println("New Subscription ")
subscription.request(10)
}
}//(1)
val flowable: Flowable<Int> = Flowable.create<Int> ({
for(i in 1..10) {
it.onNext(i)
}
it.onComplete()
},BackpressureStrategy.BUFFER)//(2)
flowable
.observeOn(Schedulers.io())
.subscribe(subscriber)//(3)
runBlocking { delay(10000) }
}
因此,在注释 (1) 中,我们创建了一个 Subscriber 实例。然后,在注释 (2) 中,我们使用 Flowable.create() 方法创建了一个 Flowable 实例,并在注释 (3) 中订阅了它。然而,请注意注释 (2)——除了 lambda 之外,我们还向 Flowable.create 方法传递了另一个参数,即 BackpressureStrategy.BUFFER。那么,这是什么?BackpressureStrategy.BUFFER 有什么作用?让我们检查一下。
Flowable.create()接受两个参数来创建一个Flowable实例。以下是Flowable.create()方法的定义:
fun <T> create(source:FlowableOnSubscribe<T>,
mode:BackpressureStrategy):Flowable<T> {
//...
}
第一个参数是排放生成的源,第二个参数是BackpressureStrategy;它是一个enum,通过缓存/缓冲或丢弃一些排放来帮助支持背压(它基本上帮助选择遵循哪种策略进行背压)。enum BackpressureStrategy有五种底层选项,用于不同类型的背压实现。在这个例子中,BackpressureStrategy.BUFFER将所有排放缓冲在无界缓冲区中,直到下游能够消费它们。显然,这不是一个最优的背压实现,在处理大量排放时可能会导致OutOfMemoryError,但至少它可以防止MissingBackpressureException,并使你的自定义Flowable在一定程度上可行。我们将在本章后面使用Flowable.generate()学习实现背压的更稳健的方法;然而,现在,让我们了解一下可以从BackpressureStrategyenum中选择哪些选项:
-
BackpressureStrategy.MISSING: 这将导致完全没有背压实现;下游必须处理背压溢出。当使用onBackpressureXXX()操作符时,此选项很有帮助。我们将在本章后面学习这个例子。 -
BackpressureStrategy.ERROR: 这同样会导致没有背压实现,并且当下游无法跟上源生时,立即发出MissingBackpressureException。 -
BackpressureStrategy.BUFFER: 这将在无界缓冲区中缓冲所有排放,直到下游能够消费它们。如果有很多排放需要缓冲,这可能会导致OutOfMemoryError。 -
BackpressureStrategy.DROP: 这种策略会在下游忙碌且无法跟上时丢弃所有排放;当下游完成之前的操作后,它将在完成时间后立即获得第一个排放,并错过任何在此之间的排放。例如,假设源生发出五个值,分别是1、2、3、4和5,下游在接收到1后变得忙碌,而源生在发出2、3和4时,下游在源生发出5之前刚好准备好;下游只会接收到5,并错过所有剩余的排放。 -
BackpressureStrategy.LATEST:这个策略将允许你在下游忙碌且无法跟上时丢弃所有排放,但保留最新的一个;当下游完成之前的操作时,它将获得它完成之前的最后一个排放,并且会错过之间的任何排放。例如,假设源依次发射五个值1、2、3、4和5,下游在收到1后变得忙碌,而此时源发射了2、3和4,下游在源发射5之前变得准备好;下游将接收这两个值(如果它在收到4后没有再次变得忙碌,那么它将无法接收5)。
让我们在从 Observables 创建 Flowables 的过程中实现一些这些背压策略作为操作符。
从 Observable 创建 Flowable
Observable.toFlowable() 操作符为你提供了另一种将 BackpressureStrategy 实现到非背压源的方法。这个操作符将任何 Observable 转换为 Flowable,所以让我们动手实践,首先,让我们尝试将一个 Observable 转换为使用缓冲策略的 Flowable,然后我们将在同一个例子中尝试其他一些策略,以更好地理解它。请参考以下代码:
fun main(args: Array<String>) {
val source = Observable.range(1, 1000)//(1)
source.toFlowable(BackpressureStrategy.BUFFER)//(2)
.map { MyItem7(it) }
.observeOn(Schedulers.io())
.subscribe{//(3)
print("Rec. $it;\t")
runBlocking { delay(1000) }
}
runBlocking { delay(100000) }
}
data class MyItem7 (val id:Int) {
init {
print("MyItem init $id")
}
}
因此,在注释 (1) 中,我们使用 Observable.range() 方法创建了一个 Observable。在注释 (2) 中,我们使用 BackpressureStrategy.BUFFER 将其转换为 Flowable。然后,我们使用 lambda 作为 Subscriber 订阅它。让我们看看输出的一部分作为截图(因为完整的输出太长,无法粘贴在这里):

因此,正如预期的那样,下游在这里处理了所有的排放,因为 BackpressureStrategy.BUFFER 会将所有的排放缓冲到下游消费为止。
因此,现在,让我们尝试使用 BackpressureStrategy.ERROR 并查看会发生什么:
fun main(args: Array<String>) {
val source = Observable.range(1, 1000)
source.toFlowable(BackpressureStrategy.ERROR)
.map { MyItem8(it) }
.observeOn(Schedulers.io())
.subscribe{
println(it)
runBlocking { delay(600) }
}
runBlocking { delay(700000) }
}
data class MyItem8 (val id:Int) {
init {
println("MyItem Created $id")
}
}
以下是输出:

它显示了一个错误,因为下游无法跟上上游,正如我们之前所描述的。
如果我们使用 BackpressureStrategy.DROP 选项会发生什么?让我们检查:
fun main(args: Array<String>) {
val source = Observable.range(1, 1000)
source.toFlowable(BackpressureStrategy.DROP)
.map { MyItem9(it) }
.observeOn(Schedulers.computation())
.subscribe{
println(it)
runBlocking { delay(1000) }
}
runBlocking { delay(700000) }
}
data class MyItem9 (val id:Int) {
init {
println("MyItem Created $id")
}
}
一切都和上一个例子一样,只是这里我们使用了 BackpressureStrategy.DROP 选项。让我们检查输出:

因此,正如我们可以在前面的输出中看到的那样,BackpressureStrategy.DROP 在 128 之后停止了 Flowable 的排放,因为下游无法跟上,正如我们之前所描述的。
现在,我们已经对 BackpressureStrategy 中可用的选项有了些了解,让我们专注于 BackpressureStrategy.MISSING 选项,以及如何使用 onBackpressureXXX() 操作符来使用它们。
BackpressureStrategy.MISSING 和 onBackpressureXXX()
BackpressureStrategy.MISSING意味着它不会实现任何背压策略,所以你需要明确告诉Flowable要遵循哪种背压策略。onBackpressureXXX()操作符可以帮助你实现这一点,同时为你提供一些额外的配置选项。
可用的onBackpressureXXX()操作符主要有三种类型:
-
onBackpressureBuffer() -
onBackpressureDrop() -
onBackpressureLatest()
onBackpressureBuffer()操作符
这个操作符的作用类似于BackpressureStrategy.BUFFER;不同之处在于,这里你会得到一些额外的配置选项,例如缓冲区大小、有界或无界等。你也可以省略配置以使用默认行为。
因此,让我们看看一些例子:
fun main(args: Array<String>) {
val source = Observable.range(1, 1000)
source.toFlowable(BackpressureStrategy.MISSING)//(1)
.onBackpressureBuffer()//(2)
.map { MyItem11(it) }
.observeOn(Schedulers.io())
.subscribe{
println(it)
runBlocking { delay(1000) }
}
runBlocking { delay(600000) }
}
data class MyItem11 (val id:Int) {
init {
println("MyItem Created $id")
}
}
再次,我们使用之前的程序,但做了一些小的调整。在注释(1)中,我们使用BackpressureStrategy.MISSING选项创建了Flowable实例。在注释(2)中,为了处理背压,我们使用了onBackpressureBuffer;输出与BackpressureStrategy.BUFFER示例中的输出类似,所以我们省略了这一点。
你可以通过使用onBackpressureBuffer()来指定缓冲区大小。所以让我们将onBackpressureBuffer()方法调用修改为onBackpressureBuffer(20)。以下是输出:

是的,这个更改导致了错误——缓冲区已满。我们定义20为缓冲区大小,但Flowable需要更多的空间。这可以通过实现onError方法来避免。
onBackpressureDrop()操作符
例如,onBackpressureBuffer与BackpressureStrategy.BUFFER匹配,onBackpressureDrop在背压策略方面与BackpressureStrategy.DROP匹配,并提供了一些配置选项。
现在,让我们尝试一下:
fun main(args: Array<String>) {
val source = Observable.range(1, 1000)
source.toFlowable(BackpressureStrategy.MISSING)//(1)
.onBackpressureDrop{ print("Dropped $it;\t") }//(2)
.map { MyItem12(it) }
.observeOn(Schedulers.io())
.subscribe{
print("Rec. $it;\t")
runBlocking { delay(1000) }
}
runBlocking { delay(600000) }
}
data class MyItem12 (val id:Int) {
init {
print("MyItem init $id;\t")
}
}
如前一个程序所示,我们在注释(1)中使用了BackpressureStrategy.MISSING。在注释(2)中,我们使用了onBackpressureDrop()操作符。这个操作符提供了一个配置选项来传递一个消费者实例,该实例将消费丢弃的排放,以便你可以进一步处理它。我们使用了这个配置并传递了一个 lambda,它将打印丢弃的排放,如截图所示:

如输出所示,Flowable在128之后丢弃了排放(因为它有一个用于128排放的内部缓冲区)。onBackpressureDrop的消费者实例在Subscriber实例开始之前就完成了处理。
onBackpressureLatest()操作符
这个操作符的工作方式与BackpressureStrategy.LATEST完全相同——当下游忙碌且无法跟上时,它会丢弃所有排放,只保留最新的一个。当下游完成之前的操作后,它将获得它完成之前的最后一个排放。不幸的是,这并不提供任何配置;你可能不需要它。
让我们看看这个代码示例:
fun main(args: Array<String>) {
val source = Observable.range(1, 1000)
source.toFlowable(BackpressureStrategy.MISSING)//(1)
.onBackpressureLatest()//(2)
.map { MyItem13(it) }
.observeOn(Schedulers.io())
.subscribe{
print("-> $it;\t")
runBlocking { delay(100) }
}
runBlocking { delay(600000) }
}
data class MyItem13 (val id:Int) {
init {
print("init $id;\t")
}
}
这里是输出:

如我们所见,Flowable 在 128 之后丢弃了所有发射,只保留了最后一个(1,000)。
在源头处带有背压的生成 Flowable
到目前为止,我们已经学会了使用处理下游背压的标准库。然而,这是否是最优的?是否总是希望在下游无法跟上时缓存和丢弃发射?这两个问题的答案都是简单的“不”。相反,更好的策略是在源头处进行背压。
Flowable.generate() 具有完全相同的目的。它与 Flowable.create() 有一些相似之处,但有一些区别。让我们看看一个例子,然后我们将尝试理解它是如何工作的,以及 Flowable.create() 和 Flowable.generate() 之间的区别是什么。
注意使用 Flowable.fromIterable(),因为它尊重背压。所以,考虑在可以将源转换为 Iterator 的情况下使用 Flowable.fromIterable()。仅在你需要更具体的东西时使用 Flowable.generate(),因为它要复杂得多。
考虑以下代码:
fun main(args: Array<String>) {
val flowable = Flowable.generate<Int> {
it.onNext(GenerateFlowableItem.item)
}//(1)
flowable
.map { MyItemFlowable(it) }
.observeOn(Schedulers.io())
.subscribe {
runBlocking { delay(100) }
println("Next $it")
}//(2)
runBlocking { delay(700000) }
}
data class MyItemFlowable(val id:Int) {
init {
println("MyItemFlowable Created $id")
}
}
object GenerateFlowableItem {//(3)
var item:Int = 0//(4)
get() {
field+=1
return field//(5)
}
}
在那个程序中,我们使用 Flowable.generate() 方法创建了 Flowable。与 Flowable.create() 不同,在 Flowable.create() 中 Flowable 发射项目,而 Subscriber 接收/等待/缓冲/丢弃它们,Flowable.generate() 在请求时生成项目并发射。Flowable.generate() 接受一个 lambda 作为源,这看起来与 Flowable.create 类似,并且每次请求项目时都会调用它(与 Flowable.create 不同)。所以,例如,如果你在 lambda 中调用 onComplete 方法,Flowable 将只发射一次。此外,你无法在 lambda 中多次调用 onNext。如果你调用了 onError,那么你将在第一次调用时得到错误。
在这个程序中,我们创建了 object,GenerateFlowableItem,使用 var item;每次你访问它时(使用自定义获取器),var item 将自动增加其值。所以,程序应该像 Flowable.range(1, Int.MAX_VALUE) 一样工作,除了当项目达到 Int.MAX_VALUE 时,而不是调用 onComplete,它将再次重复,从 Int.MIN_VALUE 开始。
在输出(此处省略,因为它太大)中,Flowable 首次发射了 128 个项目,然后等待下游处理 96 个项目,然后 Flowable 再次发射了 128 个项目,循环继续。直到你从 Flowable 取消订阅或程序执行停止,它将继续发射项目。
可连接的 Flowable
到目前为止,在本章中,我们处理了 Cold Observables。如果我们想处理热源怎么办?每种类型的 Observable 在 Flowable 中都有对应的类型。在前一章中,我们使用 ConnectableObservable 开始了热源,所以让我们从 ConnectableFlowable 开始。
与Observable类似,ConnectableFlowable类似于普通Flowable,不同之处在于它不是在订阅时开始发出项目,而是在其connect()方法被调用时才发出。这样,你可以在Flowable开始发出项目之前等待所有预期的Subscribers调用Flowable.subscribe()。请参考以下代码:
fun main(args: Array<String>) {
val connectableFlowable = listOf
("String 1","String 2","String 3","String 4",
"String 5").toFlowable()//(1)
.publish()//(2)
connectableFlowable.
subscribe({
println("Subscription 1: $it")
runBlocking { delay(1000) }
println("Subscription 1 delay")
})
connectableFlowable
.subscribe({ println("Subscription 2 $it")})
connectableFlowable.connect()
}
我们对上一章中ConnectableObservable的第一个例子进行了调整。与Observable一样,你可以在Flowable.fromIterable()的位置使用Iterable<T>.toFlowable()扩展函数。Flowable.publish()将普通Flowable转换为ConnectableFlowable。
在这个例子中,在注释(1)处,我们使用Iterable<T>.toFlowable()扩展函数从List创建Flowable,在注释(2)处,我们使用Flowable.publish()运算符从Flowable创建ConnectableFlowable。
以下为输出结果:

由于我们使用了Flowable.fromIterable(Iterable<T>.toFlowable()内部调用Flowable.fromIterable),它尊重源头的背压,因此我们可以看到Flowable等待所有下游完成处理,然后发出下一个项目,这样下游可以以交错的方式工作。
到现在为止,你可能已经在想Subjects了。这是一个很好的工具,但与Observable一样,Subjects也缺乏背压支持。那么,Flowable 中Subjects的对立面是什么?
处理器
处理器是 Flowable 中Subjects的对立面。每种类型的Subject都有一个对应的处理器,并支持背压。
在上一章(第三章,Observables, Observers, and Subjects),我们开始探索Subject,使用PublishSubject;因此,让我们在这里也这样做。让我们从PublishProcessor开始。
以下是一个PublishProcessor的示例:
fun main(args: Array<String>) {
val flowable = listOf("String 1","String 2","String 3",
"String 4","String 5").toFlowable()//(1)
val processor = PublishProcessor.create<String>()//(2)
processor.//(3)
subscribe({
println("Subscription 1: $it")
runBlocking { delay(1000) }
println("Subscription 1 delay")
})
processor//(4)
.subscribe({ println("Subscription 2 $it")})
flowable.subscribe(processor)//(5)
}
因此,在这个例子中,在注释(1)处,我们使用Iterable<T>.toFlowable()方法创建了一个Flowable。在注释(2)处,我们使用PublishProcessor.create()方法创建了一个processor实例。在注释(3)和(4)处,我们订阅了processor实例,在注释(5)处,我们使用processor实例订阅了Flowable。
以下为输出结果:

processor在推送下一个发射之前正在等待所有其Subscribers完成。
学习 Buffer、Throttle 和 Window 运算符
到目前为止,我们已经了解了背压。我们减缓了源头的速度,丢弃了项目,或者使用了缓冲区,它将持有项目直到消费者消费它;然而,这些是否足够?在下游处理背压并不是总是好的解决方案,我们也不能总是减缓源头。
当使用 Observable.interval/Flowable.interval 时,你不能减慢源的速度。一个临时的解决方案可能是某些操作符,它们可以以某种方式允许我们同时处理发射。
有三个操作符可以帮助我们实现这一点:
-
Buffer -
Throttle -
Window
buffer() 操作符
与 onBackPressureBuffer() 操作符不同,后者缓冲发射直到消费者消费,buffer() 操作符将收集发射并将它们作为一批发射,作为列表或其他任何集合类型。
那么,让我们看看这个例子:
fun main(args: Array<String>) {
val flowable = Flowable.range(1,111)//(1)
flowable.buffer(10)//(2)
.subscribe { println(it) }
}
在注释 (1) 上,我们使用 Flowable.range() 方法创建了一个 Flowable 实例,它发射从 1 到 111 的整数。在注释 (2) 上,我们使用了带有缓冲区大小 10 的 buffer 操作符,因此 buffer 操作符从 Flowable 中收集 10 项并将它们作为列表发射。
以下为输出结果,它满足了理解的需求:

buffer 操作符有相当好的配置选项,例如 skip 参数。
它接受一个整数参数作为 skip 计数。它以一种非常有趣的方式工作。如果 skip 参数的值与 count 参数的值完全相同,那么它将不执行任何操作。否则,它将首先计算 count 和 skip 参数之间的正差值作为 actual_numbers_to_skip,然后,如果 skip 参数的值大于 count 参数的值,它将在每个发射的最后一项之后跳过 actual_numbers_to_skip 项。否则,如果 count 参数的值大于 skip 参数的值,你将得到滚动缓冲区,也就是说,不是跳过项,而是跳过之前发射的计数。
感到困惑?让我们看看这个例子来澄清:
fun main(args: Array<String>) {
val flowable = Flowable.range(1,111)
flowable.buffer(10,15)//(1)
.subscribe { println("Subscription 1 $it") }
flowable.buffer(15,7)//(2)
.subscribe { println("Subscription 2 $it") }
}
在注释 (1) 上,我们使用了带有计数 10、跳过 15 的 buffer。在注释 (2) 上,我们将其用作第二次订阅的 count 15、skip 8。以下为输出结果:

对于第一次订阅,它在每次订阅后跳过了 5 项(15-10)。然而,对于第二次订阅,它从每个发射的 8^(th) 项开始重复项(15-7)。
如果前面使用的 buffer 操作符不足以满足你的需求,那么让我告诉你,buffer 操作符还允许你进行基于时间的缓冲。简单来说,它可以收集来自源的发射并在时间间隔内发射。有趣吧?让我们来探索一下:
fun main(args: Array<String>) {
val flowable = Flowable.interval(100, TimeUnit.MILLISECONDS)//(1)
flowable.buffer(1,TimeUnit.SECONDS)//(2)
.subscribe { println(it) }
runBlocking { delay(5, TimeUnit.SECONDS) }//(3)
}
为了更好地理解,我们在本例中使用了 Flowable.interval 来创建一个在注释 (1) 上的 Flowable 实例。在注释 (2) 上,我们使用了 buffer(timespan:Long, unit:TimeUnit) 重载来指示操作符缓冲所有发射并在一秒后将它们作为列表发射。
这是输出结果:

正如你在示例中看到的,每次排放都包含 10 个项目,因为 Flowable.interval() 每 100 毫秒发射一个,而 buffer 在一秒的时间框架内收集排放(1 秒 = 1000 毫秒,100 毫秒间隔的排放会在一秒内产生 10 次排放)。
缓冲操作符的另一个令人兴奋的特性是它可以接受另一个生产者作为边界,也就是说,buffer 操作符将收集源生产者在边界生产者两次排放之间的所有排放,并在每个边界生产者的排放时发出列表。
这里有一个例子:
fun main(args: Array<String>) {
val boundaryFlowable = Flowable.interval(350, TimeUnit.MILLISECONDS)
val flowable = Flowable.interval(100, TimeUnit.MILLISECONDS)//(1)
flowable.buffer(boundaryFlowable)//(2)
.subscribe { println(it) }
runBlocking { delay(5, TimeUnit.SECONDS) }//(3)
}
以下就是输出结果:

buffer 操作符在 boundaryFlowable 发射时发出一个收集到的列表。
window() 操作符
window() 操作符几乎以相同的方式工作,除了它不是在 Collection 对象中缓冲项目,而是在另一个生产者中缓冲项目。
这里有一个例子:
fun main(args: Array<String>) {
val flowable = Flowable.range(1,111)//(1)
flowable.window(10)
.subscribe {
flo->flo.subscribe {//(2)
print("$it, ")
}
println()
}
}
在我们尝试理解它之前,让我们先看看输出结果,如下所示:

window 操作符在一个新的 Flowable 实例中缓冲 10 次排放,我们将在 flowable.subscribe lambda 中再次订阅它,并以逗号作为后缀打印它们。
window 操作符也具有与 buffer 操作符的其他重载相同的函数功能。
throttle() 操作符
buffer() 和 window() 操作符收集排放。throttle 操作符省略排放。我们将在后面的章节中更详细地讨论它,但现在我们先看看:
fun main(args: Array<String>) {
val flowable = Flowable.interval(100, TimeUnit.MILLISECONDS)//(1)
flowable.throttleFirst(200,TimeUnit.MILLISECONDS)//(2)
.subscribe { println(it) }
runBlocking { delay(1,TimeUnit.SECONDS) }
}
这是输出结果:

throttleFirst 跳过了每 200 毫秒内的第一次排放。
此外还有 throttleLast 和 throttleWithTimeout 操作符。
摘要
在本章中,我们学习了背压。我们学习了如何支持背压以及 Flowables 和 processors。我们还学习了如何从消费者和生产者那里支持背压。
尽管我们在处理实时项目时对生产者有了更多的了解,但我们还需要进行异步操作。在下一章中,我们将专注于同样的事情。我们将学习异步数据操作,并且我们将更多地了解我们已经在使用的 map 操作符。
好奇吗?现在就翻到 第五章,异步数据操作符和转换。
第五章:异步数据操作符和转换
通过前面的章节,我们强烈掌握了生产者(Observable 和 Flowable)和消费者(Observer 和 Subscriber)。在学习它们的过程中,我们大量使用了map方法。如前所述,map方法实际上是一个 Rx-Operator。RxKotlin 中也有许多操作符。我可以猜到你从第一次使用map操作符时就有了一个迫切的问题。为什么它看起来像方法,我们却称之为操作符?好吧,在本章中,我们首先将尝试通过定义 RxKotlin 操作符来回答这个问题。然后我们将更深入地了解各种操作符及其实现。借助操作符,我们将高效且轻松地转换、累积、映射、分组和过滤我们的数据。
操作符
当我们第一次开始编程时,我们学习了操作符。我们了解到操作符是那些在操作数上执行特定任务并返回最终结果的特殊字符/字符序列。在响应式世界中,定义保持基本相同;它们接受一个或多个 Observable/Flowable 作为操作数,转换它们,并返回结果 Observable/Flowable。
操作符就像消费者一样作用于前面的 Observable/Flowable,监听它们的发射,转换它们,并将它们发射到下游消费者。例如,考虑map操作符,它监听上游生产者,对其发射执行一些操作,然后将修改后的项目发射到下游。
操作符帮助我们利用和表达业务逻辑和行为。RxKotlin 中有许多操作符可用。在本书中,我们将全面介绍各种类型的操作符,以便你知道何时使用哪个操作符。
记住,为了在应用程序中实现业务逻辑和行为,你应该使用操作符而不是编写阻塞代码或混合命令式编程与响应式编程。通过保持算法和过程纯粹响应式,你可以轻松利用较低的内存使用、灵活的并发性和可处置性,这些在混合响应式编程与命令式编程时可能会减少或无法实现。
这些是五种类型的操作符:
-
过滤/抑制操作符 -
转换操作符
-
减少操作符
-
集合操作符
-
错误处理操作符
-
工具操作符
因此,现在,让我们更仔细地看看它们。
过滤/抑制操作符
想象一下,当你想接收生产者的一些发射,但想丢弃其余的。可能有一些逻辑来决定合格的发射,或者你可能甚至希望批量丢弃。过滤/抑制操作符就是为了在这些情况下帮助你。
这里是一个简短的过滤/抑制操作符列表:
-
debounce -
distinct和distinctUntilChanged -
elementAt -
Filter -
first和last -
ignoreElements -
skip,skipLast,skipUntil, 和skipWhile -
take,takeLast,takeUntil, 和takeWhile
让我们现在更仔细地看看所有这些操作符。
debounce操作符
想象一下,你正在快速接收发射,并且愿意在等待一段时间以确保之后采取最后一个发射。
在开发应用程序的 UI/UX 时,我们经常会遇到这样的情况。例如,你已经创建了一个文本输入框,并希望在用户输入某些内容时执行某些操作,但你不想在每次按键时都执行这个操作。你希望等待用户停止输入(这样你就能得到一个与用户实际想要匹配的好查询),然后将它发送到下游操作员。debounce操作符正是为此目的而设计的。
为了简化,我们在这里不会使用任何平台上的 UI/UX 代码(我们将在学习如何在 Android 中实现 RxKotlin 的后续章节中尝试)。相反,我们将尝试使用Observable.create方法来模拟这种情况(如果您对Observable.create方法有任何疑问,请在阅读本节之前快速翻到第三章,Observables, Observers, and Subjects)。请参考以下代码:
fun main(args: Array<String>) {
createObservable()//(1)
.debounce(200, TimeUnit.MILLISECONDS)//(2)
.subscribe {
println(it)//(3)
}
}
inline fun createObservable():Observable<String> =
Observable.create<String> {
it.onNext("R")//(4)
runBlocking { delay(100) }//(5)
it.onNext("Re")
it.onNext("Reac")
runBlocking { delay(130) }
it.onNext("Reactiv")
runBlocking { delay(140) }
it.onNext("Reactive")
runBlocking { delay(250) }//(6)
it.onNext("Reactive P")
runBlocking { delay(130) }
it.onNext("Reactive Pro")
runBlocking { delay(100) }
it.onNext("Reactive Progra")
runBlocking { delay(100) }
it.onNext("Reactive Programming")
runBlocking { delay(300) }
it.onNext("Reactive Programming in")
runBlocking { delay(100) }
it.onNext("Reactive Programming in Ko")
runBlocking { delay(150) }
it.onNext("Reactive Programming in Kotlin")
runBlocking { delay(250) }
it.onComplete()
}
在这个程序中,我们试图通过将 Observable 的创建导出到另一个函数(createObservable())来保持main函数的简洁,以帮助您更好地理解。在注释(1)中,我们调用了createObservable()函数来创建一个Observable实例。
在createObservable()函数内部,我们试图通过以间隔发射一系列递增的Strings来模拟用户的输入行为,直到达到最终版本(Kotlin 中的响应式编程)。我们在完成每个单词后提供了更大的间隔,以描绘理想用户的行为。
在注释(2)中,我们使用了debounce()操作符,参数为200和TimeUnit.MILLISECONDS,这将使下游在每次发射后等待200毫秒,并且只有在之间没有其他发射发生时才接收发射。
输出如下:

观察者只接收了三个发射,之后 Observable 至少等待了200毫秒才发射下一个。
独特的操作符 - distinct, distinctUntilChanged
这个操作符非常简单;它帮助您从上游过滤掉重复的发射。请看以下示例以更好地理解:
fun main(args: Array<String>) {
listOf(1,2,2,3,4,5,5,5,6,7,8,9,3,10)//(1)
.toObservable()//(2)
.distinct()//(3)
.subscribe { println("Received $it") }//(4)
}
在注释(1)中,我们创建了一个包含许多重复值的Int列表。在注释(2)中,我们使用toObservable()方法从这个列表创建了一个Observable实例。在注释(3)中,我们使用了distinct操作符来过滤掉所有重复的发射。
下面是输出:

distinct操作符的作用是记住所有已经发生的发射,并过滤掉未来的任何此类发射。
distinctUntilChange 操作符略有不同。它不会丢弃所有重复的输出,只会丢弃连续重复的输出,其余的保持原位。请参考以下代码:
fun main(args: Array<String>) {
listOf(1,2,2,3,4,5,5,5,6,7,8,9,3,10)//(1)
.toObservable()//(2)
.distinctUntilChanged()//(3)
.subscribe { println("Received $it") }//(4)
}
这里是输出:

仔细观察输出;项目 3 被打印了两次,第二次在 9 之后。distinct 操作符会记住每个项目直到它收到 onComplete,但 distinctUntilChanged 操作符只会记住它们直到收到新的项目。
elementAt 操作符
使用命令式编程,我们能够访问任何数组/列表的 n^(th) 元素,这是一个相当常见的需求。elementAt 操作符在这方面非常有用;它从生产者那里拉取 n^(th) 元素,并将其作为它自己的唯一输出。
看看以下代码片段:
fun main(args: Array<String>) {
val observable = listOf(10,1,2,5,8,6,9)
.toObservable()
observable.elementAt(5)//(1)
.subscribe { println("Received $it") }
observable.elementAt(50)//(2)
.subscribe { println("Received $it") }
}
在我们继续检查代码之前,先看看以下输出:

在注释 (1) 中,我们从 Observable 中请求了第 5^(th) 个元素,并输出了相同的(计数从零开始)。然而,在注释 (2) 中,我们请求了第 50^(th) 个元素,这在 Observable 中甚至不存在,所以它没有输出任何内容。
这个操作符通过使用稍后将要介绍的 Maybe monad 来实现这种行为。
过滤输出 - 过滤操作符
filter 操作符可以说是最常用的 filtering/suppressing 操作符。它允许你实现自定义逻辑来过滤输出。
以下代码片段是 filter 操作符的最简单实现:
fun main(args: Array<String>) {
Observable.range(1,20)//(1)
.filter{//(2)
it%2==0
}
.subscribe {
println("Received $it")
}
}
在注释 (1) 中,我们使用 Observable.range() 操作符创建了一个 Observable 实例。我们使用注释 (2) 中的 filter 操作符过滤掉了输出中的奇数。
以下是输出:

第一个和最后一个操作符
这些操作符可以帮助你只监听第一个或最后一个输出,并丢弃其余的。
查看以下示例:
fun main(args: Array<String>) {
val observable = Observable.range(1,10)
observable.first(2)//(1)
.subscribeBy { item -> println("Received $item") }
observable.last(2)//(2)
.subscribeBy { item -> println("Received $item") }
Observable.empty<Int>().first(2)//(3)
.subscribeBy { item -> println("Received $item") }
}
输出如下:

在注释 (1) 中,我们使用了 first 操作符,并将 defaultValue 参数设置为 2,这样如果无法访问第一个元素,它将输出 defaultValue 参数。在注释 (2) 中,我们使用了 last 操作符。在注释 (3) 中,我们再次使用了 first 操作符,这次使用了一个空的 Observable;因此,它不会输出第一个元素,而是输出 defaultValue。
忽略元素操作符
有时候,你可能只需要监听生产者的 onComplete。ignoreElements 操作符可以帮助你做到这一点。请参考以下代码:
fun main(args: Array<String>) {
val observable = Observable.range(1,10)
observable
.ignoreElements()
.subscribe { println("Completed") }//(1)
}

ignoreElements 操作符返回一个只有 onComplete 事件的 Completable monad。
我们将在第六章“更多关于操作符和错误处理”中探讨skip和take操作符,同时讨论条件操作符。
转换操作符
如其名所示,transforming操作符可以帮助你转换由生产者发出的项目。
这里是transforming操作符的简要列表:
-
map -
flatMap,concatMap, 和flatMapIterable -
switchMap -
switchIfEmpty -
scan -
groupBy -
startWith -
defaultIfEmpty -
sorted -
buffer -
window -
cast -
delay -
repeat
平铺操作符
map操作符对每个输出的项目执行一个给定的任务(lambda)并将它们输出到下游。我们已经看到了map操作符的一些用法。对于给定的Observable<T>或Flowable<T>,map操作符将通过应用提供的Function<T,R> lambda 将类型为T的输出项转换为类型为R的输出。
因此,现在,让我们通过map操作符来看另一个例子:
fun main(args: Array<String>) {
val observable = listOf(10,9,8,7,6,5,4,3,2,1).toObservable()
observable.map {//(1)
number-> "Transforming Int to String $number"
}.subscribe {
item-> println("Received $item")
}
}
在注释(1)中,我们使用了map操作符,它将输出的类型为Int的项目转换为类型为String的输出。尽管我们清楚地知道输出会是什么样子,但让我们通过查看以下截图来验证这一点:

类型转换输出(cast操作符)
想象一个你想将 Observable 的输出转换为其他数据类型的情况。仅仅为了转换输出而传递一个 lambda 似乎不是一个好主意。cast操作符就是为了在这种情况下提供帮助。让我们看看:
fun main(args: Array<String>) {
val list = listOf<MyItemInherit>(
MyItemInherit(1),
MyItemInherit(2),
MyItemInherit(3),
MyItemInherit(4),
MyItemInherit(5),
MyItemInherit(6),
MyItemInherit(7),
MyItemInherit(8),
MyItemInherit(9),
MyItemInherit(10)
)//(1)
list.toObservable()//(2)
.map { it as MyItem }//(3)
.subscribe {
println(it)
}
println("cast")
list.toObservable()
.cast(MyItem::class.java)//(4)
.subscribe {
println(it)
}
}
open class MyItem(val id:Int) {//(5)
override fun toString(): String {
return "[MyItem $id]"
}
}
class MyItemInherit(id:Int):MyItem(id) {//(6)
override fun toString(): String {
return "[MyItemInherit $id]"
}
}
在这个程序中,我们在注释(5)和(6)分别定义了两个类:MyItem和MyItemInherit。我们将使用这两个类来展示cast操作符的用法。因此,在注释(1)中,我们创建了一个MyItemInherit列表;对于这个程序,我们的方法首先使用map操作符尝试相同的事情,然后我们将使用cast操作符做同样的事情。在注释(2)中,我们使用列表创建了一个可观察对象,然后在注释(3)中,我们使用了map操作符并传递了一个 lambda,其中我们将输出类型转换为MyItemInherit。
我们在注释(4)中做了同样的事情,但这次使用的是cast操作符。现在看看代码的简洁性,它看起来干净得多,简单得多。
平铺操作符
当map操作符对每个输出项执行给定任务(lambda)并将它们输出到下游时,flatMap操作符会创建一个新的生产者,将你传递给每个源生产者输出的函数应用于每个输出。
因此,让我们看看这个例子:
fun main(args: Array<String>) {
val observable = listOf(10,9,8,7,6,5,4,3,2,1).toObservable()
observable.flatMap {
number-> Observable.just("Transforming Int to String $number")
}.subscribe {
item-> println("Received $item")
}
}
这里是输出结果:

输出与上一个类似,但逻辑不同。我们不仅返回String,还返回具有所需String的Observable。尽管在这个例子中,你可能觉得使用它没有好处,但考虑一下你需要从单个发射中推导出多个项目的情况。考虑以下示例,我们将从每个发射中创建多个项目:
fun main(args: Array<String>) {
val observable = listOf(10,9,8,7,6,5,4,3,2,1).toObservable()
observable.flatMap {
number->
Observable.create<String> {//(1)
it.onNext("The Number $number")
it.onNext("number/2 ${number/2}")
it.onNext("number%2 ${number%2}")
it.onComplete()//(2)
}
}.subscribeBy (
onNext = {
item-> println("Received $item")
},
onComplete = {
println("Complete")
}
)
}
让我们看看输出,然后我们尝试理解程序:

在这个程序中,我们在flatMap运算符内部创建了一个新的Observable实例,它将发射三个字符串。在注释(1)中,我们使用Observable.create运算符创建了Observable实例。我们将从Observable.create运算符发射三个字符串,并在注释(2)中,在从Observable发射三个项目后发送一个onComplete通知。
然而,看看输出;它在发送onComplete通知之前发射了所有项目。原因是所有Observable都被组合在一起,然后订阅到下游。flatMap运算符内部使用merge运算符来组合多个Observable。
concatMap使用concat运算符而不是merge运算符来执行相同的操作,以组合两个Observable/Flowables。
我们将在下一章中了解更多关于这些运算符(merge、concat和其他组合运算符)的信息。
我们将再次查看flatMap,以及concatMap、switchMap和flatMapIterable,在第六章,更多关于运算符和错误处理之后,我们将对合并和连接生产者有一些了解。
defaultIfEmpty 运算符
当使用过滤运算符和/或处理复杂需求时,可能会遇到空的生产者(见以下代码块):
fun main(args: Array<String>) {
Observable.range(0,10)//(1)
.filter{it>15}//(2)
.subscribe({
println("Received $it")
})
}
在这里,在注释(1)中,我们将创建范围从0到10的Observable;然而,在注释(2)中,我们将过滤它以发射值>15。所以,基本上,我们将得到一个空的Observable。
defaultIfEmpty运算符帮助我们处理这种情况。带有defaultIfEmpty的前一个例子看起来像这样:
fun main(args: Array<String>) {
Observable.range(0,10)//(1)
.filter{it>15}//(2)
.defaultIfEmpty(15)//(3)
.subscribe({
println("Received $it")
})
}
这是相同的程序,但在注释(3)中,我们添加了defaultIfEmpty运算符。
输出看起来如下截图:

输出显示,尽管Observable不包含任何大于10的数字,但defaultIfEmpty在过滤后为空时向Observable添加了15。
switchIfEmpty 运算符
此运算符类似于defaultIfEmpty运算符;唯一的区别是,对于defaultIfEmpty运算符,它向空的生产者添加一个发射项,但对于switchIfEmpty运算符,如果源生产者是空的,它将从指定的替代生产者开始发射。
与需要传递一个项目的defaultIfEmpty操作符不同,在这里,你必须将一个替代生产者传递给switchIfEmpty操作符。如果源生产者为空,它将从替代生产者开始取排放。
下面是一个例子:
fun main(args: Array<String>) {
Observable.range(0,10)//(1)
.filter{it>15}//(2)
.switchIfEmpty(Observable.range(11,10))//(3)
.subscribe({
println("Received $it")
})
}
这与上一个例子相同;只是在注释(3)中,我们使用了switchIfEmpty而不是defaultIfEmpty与一个替代的Observable。下面的输出显示,排放是从使用switchIfEmpty操作符传递的替代Observable中取出的:

startWith操作符
startWith操作符很简单;它允许你将一个项目添加到生产者的顶部,所有现有的项目之上。
让我们看看它是如何工作的:
fun main(args: Array<String>) {
Observable.range(0,10)//(1)
.startWith(-1)//(2)
.subscribe({
println("Received $it")
})
listOf("C","C++","Java","Kotlin","Scala","Groovy")//(3)
.toObservable()
.startWith("Programming Languages")//(4)
.subscribe({
println("Received $it")
})
}
输出如下:

如我们所见,注释(2)和(4)中的startWith操作符就像现有排放列表的前缀一样工作。
排放排序(sorted 操作符)
有一些场景下,你可能想要对排放进行排序。sorted操作符可以帮助你做到这一点。它将在排序后内部收集并重新排放来自源生产者的所有排放。
让我们看看这个例子,并尝试更好地理解这个操作符:
fun main(args: Array<String>) {
println("default with integer")
listOf(2,6,7,1,3,4,5,8,10,9)
.toObservable()
.sorted()//(1)
.subscribe { println("Received $it") }
println("default with String")
listOf("alpha","gamma","beta","theta")
.toObservable()
.sorted()//(2)
.subscribe { println("Received $it") }
println("custom sortFunction with integer")
listOf(2,6,7,1,3,4,5,8,10,9)
.toObservable()
.sorted { item1, item2 -> if(item1>item2) -1 else 1 }//(3)
.subscribe { println("Received $it") }
println("custom sortFunction with custom class-object")
listOf(MyItem1(2),MyItem1(6),
MyItem1(7),MyItem1(1),MyItem1(3),
MyItem1(4),MyItem1(5),MyItem1(8),
MyItem1(10),MyItem1(9))
.toObservable()
.sorted { item1, item2 ->
if(item1.item<item2.item) -1 else 1 }//(4)
.subscribe { println("Received $it") }
}
data class MyItem1(val item:Int)
首先看看输出,然后我们将探索程序:

现在,让我们来探索这个程序。正如我们已经知道的,sorted操作符有助于排序排放;为了排序,我们需要比较,因此,sorted操作符需要一个Comparable实例来比较排放项并分别排序。这个操作符有两个重载版本,一个没有参数——它假设生产者(这里为Observable)类型将实现Comparable并调用compareTo函数,如果没有实现将生成错误;另一个重载版本是带有比较方法的(lambda)。在注释(1)和(2)中,我们使用默认的sort函数实现了sorted操作符,即它会调用项目实例的compareTo函数,如果数据类型没有实现Comparable将抛出错误。
在注释(3)中,我们使用我们自己的自定义sortFunction以降序对整数进行排序。
在注释(4)中,我们使用了一个类型为MyItem1的Observable,这显然是一个自定义类,没有实现Comparable,因此我们在这里也传递了sortFunction lambda。
注意:正如我们已经提到的,sorted操作符收集所有排放并排序后再以排序顺序重新排放;因此,使用此操作符可能会引起重大的性能影响。此外,在使用大型生产者时,它还可能导致OutOfMemory Error。所以,谨慎使用排序操作符,或者除非有广泛的需求,否则尽量避免使用。
累积数据 – scan 操作符
scan操作符是一个滚动聚合器;它通过将先前排放添加到其中来发出增量累积。
在深入探讨之前,让我们先看看以下例子:
fun main(args: Array<String>) {
Observable.range(1,10)
.scan { previousAccumulation, newEmission ->
previousAccumulation+newEmission }//(1)
.subscribe { println("Received $it") }
listOf("String 1","String 2", "String 3", "String 4")
.toObservable()
.scan{ previousAccumulation, newEmission ->
previousAccumulation+" "+newEmission }//(2)
.subscribe { println("Received $it") }
Observable.range(1,5)
.scan { previousAccumulation, newEmission ->
previousAccumulation*10+newEmission }//(3)
.subscribe { println("Received $it") }
}
输出如下:

因此,在这个程序中,我们使用了scan操作符来实现三种类型的操作,我们将在详细讨论之前,首先尝试理解scan操作符本身。它接受一个带有两个参数的 lambda 表达式。第一个参数是所有先前排放的滚动聚合结果;第二个是当前排放。
以下图表将帮助您更好地理解:

如图表所示,scan操作符将根据提供的累积函数将所有先前排放与当前排放累积起来。
因此,在先前的程序中,在注释(1)处,我们使用了scan操作符,正如图表中描述的那样。我们使用它来获取到目前为止发出的所有整数的总和。在注释(2)处,我们使用它与Observable类型的String一起,得到了连接的字符串。
在注释(3)处,我们使用了scan操作符通过将前一次累积乘以10并加上当前排放来连接整数。
有一个需要注意的事情是,只要返回相同的数据类型项,我们就可以使用scan操作符进行几乎任何操作,而不仅仅是求和。
注意,scan操作符与即将在本章中介绍的reduce操作符有相似之处;然而,请谨慎不要混淆。scan操作符是一个滚动聚合器,它将接收到的所有排放转换为累积;而reduce操作符在接收到onComplete通知后,通过累积所有排放将排放减少到只有一个。
减少操作符
在开发应用程序时,您可能会遇到需要累积和合并排放的情况。请注意,几乎符合这一标准的所有操作符都只会在调用onComplete()的有限生产者(Observable/Flowable)上工作,因为通常我们只能合并有限的数据集。我们将随着对这些操作符的介绍来探索这一行为。
这里有一个简短的减少操作符列表,我们将在本章中介绍:
-
count -
reduce -
all -
any -
contains
计算排放(计数操作符)
count操作符订阅一个生产者,计算排放量,并在生产者发出排放量的计数后发出一个Single。
这里有一个例子:
fun main(args: Array<String>) {
listOf(1,5,9,7,6,4,3,2,4,6,9).toObservable()
.count()
.subscribeBy { println("count $it") }
}
以下为输出:

从输出中我们可以看到,这个操作符计算生产者的排放量,并在接收到onComplete通知后发出计数。
累积排放 - 减法操作符
减少(reduce)是一个完美的累积操作符。它累积生产者的所有排放,并在接收到生产者的onComplete通知后发出它们。
这里有一个例子:
fun main(args: Array<String>) {
Observable.range(1,10)
.reduce { previousAccumulation, newEmission ->
previousAccumulation+newEmission }
.subscribeBy { println("accumulation $it") }
Observable.range(1,5)
.reduce { previousAccumulation, newEmission ->
previousAccumulation*10+newEmission }
.subscribeBy { println("accumulation $it") }
}
输出如下所示:

reduce操作符的工作方式与scan操作符类似,唯一的区别是它不是在每次发射时累积并发射,而是在接收到onComplete通知时累积所有发射并发射。
all和any操作符帮助验证生产者的发射;我们将在下一章中探讨它们。
collection操作符
虽然这不是一个好的实践,但考虑到一些罕见的情况,RxKotlin 为你提供了可以监听所有发射并将它们累积到集合对象的操作符。
collection操作符基本上是减少操作符的一个子集。
以下列表包含最重要的collection操作符:
-
toList和toSortedList -
toMap -
toMultiMap -
collect
我们将在本书的后面部分详细讲解collection操作符。
错误处理操作符
我们已经学习了 Subscriber/Observer 中的onError事件。然而,onError事件的问题在于错误被发射到下游消费者链,并且订阅立即终止。例如,看看以下程序:
fun main(args: Array<String>) {
Observable.just(1,2,3,5,6,7,"Errr",8,9,10)
.map { it.toIntOrError() }
.subscribeBy (
onNext = {
println("Next $it")
},
onError = {
println("Error $it")
}
)
}
程序的输出显示在以下屏幕截图:

当 Observable 发出字符串Errr时,程序在map操作符中抛出异常。异常被onError处理器捕获,但订阅没有获得任何进一步的发射。
这可能不是每次都期望的行为。虽然我们不能假装错误从未发生并继续(我们也不应该这样做),但应该至少有重新订阅或切换到备用源生产者的方法。
错误处理操作符可以帮助你实现同样的效果。
以下是一些错误处理操作符。
-
onErrorResumeNext( ) -
onErrorReturn( ) -
onExceptionResumeNext( ) -
retry( ) -
retryWhen( )
我们将在第六章详细讲解错误处理操作符,更多关于操作符和错误处理。
工具操作符
这些操作符帮助我们执行各种工具操作,例如对发射执行某些操作,记住每个发射项的时间戳,缓存等等。
以下列出了工具操作符:
-
doOnNext、doOnComplete和doOnError -
doOnSubscribe、doOnDispose和doOnSuccess -
serialize -
cache
我们将在下一章中详细讲解工具操作符。
概述
在本章中,我们学习了操作符和可用的操作符类型,我们详细学习了操作符,特别是那些用于转换、过滤和累积源生产者发射的操作符。我们还学习了错误处理操作符的必要性,我们将在下一章中介绍。
本章和下一章,即第六章“更多关于操作符和错误处理”,密切相关;在讨论本章主题时,我们对下一章的内容有了大致的了解。在下一章中,我们也将参考并使用本章学到的内容。
在本章中,我们专注于操作符、操作符类型以及特别适用于过滤、转换和累积排放(即数据)的操作符(也称为数据)的基础知识,而在下一章中,我们将介绍用于组合可观察的/流动的、错误处理以及条件目的的操作符。
立即翻到下一页开始吧。
第六章:更多关于操作符和错误处理
在上一章中,我们学习了操作符及其使用方法。我们学习了操作符如何帮助我们轻松解决复杂问题。我们掌握了操作符及其类型,并详细学习了基本过滤操作符和转换操作符。现在是时候继续学习一些有趣且高级的操作符用法了。
本章我们将涵盖以下主题:
-
组合生产者(Observable/Flowable)
-
分组发射
-
过滤/抑制操作符
-
错误处理操作符
-
实际的 HTTP 客户端示例
那么,我们还在等什么?让我们开始结合生产者(Observable/Flowable)实例。
组合生产者(Observable/Flowable)
在开发应用程序时,在开始使用之前结合多个来源的数据是一种常见的情况。其中一种情况是当你遵循离线优先的方法构建某些离线应用程序时,你希望将来自 HTTP 调用的结果数据与来自本地数据库的数据结合。
现在不再浪费时间,让我们看看可以帮助我们组合生产者的操作符:
-
startWith() -
merge(),mergeDelayError() -
concat() -
zip() -
combineLatest()
基本上,有一些机制可以组合生产者(Observables/Flowables)。它们如下:
-
合并生产者
-
连接生产者
-
生产者组合的歧义
-
压缩
-
组合最新
本章我们将讨论之前提到的所有组合生产者的技术。然而,让我们从一个我们已经熟悉的操作符开始。
startWith 操作符
在上一章中,我们介绍了 startWith 操作符,但还有很多内容要介绍。这个操作符还允许你组合多个生产者。看看下面的例子:
fun main(args: Array<String>) {
println("startWith Iterator")
Observable.range(5,10)
.startWith(listOf(1,2,3,4))//(1)
.subscribe {
println("Received $it")
}
println("startWith another source Producer")
Observable.range(5,10)
.startWith(Observable.just(1,2,3,4))//(2)
.subscribe {
println("Received $it")
}
}
我们可以将另一个源 Observable 或 Iterator 实例传递给 startWith 操作符,在它所订阅的源 Observable 开始发射之前添加。
在前面的程序中,在注释 (1) 处,我们使用了 startWith 操作符并将其传递了一个 Iterator 实例。startWith 操作符内部将传递的 Iterator 实例转换为 Observable 实例(如果你使用 Flowable,它将转换为 Flowable 实例)。以下是 startWith 操作符的签名:
fun startWith(items: Iterable<T>): Observable<T> {
return concatArray<T>(fromIterable<out T>(items), this)
}
从 startWith 操作符的前一个签名中,我们也可以看到它内部使用了 concatArray,我们将在本章中很快介绍这一点。
在注释 (2) 处,我们使用了 startWith 操作符并传递了另一个源 Observable。
下面是程序的输出:

由于我们已经对 startWith 操作符有了基本的了解,现在让我们继续学习 zip 操作符。zip 操作符实现了一种压缩机制来组合生产者。
压缩发射 – zip 操作符
zip操作符非常有趣。想象一下你正在处理多个Observable/Flowables,并想在每个生产者的每个后续排放上执行某种操作。zip操作符允许你正好执行这一点。它通过指定的函数累积多个生产者的排放以创建一个新的排放。所以,让我们通过一个图示来深入了解:

如图片所示,zip操作符将多个生产者的排放累积到一个单一的排放中。它还接受一个函数来应用于排放,就像scan或reduce操作符一样,但它将这些函数应用于来自不同生产者的排放。
为了简化起见,我们在前面的图片和下面的例子中使用了两个Observable,但zip操作符可以与多达九个Observables/Flowables一起工作。
考虑以下代码:
fun main(args: Array<String>) {
val observable1 = Observable.range(1,10)
val observable2 = Observable.range(11,10)
Observable.zip(observable1,observable2,
io.reactivex.functions.BiFunction
<Int, Int, Int> { emissionO1, emissionO2 ->
emissionO1+emissionO2
}).subscribe {
println("Received $it")
}
}
zip操作符定义在Observable类的companion object(Java 中的static方法)中,因此可以直接通过写入Observable.zip本身来访问。不需要通过另一个实例来访问。所以,在我们继续之前,让我们看看输出:

为了更好地理解和使用zip操作符,你需要记住以下关于它的要点:
-
zip操作符作用于提供的生产者的每个排放。例如,如果你将三个生产者x、y和z传递给zip操作符,它将累积x的第n个排放与y和z的第n个排放。 -
zip操作符在应用函数之前等待每个生产者发射。例如,如果你在zip操作符中使用Observable.interval作为生产者之一,zip操作符将等待每个排放,并将累积的值在指定的间隔内发射。 -
如果任何生产者在未发射它所等待的项目的情况下通知
onComplete或onError,那么它将丢弃之后的所有排放,包括来自其他生产者的那个特定排放。例如,如果生产者x发射10个项目,生产者y发射11个项目,生产者z发射8个项目,zip操作符将累积来自所有生产者的前8个排放,并将丢弃来自生产者x和y的所有剩余排放。
zipWith操作符
zip操作符的实例版本(即函数的副本,应该使用实例而不是静态调用)是zipWith,它可以从Observable实例本身调用。这个版本唯一的问题是只能传递另一个源Observable。如果你需要与三个或更多的Observable实例一起工作,你最好考虑使用zip操作符而不是zipWith。
这里有一个例子:
fun main(args: Array<String>) {
val observable1 = Observable.range(1,10)
val observable2 = listOf("String 1","String 2","String 3",
"String 4","String 5","String 6","String 7","String 8",
"String 9","String 10").toObservable()
observable1.zipWith(observable2,{e1:Int,e2:String ->
"$e2 $e1"})//(1)
.subscribe {
println("Received $it")
}
}
输出如下:

在注释(1)中,我们在Observable实例observable1上使用了zipWith操作符,并将另一个Observable实例observable2传递给它,使用 lambda 表达式来应用发射。从输出中,我们可以看出zipWith操作符累积了它订阅的生产者以及它提供给生产者的生产者。
combineLatest 操作符
combineLatest操作符的工作方式与zip操作符类似。它累积提供的生产者的发射。combineLatest和zip之间的唯一区别是,zip操作符等待其每个源生产者发射,然后才开始处理所有发射以创建其新的发射,但combineLatest操作符在接收到其源生产者中的任何一个发射后立即开始。
为了更好地理解这个操作符,我们将通过zip和combineLatest操作符的例子来展示。让我们首先尝试使用zip操作符的例子,因为我们已经对其有所了解:
fun main(args: Array<String>) {
val observable1 =
Observable.interval(100,TimeUnit.MILLISECONDS)//(1)
val observable2 =
Observable.interval(250,TimeUnit.MILLISECONDS)//(2)
Observable.zip(observable1,observable2,
BiFunction { t1:Long, t2:Long -> "t1: $t1, t2: $t2" })//(3)
.subscribe{
println("Received $it")
}
runBlocking { delay(1100) }
}
输出如下。正如预期的那样,它累积了每一个发射并打印出来:

在这个程序中,我们在注释(1)中创建了一个具有 100 毫秒间隔的Observable。在注释(2)中,我们创建了一个具有250毫秒间隔的另一个Observable。在输出中,我们可以看到有3个发射,因为,在压缩后,总间隔变为350毫秒,在 1,100 毫秒的延迟内,只有3个发射,它们之间有350毫秒的间隔。
现在,让我们用combineLatest测试相同的代码:
fun main(args: Array<String>) {
val observable1 = Observable.interval(100, TimeUnit.MILLISECONDS)
val observable2 = Observable.interval(250, TimeUnit.MILLISECONDS)
Observable.combineLatest(observable1,observable2,
BiFunction { t1:Long, t2:Long -> "t1: $t1, t2: $t2" })
.subscribe{
println("Received $it")
}
runBlocking { delay(1100) }
}
下面是输出:

如输出所示,combineLatest操作符在接收到其源生产者中的任何一个发射后立即处理并发射值,对于所有其他源生产者使用最后一个发射的值。
现在,让我们在merge操作符的帮助下继续合并生产者。
合并 Observables/Flowables – merge 操作符
压缩操作将允许你累积发射,但如果你想要订阅所有源生产者的每一个发射呢?比如说,你有两个不同的生产者,并且当订阅它们时应用相同的动作集;没有方法可以混合命令式编程和响应式编程,并使用相同的代码分别单独订阅两个生产者。这也会导致代码冗余。那么,这里的解决方案是什么?你答对了;将所有源生产者的发射合并在一起,并作为一个整体来订阅就是解决方案。
因此,让我们在这里举一个例子:
fun main(args: Array<String>) {
val observable1 = listOf("Kotlin", "Scala",
"Groovy").toObservable()
val observable2 = listOf("Python", "Java", "C++",
"C").toObservable()
Observable
.merge(observable1,observable2)//(1)
.subscribe {
println("Received $it")
}
}
在这个程序中,在注释(1)中,我们将合并两个observable并作为一个整体来订阅它们。输出如下:

如输出所示,merge操作符合并了两个Observables,并将两个Observables的发射按照发射顺序放置。
然而,合并操作并不保持指定的顺序;相反,它将立即开始监听所有提供的生产者,并在它们从源处发出时立即触发排放。让我们看看一个说明这一点的例子:
fun main(args: Array<String>) {
val observable1 = Observable.interval(500,
TimeUnit.MILLISECONDS).map { "Observable 1 $it" }//(1)
val observable2 = Observable.interval(100,
TimeUnit.MILLISECONDS).map { "Observable 2 $it" }//(2)
Observable
.merge(observable1,observable2)
.subscribe {
println("Received $it")
}
runBlocking { delay(1500) }
}
在前面的例子中,在注释(1)和(2)中,我们使用Observable.interval操作符创建了两个Observable<Long>实例,然后使用Observable编号进行映射,得到了Observable<String>的实例。这里map操作符的目标是在输出中注入一个Observable标识,这样我们就可以轻松地从合并输出中识别Observable的来源。
所以,这里就是大家讨论了很多的输出:

输出清楚地显示,merge操作符首先从observable2中获取排放,因为它们先到达,尽管我们在merge操作符中首先放置了observable1。
然而,merge操作符支持多达四个参数。作为一个后备方案,我们有mergeArray操作符,它接受vararg的Observable;以下是一个例子:
fun main(args: Array<String>) {
val observable1 = listOf("A", "B", "C").toObservable()
val observable2 = listOf("D", "E", "F", "G").toObservable()
val observable3 = listOf("I", "J", "K", "L").toObservable()
val observable4 = listOf("M", "N", "O", "P").toObservable()
val observable5 = listOf("Q", "R", "S", "T").toObservable()
val observable6 = listOf("U", "V", "W", "X").toObservable()
val observable7 = listOf("Y", "Z").toObservable()
Observable.mergeArray(observable1, observable2, observable3,
observable4, observable5, observable6, observable7)
.subscribe {
println("Received $it")
}
}
输出如下:

与zip操作符一样,merge操作符也有一个用于调用Observable实例的版本,而不是静态的mergeWith;我们可以在Observable实例上调用此操作符。所以,让我们看看一个例子:
fun main(args: Array<String>) {
val observable1 = listOf("Kotlin", "Scala",
"Groovy").toObservable()
val observable2 = listOf("Python", "Java", "C++",
"C").toObservable()
observable1
.mergeWith(observable2)
.subscribe {
println("Received $it")
}
}
程序很简单。我们创建了两个Observable实例,然后使用在observable1实例上调用的mergeWith操作符将observable1与observable2合并。
输出如下:

合并的字面意思是将两件事物结合起来创造一个新的事物,不考虑任何顺序;所有合并操作符都做同样的事情。如果你想保持顺序,你必须一个接一个地连接。
连接生产者(Observable/Flowable)
连接操作符几乎与merge操作符相同,除了连接操作符尊重规定的顺序。它不会一次性订阅所有提供的生产者,而是逐个订阅生产者;只有当它从前一个订阅中收到onComplete后,才会这样做。
因此,让我们使用concatenate操作符修改我们最后的程序,看看变化:
fun main(args: Array<String>) {
val observable1 = Observable.interval(500, TimeUnit.MILLISECONDS)
.take(2)//(1)
.map { "Observable 1 $it" }//(2)
val observable2 = Observable.interval(100,
TimeUnit.MILLISECONDS).map { "Observable 2 $it" }//(3)
Observable
.concat(observable1,observable2)
.subscribe {
println("Received $it")
}
runBlocking { delay(1500) }
}
正如我们之前提到的,concat操作符只有在从当前源Observable收到onComplete后才会订阅队列中的下一个源Observable;我们也知道使用Observable.interval创建的Observable实例永远不会发出onComplete。相反,它们会一直发出数字,直到达到Long.MAX_VALUE。因此,作为一个快速修复,我们在注释(1)上使用了take操作符,它将从Observable.interval中取出前两个排放,然后向其中添加一个onComplete通知,这样concat操作符也可以开始监听下一个源Observable。
我们在本章的 跳过和获取发射 部分讨论了 take 操作符。别忘了看看。
因此,以下是输出结果:

从输出中,我们可以清楚地看到,concat 操作符仅在从其第一个 Observable 收到 onComplete 通知后,才订阅下一个提供的源 Observable。
就像 merge 操作符一样,concat 操作符也有 concatArray 和 concatWith 变体,它们几乎以相同的方式工作,只是连接而不是合并。
模糊组合生产者
生产者的模糊组合可能是所有组合类型中最简单的一种。想象一下,你正在从两个数据源(可能是两个不同的 API 或数据库表)获取数据,并希望使用你得到的第一份数据并丢弃其他数据。在命令式编程技术中,你可能需要编写检查来处理这种情况;然而,使用 RxKotlin,amb 操作符就在那里为你提供支持。
amb 操作符接受一个 Observable 列表(Iterable<Observable> 实例)作为参数,订阅 Iterable 实例中存在的所有 Observable,从它接收到的第一个 Observable 发射的项目,并丢弃 Iterable 实例上存在的其余 Observable。
以下示例将帮助我们更好地理解:
fun main(args: Array<String>) {
val observable1 = Observable.interval(500,
TimeUnit.MILLISECONDS).map { "Observable 1 $it" }//(1)
val observable2 = Observable.interval(100,
TimeUnit.MILLISECONDS).map { "Observable 2 $it" }//(2)
Observable
.amb(listOf(observable1,observable2))//(3)
.subscribe {
println("Received $it")
}
runBlocking { delay(1500) }
}
因此,在这个程序中,我们在注释 (1) 和 (2) 分别创建了两个具有 500 毫秒和 100 毫秒间隔的 Observable。在注释 (3) 中,我们使用了 listOf 函数从这两个 Observable 创建了一个 List<Observable>,并将其传递给了 amb 操作符。以下是输出结果:

从输出中我们可以看到,amb 操作符从 observable2 中获取了发射项,并不关心 observable1,因为 observable2 实例先发射了。
就像其他组合操作符一样,amb 也有 ambArray 和 ambWith 操作符变体。
分组
分组是使用 RxKotlin 可以实现的一种强大操作。这个操作允许你根据它们的属性对发射项进行分组。比如说,你有一个发射整数数字(Int)的 Observable / Flowable,根据你的业务逻辑,你为偶数和奇数有一些单独的代码,并希望分别处理它们。在这种情况下,分组是最好的解决方案。
让我们举一个例子:
fun main(args: Array<String>) {
val observable = Observable.range(1,30)
observable.groupBy {//(1)
it%5
}.blockingSubscribe {//(2)
println("Key ${it.key} ")
it.subscribe {//(3)
println("Received $it")
}
}
}
在这个例子中,我根据它们除以 5 的余数对发射项进行了分组,所以,基本上应该有 5 组(从 0 到 4)。在这个例子的注释 (1) 中,我们使用了 groupBy 操作符并将其传递给一个谓词,根据这个谓词进行分组。groupBy 操作符根据谓词的结果对发射项进行分组。
在此示例的注释(2)中,我们使用了blockingSubscribe操作符来订阅新创建的Observable<GroupedObservable<K, T>>实例。我们也可以使用简单的subscribe操作符;然而,由于我们将输出打印到控制台,使用subscribe会使一切看起来都很混乱。主要是因为subscribe操作符在接收下一个排放之前不会等待给定的任务完成。另一方面,blockingSubscribe将使程序等待直到它完成处理一个排放,然后才会进行下一个。
groupBy操作符返回包含我们的组的Observable,因此,在blockingSubscribe内部,我们需要订阅发出的GroupedObservable实例。在注释(3)中,我们做了同样的事情,在打印发出GroupedObservable实例的key之后。
输出如下:

flatMap,concatMap – 详细说明
如前一章所承诺的,现在我们将更深入地探讨flatMap和concatMap操作符,因为我们已经对merge和concat操作符有了某种程度的了解,并且知道它们之间的区别。
让我们从flatMap和concatMap之间的区别开始,之后,我们还将讨论它们的理想实现场景。我们还将讨论它们的一些变体,以便更好地了解它们。
在前一章中,我们提到flatMap内部使用merge操作符,而concatMap内部使用concat操作符。然而,这有什么区别呢?你刚刚学习了merge和concat操作符之间的区别,但是基于它们有两个独立的映射操作符有什么意义呢?所以,让我们从一个例子开始。我们将看到一个使用flatMap的例子,然后我们将尝试使用concatMap实现相同的操作:
fun main(args: Array<String>) {
Observable.range(1,10)
.flatMap {
val randDelay = Random().nextInt(10)
return@flatMap Observable.just(it)
.delay(randDelay.toLong(),TimeUnit.MILLISECONDS)//(1)
}
.blockingSubscribe {
println("Received $it")
}
}
在前面的程序中,我们创建了一个Observable实例。然后,我们在它上面使用flatMap操作符和delay操作符来为排放添加随机延迟。
输出如下:

从输出中,我们可以看到下游没有按照规定的顺序接收排放;我想你已经找到了原因,不是吗?没错;背后的原因是简单的merge操作符,因为merge操作符异步地一次性订阅和重新发射排放,因此顺序没有得到保持。
现在,让我们使用concatMap操作符来实现代码:
fun main(args: Array<String>) {
Observable.range(1,10)
.concatMap {
val randDelay = Random().nextInt(10)
return@concatMap Observable.just(it)
.delay(randDelay.toLong(), TimeUnit.MILLISECONDS)//(1)
}
.blockingSubscribe {
println("Received $it")
}
}
输出如下:

由于concatMap操作符内部使用concat,它保持了规定的排放顺序。
那么,何时使用哪个操作符呢?让我们看看以下实时场景;所有这些场景都适用,尤其是在你构建应用程序时。
何时使用flatMap操作符
看看以下列表——它包含了flatMap最适合的上下文和情况:
-
当你在页面、活动或片段中处理数据列表,并希望为列表中的每一项发送数据到服务器或数据库时。
concatMap操作符在这里也会这样做;然而,由于flatMap操作符是异步的,它将更快,而且,由于你正在发送数据,顺序实际上并不重要。 -
当你想要在相对较短的时间内异步地对列表项执行任何操作时。
何时使用concatMap操作符
那么,何时使用concatMap?
以下列表包含concatMap最适合的上下文和情况:
-
当你正在下载要显示给用户的列表数据时。这里的顺序真的很重要,你当然不希望在不显示第三和第四项之后加载并显示列表的第二项,对吧?
-
在有序列表上执行一些操作,确保列表保持不变。
理解switchMap操作符
switchMap操作符非常有趣。它异步监听源生产者(Observable/Flowable)的所有发射,但只发射时间段内的最新项。让我们进一步解释一下。
当源Observable在switchMap发射任何项之前连续发射多个项时,switchMap将只取最后一个,并丢弃任何介于其间的发射。让我们通过一个例子来更好地理解它:
fun main(args: Array<String>) {
println("Without delay")
Observable.range(1,10)
.switchMap {
val randDelay = Random().nextInt(10)
return@switchMap Observable.just(it)//(1)
}
.blockingSubscribe {
println("Received $it")
}
println("With delay")
Observable.range(1,10)
.switchMap {
val randDelay = Random().nextInt(10)
return@switchMap Observable.just(it)
.delay(randDelay.toLong(), TimeUnit.MILLISECONDS)//(2)
}
.blockingSubscribe {
println("Received $it")
}
}
输出如下:

在程序中,我们最初采取了两种方法,我们使用了delay操作符,然后我们用delay操作符重用了同样的方法。从输出中我们可以看到,对于第二种情况,switchMap只发射了最后一个项,因为它在重新发射之前收到了连续的发射。然而,对于第一种情况,它在收到任何进一步的发射之前重新发射了所有项。
仍然感到困惑?让我们进一步修改程序:
fun main(args: Array<String>) {
Observable.range(1,10)
.switchMap {
val randDelay = Random().nextInt(10)
if(it%3 == 0)
Observable.just(it)
else
Observable.just(it)
.delay(randDelay.toLong(), TimeUnit.MILLISECONDS)
}
.blockingSubscribe {
println("Received $it")
}
}
在这个程序中,我们不是对所有发射添加延迟,而是立即发射所有能被3整除的数字,并对其余的添加延迟。
输出如下:

如预期,switchMap操作符只发射了那些在源操作符无延迟发射的项,以及源操作符最后发射的项。原因很简单;switchMap操作符在接收到下一个项之前就已经能够发射它们。
跳过和取发射
就像本章前面使用take操作符的情况一样,通常有一些场景,你希望取一些发射并跳过其余的。skip和take操作符在这些场景中非常有帮助。它们实际上是我们在上一章讨论的过滤操作符的一部分;然而,坦白说,它们确实值得专门的讨论。所以,这就是。
跳过发射(skip、skipLast、skipUntil 和 skipWhile)
可能会有这样的需求,你希望跳过开始的一些发射,或者跳过发射直到满足特定条件。你可能甚至需要等待另一个生产者发射后再获取发射并跳过所有剩余的发射。
这些操作符是根据具体场景设计的。它们以各种方式帮助你跳过发射。
RxKotlin 为我们提供了skip操作符的许多变体和重载;我们将讨论其中最重要的几个:
-
skip -
skipLast -
skipWhile -
skipUntil
我们将逐一查看所有列出的前述操作符。
让我们从skip开始:
fun main(args: Array<String>) {
val observable1 = Observable.range(1,20)
observable1
.skip(5)//(1)
.subscribe(object:Observer<Int> {
override fun onError(e: Throwable) {
println("Error $e")
}
override fun onComplete() {
println("Complete")
}
override fun onNext(t: Int) {
println("Received $t")
}
override fun onSubscribe(d: Disposable) {
println("starting skip(count)")
}
})
val observable2 = Observable.interval(100,TimeUnit.MILLISECONDS)
observable2
.skip(400,TimeUnit.MILLISECONDS)//(2)
.subscribe(
object:Observer<Long> {
override fun onError(e: Throwable) {
println("Error $e")
}
override fun onComplete() {
println("Complete")
}
override fun onNext(t: Long) {
println("Received $t")
}
override fun onSubscribe(d: Disposable) {
println("starting skip(time)")
}
}
)
runBlocking {
delay(1000)
}
}
skip操作符有两个重要的重载:skip(count:Long)和skip(time:Long, unit:TimeUnit);第一个重载基于计数,丢弃前n个发射,而第二个重载基于时间,丢弃在指定时间间隔内到达的所有发射。
在这个程序中,在注释(1)处,我们使用了skip(count)操作符来跳过前5次发射。在注释(2)处,我们使用了skip(time,unit)操作符来跳过订阅前400毫秒(4 秒)内的所有发射。
下面是输出:

现在,让我们看看skipLast操作符是如何工作的:
fun main(args: Array<String>) {
val observable = Observable.range(1,20)
observable
.skipLast(5)//(1)
.subscribe(object: Observer<Int> {
override fun onError(e: Throwable) {
println("Error $e")
}
override fun onComplete() {
println("Complete")
}
override fun onNext(t: Int) {
println("Received $t")
}
override fun onSubscribe(d: Disposable) {
println("starting skipLast(count)")
}
})
}
skipLast操作符有多个重载,就像skip操作符一样。唯一的区别是,这个操作符会丢弃最后的发射。在这个程序中,我们在注释(1)处使用了skipLast(count)操作符来跳过最后的5次发射。
下面是输出:

与skip和skipLast不同,这两个操作符都是基于计数或时间来跳过发射,而skipWhile是基于谓词(逻辑表达式)来跳过发射。你需要向skipWhile操作符传递一个谓词,就像filter操作符一样。它会在谓词评估为 true 时继续跳过发射。一旦谓词返回 false,它将开始将所有发射传递到下游。让我们看看以下代码片段:
fun main(args: Array<String>) {
val observable = Observable.range(1,20)
observable
.skipWhile {item->item<10}//(1)
.subscribe(object: Observer<Int> {
override fun onError(e: Throwable) {
println("Error $e")
}
override fun onComplete() {
println("Complete")
}
override fun onNext(t: Int) {
println("Received $t")
}
override fun onSubscribe(d: Disposable) {
println("starting skipWhile")
}
})
}
输出如下:

注意,与 filter 不同,skipWhile操作符会执行谓词直到它返回 false,并传递所有后续的发射。如果你想检查所有发射的谓词,你更应该考虑filter操作符。
想象一个场景,你正在处理两个生产者,producer1 和 producer2,并且希望在 producer2 开始发射时立即开始处理 producer1 的发射。在这种情况下,skipUntil可以帮助你。让我们看看这个例子:
fun main(args: Array<String>) {
val observable1 = Observable.interval(100, TimeUnit.MILLISECONDS)
val observable2 =
Observable.timer(500,TimeUnit.MILLISECONDS)//(1)
observable1
.skipUntil(observable2)//(2)
.subscribe(
object: Observer<Long> {
override fun onError(e: Throwable) {
println("Error $e")
}
override fun onComplete() {
println("Complete")
}
override fun onNext(t: Long) {
println("Received $t")
}
override fun onSubscribe(d: Disposable) {
println("starting skip(time)")
}
}
)
runBlocking { delay(1500) }
}
我们将解释代码,但首先看看输出:

在注释(1)中,我们使用Observable.timer创建了一个Observable实例(observable2),它应该在500毫秒后触发发射。在注释(2)中,我们使用那个Observable实例(observable2)作为skipUntil操作符的参数,这将使它丢弃observable1的所有发射,直到observable2发射。
取操作符(take、takeLast、takeWhile 和 takeUntil)
take操作符与skip操作符正好相反。让我们逐个举例说明,了解它们是如何工作的:
fun main(args: Array<String>) {
val observable1 = Observable.range(1,20)
observable1
.take(5)//(1)
.subscribe(object:Observer<Int> {
override fun onError(e: Throwable) {
println("Error $e")
}
override fun onComplete() {
println("Complete")
}
override fun onNext(t: Int) {
println("Received $t")
}
override fun onSubscribe(d: Disposable) {
println("starting skip(count)")
}
})
val observable2 = Observable.interval(100,TimeUnit.MILLISECONDS)
observable2
.take(400,TimeUnit.MILLISECONDS)//(2)
.subscribe(
object:Observer<Long> {
override fun onError(e: Throwable) {
println("Error $e")
}
override fun onComplete() {
println("Complete")
}
override fun onNext(t: Long) {
println("Received $t")
}
override fun onSubscribe(d: Disposable) {
println("starting skip(time)")
}
}
)
runBlocking {
delay(1000)
}
}
这个程序几乎和带有skip的程序一样。区别在于这里,我们使用了take而不是skip。让我们检查一下差异,以便更好地理解:

输出清楚地显示了这一点。与skip操作符正好相反,take操作符将指定的发射传递给下游,并丢弃剩余的。最重要的是,它还在其完成传递所有指定的发射后,向下游发送onComplete通知。
让我们用takeLast操作符来测试一下:
fun main(args: Array<String>) {
val observable = Observable.range(1,20)
observable
.takeLast(5)//(1)
.subscribe(object: Observer<Int> {
override fun onError(e: Throwable) {
println("Error $e")
}
override fun onComplete() {
println("Complete")
}
override fun onNext(t: Int) {
println("Received $t")
}
override fun onSubscribe(d: Disposable) {
println("starting skipLast(count)")
}
})
}
然后,这里是输出;它打印了排放中的最后5个数字:

现在看看takeWhile:
fun main(args: Array<String>) {
val observable = Observable.range(1,20)
observable
.takeWhile{item->item<10}//(1)
.subscribe(object: Observer<Int> {
override fun onError(e: Throwable) {
println("Error $e")
}
override fun onComplete() {
println("Complete")
}
override fun onNext(t: Int) {
println("Received $t")
}
override fun onSubscribe(d: Disposable) {
println("starting skipWhile")
}
})
}
输出与skipWhile正好相反;它不是跳过前10个数字,而是打印它们并丢弃剩余的:

错误处理操作符
在开发应用程序时,可能会发生错误。我们必须正确处理这些错误,以确保我们的应用程序在用户端无缝运行。以下程序为例:
fun main(args: Array<String>) {
Observable.just(1,2,3,4,5)
.map { it/(3-it) }
.subscribe {
println("Received $it")
}
}
这里是输出:

如预期,程序抛出了错误,如果这种情况发生在用户端,那将是一件坏事。所以,让我们看看我们如何以响应式的方式处理错误。RxKotlin 为我们提供了一些错误处理操作符,我们将查看它们。我们将使用之前的程序并将各种错误处理操作符应用到它们上,以便更好地理解它们。
onErrorReturn – 在错误发生时返回默认值
onErrorReturn为你提供了一个技术,以便在上游发生错误时指定一个默认值返回给下游。看看下面的代码片段:
fun main(args: Array<String>) {
Observable.just(1,2,3,4,5)
.map { it/(3-it) }
.onErrorReturn { -1 }//(1)
.subscribe {
println("Received $it")
}
}
我们使用onErrorReturn操作符在发生错误时返回-1。输出如下:

如输出所示,onErrorReturn操作符返回指定的默认值。由于上游在发生错误后停止发射项目,下游没有接收到任何其他项目。
如我们之前提到的,onError和onComplete都是终端操作符,所以一旦它接收了它们中的任何一个,下游就停止监听上游。
onErrorResumeNext 操作符
onErrorResumeNext 操作符可以帮助你在发生任何错误时订阅不同的生产者。
这里是一个例子:
fun main(args: Array<String>) {
Observable.just(1,2,3,4,5)
.map { it/(3-it) }
.onErrorResumeNext(Observable.range(10,5))//(1)
.subscribe {
println("Received $it")
}
}
输出如下:

这个操作符在你想在发生任何错误时订阅另一个源生产者时特别有用。
错误重试
retry 操作符是另一个错误处理操作符,它允许你在发生错误时重新尝试/重新订阅相同的生产者。你只需要提供一个谓词或重试限制,以确定何时停止重试。所以,让我们看看一个例子:
fun main(args: Array<String>) {
Observable.just(1,2,3,4,5)
.map { it/(3-it) }
.retry(3)//(1)
.subscribeBy (
onNext = {println("Received $it")},
onError = {println("Error")}
)
println("\n With Predicate \n")
var retryCount = 0
Observable.just(1,2,3,4,5)
.map { it/(3-it) }
.retry {//(2)
_, _->
(++retryCount)<3
}
.subscribeBy (
onNext = {println("Received $it")},
onError = {println("Error")}
)
}
在注释 (1) 中,我们使用了一个带有重试限制的 retry 操作符,在注释 (2) 中,我们使用了一个带有谓词的 retry 操作符。retry 操作符将一直重试,直到谓词返回 true,并且每当谓词返回 false 时,都会将错误传递到下游。
这里是输出:

HTTP 示例
任何学习只有在将其应用于实时场景之后才算完整。到目前为止,你已经学习了响应式编程的许多概念。现在,是时候将它们应用于现实世界的场景了,我们将使用 API 通过 HTTP 请求获取一些数据,并将响应数据打印到控制台。
我们为这个例子使用了一个额外的插件——RxJava-Apache-HTTP。如果你使用 Gradle 作为构建工具,请添加以下依赖项:
//RxJava - Apache - HTTP
compile "com.netflix.rxjava:rxjava-apache-http:0.20.7"
这里是代码:
fun main(args: Array<String>) {
val httpClient = HttpAsyncClients.createDefault()//(1)
httpClient.start()//(2)
ObservableHttp.createGet("http://rivuchk.com/feed/json",
httpClient).toObservable()//(3)
.flatMap{ response ->
response.content.map{ bytes ->
String(bytes)
}//(4)
}
.onErrorReturn {//(5)
"Error Parsing data "
}
.subscribe {
println(it)//(6)
httpClient.close()//(7)
}
}
在这个程序中,我们使用 HttpAsyncClients.createDefault() 获取 CloseableHttpAsyncClient 的实例。在开始 HTTP 请求之前,我们首先需要启动客户端。我们在注释 (2) 中的代码中这样做,使用 httpClient.start()。在注释 (3) 中,我们创建了一个 GET 请求并将其转换为类型为 ObservableHttpResponse 的可观察对象,因此我们使用了 flatMap 操作符来访问响应的内容。在 flatMap 操作符内部,我们在注释 (4) 中使用 map 操作符将字节响应转换为 String。
在注释 (5) 中,我们使用了 onErrorReturn 操作符来返回一个默认的 String,以防出现错误。
最后,在 onErrorReturn 操作符之后,我们订阅了链,并在注释 (6) 中打印了响应。我们一完成响应,就关闭了 httpClient。
下面的部分是输出截图:

摘要
这是一个相当长的章节。你学习了如何组合生产者,并深入了解了 flatMap、concatMap 和 switchMap 操作符。你了解了 take 和 skip 操作符及其变体。你学习了响应式编程中的错误处理方法。我们还尝试了一个 HTTP 客户端示例,其中我们请求 API 获取 JSON 数据并将其打印到控制台。我们没有尝试解析 JSON 数据,因为这可能会增加这一级别的复杂性。在本书的后面部分,我们肯定会解析数据并正确显示。
虽然第五章,“异步数据操作符和转换”更多地涉及操作符,但下一章第七章,“使用调度器的 RxKotlin 并发和并行处理”主要关注调度器、处理并发和多线程,我们将更深入地探讨使用 RxKotlin 的异步编程。随着我们通过这本书逐渐过渡到更高级的主题和章节,你需要更加关注每一章,以便正确掌握 Kotlin 中反应式编程的各个方面。
那么,你还在等什么?翻到下一页,第七章,“使用调度器的 RxKotlin 并发和并行处理”正在等待着你。
第七章:使用调度器在 RxKotlin 中进行并发和并行处理
因此,到目前为止,你已经学习了响应式编程的基础。你学习了 Observable、Observers 和 Subjects,以及背压、Flowable、处理器和操作符。现在,是我们学习响应式编程中一些其他新主题的时候了,可能是最重要的主题——并发和并行处理。
关于响应式编程的一个常见误解是响应式编程默认是多线程的。实际上,RxKotlin 默认是在单线程上工作的,尽管它为我们提供了大量的操作符,可以轻松地根据我们的业务逻辑和需求实现多线程。
在本章中,我们将涵盖以下主题:
-
并发编程简介
-
subscribeOn()和observeOn()操作符 -
并行化
并发编程简介
并发的定义可以描述如下:
作为一种编程范式,并发计算是一种模块化编程,即把整体计算分解成可以并发执行的子计算。
– 维基百科
根据定义,并发就是将整个任务分解成小部分,然后并发执行(并发执行和并行执行之间有一个小的区别,我们将在稍后讨论)。
那么,并发执行子计算意味着什么呢?让我们看看一个现实生活中的例子。想象一下你在家做一道新菜,你有三项任务——拿调料、切蔬菜,以及腌制一些东西。现在,如果你一个人做,你必须一个接一个地做,但如果你有一个家庭成员可以帮忙,那么你们可以分担任务。你可以在另一个人拿调料的时候切蔬菜,而你们中谁先完成可以继续第三个任务——腌制食物。
你可以把你自己和帮助你的人(家庭成员)想象成两个线程,或者更具体地说,你作为程序的主要线程(在这里,烹饪)是整个工作的负责人,你将分配任务给你和家庭成员,他是工作线程。你们两个和你的家庭成员共同构成一个线程池。
如果有更多的线程并且整个任务被适当地分配给它们,整个程序将执行得更快。
并行执行与并发
并发和并行化的概念不仅相关,而且它们之间有着深刻的联系;你可以把它们想象成孪生兄弟。它们看起来几乎一样,但也有一些区别。让我们试着去发现。
在之前的例子中,我们讨论了并发,但它似乎是在并行执行的。现在,让我们用一个更好的例子来代替,这个例子不仅可以帮助我们理解并行化,还可以让我们理解并发和并行化之间的区别。
想象一下有一家酒店,有 5 位顾客点了 15 道菜。这 15 道菜代表的是相同的工作任务,每个任务都需要厨师来烹饪。现在,就像之前的例子一样,把厨师想象成线程(在之前的例子中,你和你的家庭成员扮演的是在家中的厨师角色),但他们不会共享菜肴的子部分,而是每次只烹饪一道菜(因为显然有 15 个订单!)。
现在,如果你有 15 名厨师可供使用(包括 15 个烤箱和其他资源),那么你可以一次性完成所有菜肴的烹饪,但这并不经济。你不能随着订单数量的增加无限地增加厨师和资源。更经济的方法是雇佣 5 名厨师,并创建一个订单池(或者你也可以说是一个队列),依次执行订单。因此,每位厨师需要制作三道菜(或任务的迭代)。如果有更多的订单,那么池子会变得更大。
并行化建议在池中明智地划分任务;而不是为每个任务创建线程,创建一个任务池,并将它们分配给现有的线程,并重复使用它们。
结论是,通过并发实现并行化,但这并不是同一件事;相反,它关乎如何使用并发。
现在,为什么这如此重要?或者更确切地说,为什么它真的有必要?我想你已经得到了答案,但让我们来检查一下。
想象一下你正在处理一个大型数据集,并且需要在将数据展示给用户之前对它们执行一系列长链操作。如果你是应用开发者,你可能希望在后台执行所有操作,并将结果数据传递到前台以展示给用户。并发对于这种情况非常有用。
正如我之前提到的,RxKotlin 不会并发执行操作,但它提供了大量的选项来并发执行选定的操作,并将选择权留给你。
你可能想知道,如果 RxKotlin 默认是单线程的,那么它是如何处理订阅的?订阅应该是并发的吗?在我们进一步使用 RxKotlin 进行并发计算之前,让我们找到答案。
所以,无论何时你订阅了一个 Observable 和/或 Flowable,当前线程都会被阻塞,直到所有项目都被观察者链发出和接收(除了具有间隔和定时器工厂方法的场景)。惊讶吗?然而,这实际上很好,因为对于 Observable 链,如果为每个操作符分配一个单独的线程(任何操作符通常订阅源 Observable 并对其发射进行操作,下一个操作符订阅当前操作符的发射),那么将会非常混乱。
为了解决这个场景,ReactiveX 为我们提供了调度器和调度操作符。通过使用它们,线程管理变得简单,因为同步几乎是自动的,并且线程之间没有共享数据(作为函数式编程的基本属性,因此是函数式响应式编程)。
现在我们已经了解了并发背后的理念,我们可以继续使用 RxKotlin 来实现并发。
什么是调度器?
在 ReactiveX 中,并发的核心在于调度器。正如我已经提到的,默认情况下,Observable 和应用于它的操作符链将在调用 subscribe 的同一线程上执行工作,并且线程将被阻塞,直到观察者收到 onComplete 或 onError 通知。我们可以使用调度器来改变这种行为。
调度器可以被视为一个线程池,ReactiveX 可以从中池化一个线程并在其上执行任务。它基本上是多线程和并发的抽象,使得在 ReactiveX 中实现并发变得更加容易。
调度器类型
作为线程池管理的抽象层,调度器 API 为你提供了一些预定义的调度器。它还允许你创建一个新的用户定义的调度器。让我们看看可用的调度器类型:
-
Schedulers.io() -
Schedulers.computation() -
Schedulers.newThread() -
Schedulers.single() -
Schedulers.trampoline() -
Schedulers.from()
我们将探讨它们的定义和预定的使用场景,但首先,让我们从一些代码开始。
我们将从没有调度器的常规示例开始,然后在这个示例中实现一个调度器来观察差异,如下所示:
fun main(args: Array<String>) {
Observable.range(1,10)
.subscribe {
runBlocking { delay(200) }
println("Observable1 Item Received $it")
}
Observable.range(21,10)
.subscribe {
runBlocking { delay(100) }
println("Observable2 Item Received $it")
}
}
在这个程序中,我们使用了两个 Observable;我们在它们的订阅中使用了延迟来模拟长时间运行的任务。
下面的输出显示了预期的结果。观察者一个接一个地运行:

这个程序的总执行时间大约为 3,100 毫秒(因为延迟是在打印之前执行的),而线程池在这期间处于空闲状态。使用调度器,这个时间可以显著减少。让我们来完成它:
fun main(args: Array<String>) {
Observable.range(1, 10)
.subscribeOn(Schedulers.computation())//(1)
.subscribe {
runBlocking { delay(200) }
println("Observable1 Item Received $it")
}
Observable.range(21, 10)
.subscribeOn(Schedulers.computation())//(2)
.subscribe {
runBlocking { delay(100) }
println("Observable2 Item Received $it")
}
runBlocking { delay(2100) }//(3)
}
与前一个程序相比,此程序包含三条新行。在注释(1)和(2)处,subscribeOn(Schedulers.computation()),以及在注释(3)处的runBlocking { delay(2100) }。在查看输出后,我们将检查这些行的意义:

如输出所示,此例中的Observable是并发发出的。subscribeOn(Schedulers.computation())代码的这一行使得两个下游都可以在不同的(后台)线程中订阅Observable,这影响了并发性。你应该已经习惯了在注释(3)中使用runBlocking { delay(2100) };我们使用它来保持程序运行。由于所有操作都在不同的线程中执行,我们需要阻塞主线程以保持程序运行。然而,请注意我们传递的延迟时间;它仅为 2,100 毫秒,输出确认了两次订阅都处理了所有发射。因此,很明显,我们立即节省了 1,000 毫秒。
现在,让我们继续讨论可用的不同类型的调度器——然后我们将深入了解如何使用它们的不同方法。
Schedulers.io() - I/O 密集型调度器
Schedulers.io()为我们提供了 I/O 密集型线程。更准确地说,Schedulers.io()为你提供了一个ThreadPool,它可以创建无限数量的工作线程,这些线程旨在执行 I/O 密集型任务。
现在,I/O 密集型线程究竟是什么意思?为什么我们称之为 I/O 密集型?让我们来检查一下。
此池中的所有线程都是阻塞的,并且旨在执行比计算密集型任务更多的 I/O 操作,从而减轻 CPU 的负载,但由于等待 I/O,可能会花费更长的时间。通过 I/O 操作,我们指的是与文件系统、数据库、服务或 I/O 设备的交互。
我们在使用此调度器时应谨慎,因为它可以创建无限数量的线程(直到内存耗尽),并可能导致OutOfMemory错误。
Schedulers.computation() - CPU 密集型调度器
Schedulers.computation()可能是对程序员最有用的调度器。它为我们提供了一个有界线程池,该线程池可以包含与可用 CPU 核心数量相等的线程数。正如其名称所暗示的,此调度器旨在用于 CPU 密集型工作。
我们应该只将此调度器用于 CPU 密集型任务,而不用于任何其他原因。原因是此调度器中的线程会保持 CPU 核心忙碌,如果用于 I/O 密集型或涉及非计算任务的任何其他任务,可能会减慢整个应用程序的运行速度。
我们应该考虑使用 Schedulers.io() 来处理 I/O 密集型任务,以及使用 Schedulers.computation() 来处理计算任务的主要原因,是因为 computation() 线程更好地利用了处理器,并且不会创建超过可用 CPU 核心的线程,而是重用它们。而 Schedulers.io() 是无界的,如果你在 io() 上并行调度 10,000 个计算任务,那么每个任务都将拥有自己的线程,并竞争 CPU,从而产生上下文切换的成本。
Schedulers.newThread()
Schedulers.newThread() 为我们提供了一个创建每个任务都分配一个新线程的调度器。虽然乍一看可能看起来与 Schedulers.io() 相似,但实际上存在巨大的差异。
Schedulers.io() 使用线程池,并且每当它得到一个新的工作单元时,它首先检查线程池,看是否有空闲的线程可以承担任务;如果没有可用的现有线程来承担工作,它将创建一个新的线程。
然而,Schedulers.newThread() 甚至不使用线程池;相反,它为每个请求创建一个新的线程,并且永远不再记住它们。
在大多数情况下,当你不使用 Schedulers.computation() 时,你应该考虑使用 Schedulers.io(),并且应该主要避免使用 Schedulers.newThread();线程是非常昂贵的资源,你应该尽可能避免创建新的线程。
Schedulers.single()
Schedulers.single() 为我们提供了一个只包含一个线程的调度器,并为每个调用返回单个实例。困惑吗?让我们澄清一下。想象一下你需要执行强顺序的任务的情况——Schedulers.single() 是你这里的最佳选择。因为它只提供给你一个线程,所以你在这里排队的每个任务都必然是顺序执行的。
Schedulers.trampoline()
Schedulers.single() 和 Schedulers.trampoline() 听起来有些相似,这两个调度器都是用于顺序执行。虽然 Schedulers.single() 保证所有任务都将顺序执行,但它可能与被调用的线程并行运行(如果不是,那么该线程也是来自 Schedulers.single());而 Schedulers.trampoline() 在这一点上有所不同。
与 Schedulers.single() 一样维护一个线程不同,Schedulers.trampoline() 将任务排队到被调用的线程上。
因此,它将与被调用的线程顺序执行。
让我们看看 Schedulers.single() 和 Schedulers.trampoline() 的几个示例,以更好地理解它们:
fun main(args: Array<String>) {
async(CommonPool) {
Observable.range(1, 10)
.subscribeOn(Schedulers.single())//(1)
.subscribe {
runBlocking { delay(200) }
println("Observable1 Item Received $it")
}
Observable.range(21, 10)
.subscribeOn(Schedulers.single())//(2)
.subscribe {
runBlocking { delay(100) }
println("Observable2 Item Received $it")
}
for (i in 1..10) {
delay(100)
println("Blocking Thread $i")
}
}
runBlocking { delay(6000) }
}
输出如下:

输出清楚地显示,尽管两个订阅都是顺序执行的,但它们与调用线程并行运行。
现在,让我们使用 Schedulers.trampoline() 实现相同的代码,并观察差异:
fun main(args: Array<String>) {
async(CommonPool) {
Observable.range(1, 10)
.subscribeOn(Schedulers.trampoline())//(1)
.subscribe {
runBlocking { delay(200) }
println("Observable1 Item Received $it")
}
Observable.range(21, 10)
.subscribeOn(Schedulers.trampoline())//(2)
.subscribe {
runBlocking { delay(100) }
println("Observable2 Item Received $it")
}
for (i in 1..10) {
delay(100)
println("Blocking Thread $i")
}
}
runBlocking { delay(6000) }
}
以下输出显示了调度器是顺序运行到调用线程的:

Schedulers.from
到目前为止,我们已经看到了 RxKotlin 中可用的默认/预定义的调度器。然而,在开发应用程序时,您可能需要定义自己的自定义调度器。考虑到这种情况,ReactiveX 为您提供了 Schedulers.from(executor:Executor),这允许您将任何执行器转换为调度器。
让我们看看以下示例:
fun main(args: Array<String>) {
val executor:Executor = Executors.newFixedThreadPool(2)//(1)
val scheduler:Scheduler = Schedulers.from(executor)//(2)
Observable.range(1, 10)
.subscribeOn(scheduler)//(3)
.subscribe {
runBlocking { delay(200) }
println("Observable1 Item Received $it -
${Thread.currentThread().name}")
}
Observable.range(21, 10)
.subscribeOn(scheduler)//(4)
.subscribe {
runBlocking { delay(100) }
println("Observable2 Item Received $it -
${Thread.currentThread().name}")
}
Observable.range(51, 10)
.subscribeOn(scheduler)//(5)
.subscribe {
runBlocking { delay(100) }
println("Observable3 Item Received $it -
${Thread.currentThread().name}")
}
runBlocking { delay(10000) }//(6)
}
在这个示例中,我们从一个 Executor(为了简单起见,我们使用了标准的线程池执行器;您可以使用自己的自定义执行器)创建了一个自定义 Scheduler。
在注释 (1) 中,我们使用 Executors.newFixedThreadPool() 方法创建了执行器,在注释 (2) 中,我们使用 Schedulers.from(executor:Executor) 创建了 scheduler 实例。我们在注释 (3)、(4) 和 (5) 中使用了 scheduler 实例。
这是输出结果:

如何使用调度器 - subscribeOn 和 observeOn 操作符
现在我们已经对调度器有了基本的了解,知道了有多少种类型的调度器,以及如何创建 scheduler 实例,我们将关注如何使用调度器。
基本上有两个操作符帮助我们实现调度器。到目前为止,在本章中,我们在所有示例中都使用了带有调度器的 subscribeOn 操作符;然而,还有一个操作符——observeOn。我们现在将专注于这两个操作符,学习它们是如何工作的,以及它们之间的区别。
让我们从 subscribeOn 操作符开始。
在订阅时更改线程 - subscribeOn 操作符
在深入了解如何使用调度器之前,我们需要理解 Observable 的工作原理。让我们看看以下图形:

如前图所示,是线程负责通过操作符将项目从源传递到订阅者。在整个订阅过程中,这可能是一个线程,或者在不同级别上可能是不同的线程。
默认情况下,我们执行订阅的线程负责将所有排放项传递给订阅者,除非我们指示它这样做。
首先,让我们看看代码示例:
fun main(args: Array<String>) {
listOf("1","2","3","4","5","6","7","8","9","10")
.toObservable()
.map {
item->
println("Mapping $item ${Thread.currentThread().name}")
return@map item.toInt()
}
.subscribe {
item -> println("Received $item
${Thread.currentThread().name}")
}
}
这是一个简单的 RxKotlin 代码示例;我们创建了一个 Observable,对其进行映射,然后订阅它。这里唯一的区别是我已经在 map 和 subscribe 独立函数中打印了 Thread 名称。
让我们看看输出结果:

从输出中,我们可以确定主线程执行了整个订阅过程。
subscribeOn 操作符,正如其名所示,帮助我们改变订阅的线程。让我们修改一次程序并查看结果:
fun main(args: Array<String>) {
listOf("1","2","3","4","5","6","7","8","9","10")
.toObservable()
.map {
item->
println("Mapping $item - ${Thread.currentThread().name}")
return@map item.toInt()
}
.subscribeOn(Schedulers.computation())//(1)
.subscribe {
item -> println("Received $item -
${Thread.currentThread().name}")
}
runBlocking { delay(1000) }
}
整个程序保持不变,只是在 map 和 subscribe 之间,我们在注释 (1) 中使用了 subscribeOn 操作符。让我们查看输出结果:

subscribeOn操作符会改变整个订阅的线程;你可以在订阅流程的任何位置使用它。它将一次改变线程,并永久改变。
在不同的线程上进行观察——observeOn操作符
虽然subscribeOn看起来像是来自天堂的神奇礼物,但在某些情况下可能并不适用。例如,你可能想在computation线程上进行计算,并从io线程显示结果,这实际上是你应该做的。subscribeOn操作符需要所有这些事情的伴侣;虽然它将指定整个订阅的线程,但它需要其伴侣来指定特定操作符的线程。
subscribeOn操作符的完美伴侣是observeOn操作符。observeOn操作符指定了其后所有调用操作符的调度器。
让我们通过observeOn修改我们的程序,以在Schedulers.computation()中执行map操作,并在Schedulers.io()中接收订阅的结果(onNext):
fun main(args: Array<String>) {
listOf("1","2","3","4","5","6","7","8","9","10")
.toObservable()
.observeOn(Schedulers.computation())//(1)
.map {
item->
println("Mapping $item - ${Thread.currentThread().name}")
return@map item.toInt()
}
.observeOn(Schedulers.io())//(2)
.subscribe {
item -> println("Received $item -
${Thread.currentThread().name}")
}
runBlocking { delay(1000) }
}
以下输出清楚地表明我们成功地实现了我们的目标:

那么,我们做了什么呢?我们在map操作符之前通过调用observeOn(Schedulers.computation())来指定computation线程,并在订阅之前调用observeOn(Schedulers.io())以切换到io线程来接收结果。
在这个程序中,我们进行了上下文切换;我们轻松地与线程交换数据,并通过仅仅 7-8 行代码实现了线程间的通信——这就是调度器为我们提供的抽象。
摘要
在本章中,你学习了并发执行和并行性,以及如何在 RxKotlin 中实现多线程。在当今以应用程序驱动的时代,多线程是必要的,因为现代用户不喜欢等待,或者,为了避免阻塞,你需要不断地切换线程来执行计算和 UX 操作。
在本章中,你学习了 RxKotlin 中的调度器如何帮助你,或者更确切地说,调度器如何抽象出多线程的复杂性。
虽然并发执行和并行性是现代应用程序开发的一个基本组成部分,但测试可能是最关键的部分。我们无法在没有测试的情况下交付任何应用程序。敏捷方法(尽管我们在这里不讨论敏捷)表示我们应该反复进行测试,并且在我们产品的(应用程序)开发的每一次迭代中都要进行测试。
在第八章《测试 RxKotlin 应用程序》中,我们将讨论测试。不要错过它,现在就翻到下一页!
第八章:测试 RxKotlin 应用程序
我们已经覆盖了这本书的 60%以上,学到了很多概念。从第一章开始,从反应式编程的概念到上一章关于并发执行和并行性的内容。但是,我们无法在不引入一些测试的情况下完成应用程序开发。这可能是应用程序开发过程中最关键的一点。
本章专门介绍测试。由于 Kotlin 本身相对较新,我们的第一个目标将是学习在 Kotlin 中进行测试。然后我们将继续学习在 RxKotlin 中的测试。以下是本章将要涵盖的主题:
-
单元测试简介及其重要性
-
Kotlin 和 JUnit,Kotlin-test
-
RxKotlin 中的测试工具
-
阻塞订阅者
-
阻塞操作符
-
TestObserver和TestSubscriber
那么,让我们开始吧。
单元测试简介及其重要性
虽然测试在应用程序开发中是绝对必要的,但许多新手开发者逃避了一些关于测试的基本问题。他们是:
-
什么是单元测试?为什么它是开发者的工作?
-
为什么单元测试如此重要?
-
那么,我们是否需要为程序的每个部分编写单元测试?
我们将从这个章节的开始就回答这些基本问题。如果你更愿意直接使用 RxKotlin 进行测试,你可以跳过本章的前几节,从RxKotlin 中的测试工具开始。尽管我鼓励你通读本章,即使你之前有使用 Kotlin 进行测试的经验。
让我们先定义一下单元测试。单元测试是软件测试的一个级别,其中软件(即应用程序)的最小可测试组件(称为单元)被测试。其目的是验证每个软件单元是否按预期执行。
单元测试可以手动进行,但通常都是自动化的。自动化单元测试的唯一目的是减少人为错误并消除由它们引起的任何额外错误/bug。为了解释这一点,我们首先记住这个谚语:
人非圣贤,孰能无过
因此,如果我们手动进行单元测试,额外的错误或 bug 的可能性会增加。自动化单元测试可以消除这种风险,因为它们包含最小的人为努力。
此外,我们需要记录我们进行的测试,并且我们需要在产品每次增量构建时使用新的测试重新执行相同的测试。自动化单元测试消除了这项额外的工作,因为你只需要编写一次测试,然后你可以在未来的任何时间运行它们。此外,自动化单元测试还可以减少文档工作。
为什么它是开发者的工作?除了开发者,还有谁会编写自动化测试的代码?
此外,在完成应用程序的每个小单元后,开发者无法向测试者提供理解。即使你可能已经完成了一些模块,这些模块尚未在 GUI 上,因此测试者或其他人可能甚至无法到达该单元进行测试。此外,它可能没有直接的影响或与 UI/UX 的关系,它可能是一个小的内部代码部分。
总结一下,开发者更好地理解他的代码,并且他知道他确切地想要从这堆代码中得到什么。因此,开发者是编写该模块单元测试的最佳人选。
为什么单元测试如此重要?
让我们用一个现实生活中的例子来说明。想象一个工程师,正在创建一个新的电机或设备。工程师将在完成该电机的每个单元后测试其功能,而不是在最后测试整个电机(尽管他/她最终也会测试整个电机,但在构建过程中也会反复和逐步地进行测试)。这种行为的背后主要原因是,如果他/她不这样做,最终将需要大量努力来识别确切的问题(如果有的话)。而逐步测试将允许你在问题出现时立即修复它。同样,这也适用于软件(应用程序)。
你应该定期和反复地执行单元测试,随着你开发应用程序的每个模块,测试越多,最终产品就越好。是的,我们应该为应用程序的每个功能部分编写单元测试。
通过 功能部分 我们指的是执行任何小操作和/或功能的每个部分。我们可以跳过测试只有获取器和设置器的 POJO 类,但我们必须测试使用该 POJO 类来完成某事的代码。
因此,既然我们已经理解了测试的重要性,让我们从在 Kotlin 中编写 JUnit 测试开始。
在 Kotlin 中编写 JUnit 测试
如果你有任何 Java 开发的经验,你一定听说过或最可能使用过 JUnit。它是一个用于 Java(以及 Kotlin)的测试框架。
通常情况下,单元测试是在与实际源代码分开的单独源文件夹中创建的,以保持其分离。标准的 Maven/Gradle 习惯使用 src/main 用于实际代码(Java/Kotlin 文件或类)和 src/test 用于测试类。以下截图显示了本书中使用的项目的结构:

在开始编写测试用例之前,我们必须添加以下 Gradle 依赖项:
testCompile 'junit:junit:4.12'
testCompile "org.mockito:mockito-core:1.9.5"
testCompile "org.jetbrains.kotlin:kotlin-test-
junit:$kotlin_version"
我们还添加了对 Mockito 的依赖,我们将在不久的将来介绍它。
因此,我们已经准备好了所有东西,让我们编写我们的第一个测试用例。请参考以下代码:
package com.rivuchk.packtpub.reactivekotlin
import org.junit.Test
import kotlin.test.assertEquals
class TestClass {
@Test//(1)
fun `my first test`() {//(2)
assertEquals(3,1+2)//(3)
}
}
仔细看看前面的程序。每个 JUnit 测试用例都应该定义为一个类内的函数。包含 JUnit 测试函数的类应仅用于测试目的,不应具有其他用途。test 函数应使用 @Test 注解,就像我们在注释 (1) 中做的那样。这个注解帮助 JUnit 识别和执行测试。
现在,仔细看看包含注释 (2) 的那一行。函数名是 `my first test`()。是的,函数名中包含空格。这可能是你在 Kotlin 编写测试用例时能得到的最好的东西。Kotlin 允许你拥有没有空格的函数名,虽然这在编写代码时不是好的实践,但在编写测试时它们实际上可以救命;因为你不需要在其他地方调用 test 函数,它们实际上充当了可读的测试名称。
在注释 (3) 中,我们编写了实际的测试。assertEquals 测试检查 expected 和 actual 值之间的相等性。此测试的第一个参数是预期值,第二个参数是实际值,它应该与预期值相等。
如果您运行测试,您将得到以下输出:

如果我们修改程序并将 2+3 而不是 1+2 作为实际参数传递,那么测试将失败,并给出以下输出:

您还可以传递一个错误消息,该消息将在失败时显示,如下所示:
class TestClass {
@Test//(1)
fun `my first test`() {//(2)
assertEquals(3,2+3, "Actual value is not equal to the expected
one.")//(3)
}
}
如果测试失败,错误报告中将显示该消息。请查看以下输出:

测试您的代码
在前面的部分,我们学习了如何编写测试用例,但我们测试了我们的代码吗?没有。我们使用一些无意识的值进行了测试。我们知道这不是测试的目的。测试是为了确保我们的函数、类和代码块按预期工作。
我们应该在现有的代码之上编写测试(除非我们正在遵循测试驱动开发(TDD))。
测试驱动开发是一种开发方法,其中首先编写测试,然后编写实际的源代码以通过测试用例。测试驱动开发在开发人员和架构师中非常受欢迎,许多公司将其作为其开发流程遵循的 TDD。
以下是一个包含一些计算方法的 Kotlin 小文件,我们将在该文件上执行测试:
package com.rivuchk.packtpub.reactivekotlin.chapter8
fun add(a:Int, b:Int):Int = a+b
fun substract(a:Int, b:Int):Int = a-b
fun mult(a:Int, b:Int):Int = a*b
fun divide(a:Int, b:Int):Int = a/b
然后,以下类包含测试用例,仔细查看代码,然后我们将对其进行描述:
package com.rivuchk.packtpub.reactivekotlin.chapter8//(1)
import org.junit.Test
import kotlin.test.*
class TestCalculator {
@Test
fun `addition test`() {//(2)
assertEquals(1 + 2, add(1,2))
}
@Test
fun `substraction test`() {//(3)
assertEquals(8-5, substract(8,5))
}
@Test
fun `multiplication test`() {//(4)
assertEquals(4 * 2, mult(4,2))
}
@Test
fun `division test`() {//(5)
assertEquals(8 / 2, divide(8,2))
}
}
看看包声明。两个文件共享相同的包名,我们故意这样做,这样我们就不需要导入函数。
我们在源代码中使用了最简单的函数,这样你可以轻松理解代码。同时请注意,我们像函数一样单独编写了每个测试用例。虽然显然可以在一个测试用例中调用多个测试函数。困惑吗?让我们详细说明一下,当你测试单个函数或属性的多个方面时,你可以(并且应该)将它们全部组合在一个测试函数(带有@Test注解的函数)中。通常,编译器在遇到测试函数时会显示测试结果,而不考虑每个测试函数执行了多少测试。所以请放心,如果你将它们组合在一个单独的测试函数中,你的测试将会被执行,但它们将作为一个单独的测试显示。然而,当你为不同的函数或属性编写测试时,你显然希望为所有这些函数生成单独的报告,在这种情况下,你应该像前面的例子一样单独编写它们。
现在看看输出结果:

但在每个先前的例子中,我们只使用了assertEquals;看到这一点,你可能会有疑问,assertEquals是唯一的测试函数吗?答案是绝对不是。我们有大量的测试函数可用。以下是一些具有未知值的测试用例,只是为了让你了解 Kotlin 中最有用的测试函数。请参考以下代码:
package com.rivuchk.packtpub.reactivekotlin.chapter8
import org.junit.Test
import java.util.*
import kotlin.test.*
class TestFunctions {
@Test
fun `expected block evaluation`() {
expect(10,{
val x=5
val y=2
x*y
})
}
@Test
fun `assert illegal value`() {
assertNotEquals(-1,Random().nextInt(1))
}
@Test
fun `assert true boolean value`() {
assertTrue(true)
}
@Test
fun `assert false boolean value`() {
assertFalse(false)
}
@Test
fun `assert that passed value is null`() {
assertNull(null)
}
@Test
fun `assert that passed value is not null`() {
assertNotNull(null)
}
}
在检查这里的测试用例之前,让我们看看以下测试输出截图:

现在,让我们尝试理解代码。我们将从``` expected block evaluation() ``测试用例开始。expect测试函数将期望值作为第一个参数,将一个块(lambda)作为第二个参数,执行 lambda,并检查返回值与期望值是否相等。
第二个测试用例是``` assert illegal value() ``,在该测试用例中,我们使用了assertNotEquals()测试方法。这个测试方法与assertEquals()相反。如果两个参数相等,则测试失败。assertNotEquals()在有一个函数应该返回任何值除了特定值时特别有用。
在 `assert true boolean value`() ``和 assert true boolean value() ``测试用例中,我们分别使用了assertTrue()和assertFalse()。这两个测试方法都接受一个Boolean值作为参数。正如其名所示,assertTrue()期望值是true,而assertFalse()期望值是false。
接下来的两个测试用例是针对 null 值的。第一个``` assert that passed value is null() ``使用assertNull(),它期望传递的值包含null。第二个使用assertNotNull(),与assertNull()完全相反,它期望值不是null。
因此,在我们对编写测试用例有了实际了解之后,让我们开始使用RxKotlin进行测试。
在 RxKotlin 中进行测试
现在,既然您在 Kotlin 中有一些实际测试经验,也对 RxKotlin 有了一些了解,您可能想知道如何在 RxKotlin 中实现测试用例?在 RxKotlin 中进行测试可能看起来并不直接;原因是 ReactiveX 定义的是行为而不是状态,而包括 JUnit 和 kotlin—test 在内的大多数测试框架都适用于测试状态。
为了帮助开发者,RxKotlin 附带了一套用于测试的工具,您可以使用您喜欢的测试框架。在这本书中,我们将介绍如何使用 JUnit 和 Kotlin-test 在 RxKotlin 中进行测试。
那么,我们在等待什么呢?让我们开始吧。
阻塞订阅者
尝试回忆一下之前章节中的代码块,我们在使用 delay 使主线程等待时使用了它,无论我们使用的是在另一个线程上操作的 Observable 或 Flowable。一个完美的例子是当我们使用 Observable.interval 作为工厂方法或使用 subscribeOn 操作符时。为了让您回忆起来,以下是一个这样的代码示例:
fun main(args: Array<String>) {
Observable.range(1,10)
.subscribeOn(Schedulers.computation())
.subscribe {
item -> println("Received $item")
}
runBlocking { delay(10) }
}
在这个例子中,我们将订阅切换到了 Schedulers.computation。现在让我们看看,我们如何测试这个 Observable 并检查我们是否接收到了正好 10 个发射:
@Test
fun `check emissions count` () {
val emissionsCount = AtomicInteger()//(1)
Observable.range(1,10)
.subscribeOn(Schedulers.computation())
.blockingSubscribe {//(2)
_ -> emissionsCount.incrementAndGet()
}
assertEquals(10,emissionsCount.get())//(3)
}
在深入研究代码之前,我们先看看测试结果:

在这段代码中,有几件事情需要解释。首先是 AtomicInteger。AtomicInteger 是 Java 中整数的一个包装器,它允许 Int 值原子性地更新。尽管 AtomicInteger 扩展了 Number 以允许工具和实用程序以统一的方式访问基于数字的类,但它不能作为 Integer 的替代品。我们在代码中使用 AtomicInteger 来确保原子性,因为订阅是在 computationScheduler(因此是多线程)中运行的。
需要我们注意的行是我们在其中放置注释 (2) 的地方。我们使用了 blockingSubscribe 而不是仅仅使用 subscribe。当我们使用 subscribe 操作符订阅一个生产者,并且订阅不在当前线程时,当前线程不会等待订阅完成,而是立即移动到下一行。这就是为什么我们使用延迟来使当前线程等待。在 tests 中使用 delay 是麻烦的。而 blockingSubscribe 会阻塞当前运行的线程直到订阅完成(即使订阅发生在另一个线程),这在编写测试时是有用的。
阻塞操作符
虽然 blockingSubscribe 在测试中很有用,但它并不总是能满足您的需求。您可能需要测试生产者的第一个、最后一个或所有值。为此,您需要数据以纯命令式的方式。
在那种情况下,RxKotlin 中尚未发现的操作符集在你手中。阻塞操作符作为响应式世界和命令式世界之间立即可访问的桥梁。它们阻塞当前线程并使其等待结果被发出,但以非响应式的方式返回它们。
blockingSubscribe 和阻塞操作符之间唯一的相似之处在于,即使响应式操作在另一个线程中执行,两者都会阻塞声明线程。
除了这个之外,没有更多的相似之处。blockingSubscribe 将数据视为响应式,并且不返回任何内容。它而是将它们推送到指定的订阅者(或 lambda)那里。而阻塞操作符将以非响应式的方式返回数据。
以下列表包含我们将要介绍的阻塞操作符:
-
blockingFirst() -
blockingGet() -
blockingLast() -
blockingIterable() -
blockingForEach()
尽管我们应该避免在生产环境中使用它们,因为它们鼓励反模式并减少了响应式编程的好处,但是我们可以肯定地用于测试目的。
获取第一个发出的项 – blockingFirst()
我们将要讨论的第一个阻塞操作符是 blockingFirst 操作符。此操作符会阻塞调用线程,直到第一个项被发出并返回它。以下是一个理想的 blockingFirst() 测试用例,其中我们在 Observable 上执行排序操作,并通过检查第一个发出的项是否是最小的来测试它。请参考以下代码:
@Test
fun `test with blockingFirst`() {
val observable = listOf(2,10,5,6,9,8,7,1,4,3).toObservable()
.sorted()
val firstItem = observable.blockingFirst()
assertEquals(1,firstItem)
}
测试结果如下:

在程序中,我们创建了一个从 1 到 10 的未排序整数列表,并使用该列表创建了一个 Observable,因此从该 Observable 中最小的项应该是 1。我们获取了第一个项,并使用 blockingFirst() 操作符使线程等待,直到我们获取到它。
然后使用 assertEquals 测试函数来断言第一个发出的项是 1。
从单个或 maybe 中获取唯一项 – blockingGet
当你与 single 或 maybe 一起工作时,你只能使用 blockingGet() 以外的任何阻塞操作符。原因很简单,这两个单子只能包含一个项。
因此,让我们通过修改最后一个测试用例来创建两个新的测试用例,如下所示:
@Test
fun `test Single with blockingGet`() {
val observable = listOf(2,10,5,6,9,8,7,1,4,3).toObservable()
.sorted()
val firstElement:Single<Int> = observable.first(0)
val firstItem = firstElement.blockingGet()
assertEquals(1,firstItem)
}
@Test
fun `test Maybe with blockingGet`() {
val observable = listOf(2,10,5,6,9,8,7,1,4,3).toObservable()
.sorted()
val firstElement:Maybe<Int> = observable.firstElement()
val firstItem = firstElement.blockingGet()
assertEquals(1,firstItem)
}
在第一个测试用例中,我们使用了 observable.first() 并带有默认值,此操作符返回一个 Single;在第二个操作符中,我们使用了 observable.firstElement(),此操作符返回一个 Maybe。然后我们在两个测试用例中都使用了 blockingGet 来获取第一个元素作为 Int 并执行测试函数。
因此,以下截图是测试结果:

获取最后一个项 - blockingLast
我们有blockingFirst,所以很明显我们会有一个blockingLast。正如预期的那样,它会在阻塞线程直到源发出它之前获取最后一个发出项。以下是一个代码示例:
@Test
fun `test with blockingLast`() {
val observable = listOf(2,10,5,6,9,8,7,1,4,3).toObservable()
.sorted()
val firstItem = observable.blockingLast()
assertEquals(10,firstItem)
}
由于我们期望获取最后一个发出项,所以我们正在检查与10的等价性。
以下是测试结果的截图:

将所有发出项作为可迭代对象获取 - blockingIterable 操作符
因此,我们获取了第一个发出的项目,我们也获取了最后一个发出的项目,但如果我们想获取所有用于测试发出的项目呢?blockingIterable操作符可以提供同样的功能。blockingIterable操作符以一种有趣的方式工作,它将一个发出项传递给Iterable,然后Iterable将保持阻塞迭代线程,直到下一个发出项可用。这个操作符将未消费的值排队,直到Iterator可以消费它们,这可能会导致OutOfMemory异常。
所以以下是一个示例,其中我们获取了完整的列表,然后我们将返回的Iterable转换为List,并在排序后与源list进行比较以检查等价性。请参考以下代码:
@Test
fun `test with blockingIterable`() {
val list = listOf(2,10,5,6,9,8,7,1,4,3)
val observable = list.toObservable()
.sorted()
val iterable = observable.blockingIterable()
assertEquals(list.sorted(),iterable.toList())
}
如果发出项是排序的,那么当转换为list时,iterable应该等于list.sorted()。
以下是测试结果的截图:

遍历所有发出项 - blockingForEach
如果你想要遍历所有发出项,那么blockingForEach可能是一个更好的解决方案。它比blockingIterable更好,因为它不会排队发出项。相反,它会阻塞调用线程,等待每个发出项被处理后再允许线程继续。
在下面的示例中,我们从一个Int列表创建了一个Observable,然后只应用了偶数的过滤器,然后在blockingForEach中测试是否所有接收到的数字都是偶数:
@Test
fun `test with blockingForEach`() {
val list =
listOf(2,10,5,6,9,8,7,1,4,3,12,20,15,16,19,18,17,11,14,13)
val observable = list.toObservable()
.filter { item -> item%2==0 }
observable.forEach {
item->
assertTrue { item%2==0 }
}
}
测试的结果如下:

我们已经涵盖了到目前为止最有用的阻塞操作符。它们对于简单的断言很有用,并且可以有效地阻塞代码,以便我们可以执行我们的测试操作。
然而,在生产中使用阻塞代码并没有好处。虽然看起来在测试中使用阻塞代码是可以的,但实际上并不是这样。它可能会对你从测试中获得的利益造成重大损害。如何?只需想想多个 Observables/Flowables 并发地向你的应用程序发出,如果你将它们放在阻塞代码上,它们的完整行为可能会改变,结果你将失去单元测试的利益。
那么,出路在哪里呢?让我们看看。
介绍 TestObserver 和 TestSubscriber
随着你阅读这一章,你可能已经形成了一个想法,我们只能通过阻塞代码来执行测试,要么使用 blockingSubscribe,要么使用阻塞操作符。但这并不是事实。实际上,还有更多全面的方法来处理响应式代码,或者说我们可以以响应式的方式测试响应式代码。
更精确地说,在 Subscriber 中,我们有 onError 和 onComplete,它们需要与 onNext 一起测试,而仅仅使用阻塞并不总是可能的。是的,某种形式的阻塞是必要的,但它不能独自完成所有的事情,它还需要以响应式的方式进行管理。
因此,这里有你的两位超级英雄,让开发者的生活变得简单——TestObserver 和 TestSubscriber。与 Subscriber 和 Observer 一样,你可以使用 TestSubscriber 与 Flowables,使用 TestObserver 与 Observables,这两者之间除了这一点外,其他都是相似的。
那么,让我们从一个例子开始:
@Test
fun `test with TestObserver`() {
val list =
listOf(2,10,5,6,9,8,7,1,4,3,12,20,15,16,19,18,17,11,14,13)
val observable = list.toObservable().sorted()
val testObserver = TestObserver<Int>()
observable.subscribe(testObserver)//(1)
testObserver.assertSubscribed()//(2)
testObserver.awaitTerminalEvent()//(3)
testObserver.assertNoErrors()//(4)
testObserver.assertComplete()//(5)
testObserver.assertValueCount(20)//(6)
testObserver.assertValues
(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20)//(7)
}
@Test
fun `test with TestSubscriber`() {
val list =
listOf(2,10,5,6,9,8,7,1,4,3,12,20,15,16,19,18,17,11,14,13)
val flowable = list.toFlowable().sorted()
val testSubscriber = TestSubscriber<Int>()
flowable.subscribe(testSubscriber)//(1)
testSubscriber.assertSubscribed()//(2)
testSubscriber.awaitTerminalEvent()//(3)
testSubscriber.assertNoErrors()//(4)
testSubscriber.assertComplete()//(5)
testSubscriber.assertValueCount(20)//(6)
testSubscriber.assertValues
(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20)//(7)
}
因此,我们确实使用 TestObserver 和 TestSubscriber 执行了相同的测试集。测试结果显然是通过的:

让我们现在理解测试用例。在注释 (1) 中,我们正在订阅 Observable/Flowable。在注释 (2) 中,我们正在检查订阅是否成功,并且只有一个,这是通过 assertSubscribed() 测试来实现的。在注释 (3) 中,我们正在使用 awaitTerminalEvent() 方法阻塞线程,直到 Observable/Flowable 完成其执行。这个终端事件可以是 onComplete 或 onError。在注释 (4) 和 (5) 中,我们正在检查 Observable 和/或 Flowable 是否成功完成而没有错误,assertNoErrors() 将测试订阅是否没有收到任何错误,而 assertComplete() 将测试生产者是否成功完成。在注释 (6) 中,我们正在测试接收到的总发射计数为 20(列表中有 20 个项目),assertValuesCount() 帮助我们实现这个目标。在注释 (6) 中,我们通过 assertValues() 测试每个发射的预期和实际值及其顺序。
所以,这很酷,对吧?接下来我要展示的可能会更酷。
理解 TestScheduler
想象一下使用 Observable.interval() / Flowable.interval() 工厂方法创建的 Observable/Flowable。如果你在其中给出了一个较长的间隔(比如说五分钟),并且至少测试了 100 次发射,那么测试完成将需要很长时间(500 分钟 = 8.3 小时,也就是说,仅仅为了测试一个生产者就需要一个完整的人时)。现在,如果你有更多类似这样的生产者,它们有更大的间隔和更多的发射需要测试,那么测试可能需要整个生命周期,那么你什么时候才能发货呢?
TestScheduler 的存在就是为了拯救你的生命。它们可以通过时间驱动的生产者有效地模拟时间,这样我们就可以通过向前快进特定的时间量来进行断言。
因此,以下是对应的实现:
@Test
fun `test by fast forwarding time`() {
val testScheduler = TestScheduler()
val observable =
Observable.interval(5,TimeUnit.MINUTES,testScheduler)
val testObserver = TestObserver<Long>()
observable.subscribe(testObserver)
testObserver.assertSubscribed()
testObserver.assertValueCount(0)//(1)
testScheduler.advanceTimeBy(100,TimeUnit.MINUTES)//(2)
testObserver.assertValueCount(20)//(3)
testScheduler.advanceTimeBy(400,TimeUnit.MINUTES)//(4)
testObserver.assertValueCount(100)//(5)
}
因此,在这里我们使用 Observable.interval 创建了一个具有 5 分钟间隔的 Observable,并将其 Scheduler 设置为 TestScheduler。
在评论 (1) 上,它不应该收到任何排放(因为还有 5 分钟它才应该收到第一次排放),我们使用 assertValuesCount(0) 进行测试。
我们然后在评论 (2) 上快进了 100 分钟,并测试了是否在评论 (3) 上收到了 20 次排放。TestScheduler 为我们提供了 advanceTimeBy 方法,该方法接受时间段和单位作为参数,并为我们模拟这一过程。
我们然后又快进了 400 分钟,并测试了是否在评论 (4) 和评论 (5) 上总共收到了 100 次排放。
如您所预期,测试通过了。
摘要
因此,在本章中,我们学习了 Kotlin 中的测试。我们从测试的好处开始,然后转向 Kotlin 中的测试,使用 JUnit 和 Kotlin-test 进行测试。
由于我们在 Kotlin 中获得了一些实际测试经验,我们逐渐转向了 RxKotlin 的测试,我们学习了一些测试 RxKotlin 的技巧,并了解了 RxKotlin 为我们提供的超级方便的测试工具。
由于我们在 RxKotlin 中建立了坚实的知识基础,在下一章——第九章,
在 资源管理和扩展 RxKotlin 中,我们将讨论一些高级主题。我们将讨论如何管理资源——如何释放分配的内存和防止内存泄漏。我们还将学习如何创建自己的自定义操作符,这些操作符可以在 RxKotlin 逻辑中像预定义的操作符一样链式使用。
那么,你还在等什么?现在就开始学习 第九章,资源管理和扩展 RxKotlin,并且从现在开始,不要忘记测试你写的每一行代码。
第九章:资源管理和扩展 RxKotlin
到目前为止,你已经学习了关于 Observables、Flowables、Subjects、处理器、操作符、组合生产者、测试以及许多其他内容。我们已经获得了开始编码应用程序所需的大部分必要知识。唯一剩下要关注的话题是资源管理——创建、访问和清理资源的技术。此外,如果你是那些渴望挑战的开发者之一,那么你总会寻找定制一切的方法。到目前为止,在这本书中,我们已经看到了如何按照规定的方式使用操作符。我们没有做任何创新,也没有尝试自定义操作符。因此,本章致力于资源管理和通过自定义操作符扩展 RxKotlin。
以下列表包含本章我们将要讨论的主题:
-
使用
using方法进行资源管理 -
使用
lift操作符创建自定义操作符 -
使用
compose操作符创建自定义转换器(转换操作符)
因此,首先,让我们从资源管理开始。
资源管理
资源管理,这是什么意思?为什么我们应该关心它?如果你在 Java、Kotlin、JavaScript 或任何其他语言的 应用程序开发中有一点经验,那么你可能熟悉这样一个事实:在开发应用程序时,我们经常需要访问资源,并且在完成时必须关闭它们。
如果你对这个短语不熟悉,那么资源管理,让我们来分解一下。我们将从探索资源的定义开始。
那么,什么是资源?在开发应用程序时,你可能经常需要访问 API(通过 HTTP 连接)、访问数据库、从文件中读取/写入,或者你可能甚至需要访问任何 I/O 端口/套接字/设备。所有这些事情在一般情况下都被认为是 资源。
为什么我们需要管理/关闭它们?每当我们在访问资源时,尤其是写入时,系统通常会为我们锁定它,并阻止其他程序访问它。如果你在完成时没有释放或关闭资源,系统性能可能会下降,甚至可能出现死锁。即使系统没有为我们锁定资源,它也会保持打开状态,直到我们释放或关闭它,从而导致性能下降。
因此,每当我们在使用完资源后,我们必须关闭或释放它。
通常,在 JVM 上,我们通过一个类来访问资源。通常,这个类实现了 Closable 接口,通过调用其 close 方法,使我们能够轻松地释放资源。在命令式编程中这相当简单,但你可能想知道如何在响应式编程中做到这一点。
你可能正在考虑将命令式编程与响应式编程混合,并将资源作为全局属性,然后在 subscribe 方法中使用后将其丢弃。这基本上就是我们第五章中所做的。
异步数据运算符和转换 HTTP 请求。
很抱歉让你失望,但这确实是错误的过程;在第五章,
异步数据运算符和转换,我们这样做是为了避免进一步的复杂性,以便你更好地理解代码,但现在我们应该学习正确的方法。
为了使事情不那么复杂,我们将创建一个具有自定义Closable接口实现的虚拟资源。所以,不再有悬念;看看下面的代码片段:
class Resource():Closeable {
init {
println("Resource Created")
}
val data:String = "Hello World"
override fun close() {
println("Resource Closed")
}
}
在前面的代码中,我们创建了一个Resource类,并在该类中实现了Closeable接口(只是为了模拟一个典型的 Java 资源类)。我们还在该类内部创建了一个名为data的val属性,它将被用来模拟从Resource中获取数据。
现在,我们如何在响应式链中使用它呢?RxKotlin 提供了一个非常方便的方式来处理可丢弃的资源。为了用可丢弃的资源拯救你的生命,RxKotlin 为你准备了一个礼物——using运算符。
using运算符允许你创建一个仅在Observable的生命周期内存在的资源,并且一旦Observable完成,该资源就会被关闭。
以下图表描述了使用using运算符创建的Observable的生命周期与附加的资源之间的关系,该图表取自 ReactiveX 文档(reactivex.io/documentation/operators/using.html):

前面的图像清楚地显示,资源仅在Observable的生命周期内存在——一个完美的伴侣,不是吗?
下面是using运算符的定义:
fun <T, D> using(resourceSupplier: Callable<out D>, sourceSupplier:
Function<in D, out ObservableSource<out T>>,
disposer: Consumer<in D>): Observable<T> {
return using(resourceSupplier, sourceSupplier, disposer, true)
}
看起来很复杂,但当我们分解它时,就很容易了。using方法接受一个Callable实例,该实例将创建一个资源并将其返回(out D就是为了这个目的)。然后,最后一个是释放/关闭资源。using运算符将在创建Resource实例之前调用第一个 lambda。然后,它将Resource实例传递给第二个 lambda,以便你创建Observable并返回它,这样你就可以订阅了。最后,当Observable调用其onComplete事件时,它将调用第三个 lambda 来关闭resource。
你现在迫不及待地想看到示例,对吧?以下是一个示例:
fun main(args: Array<String>) {
Observable.using({//(1)
Resource()
},{//(2)
resource:Resource->
Observable.just(resource)
},{//(3)
resource:Resource->
resource.close()
}).subscribe {
println("Resource Data ${it.data}")
}
}
在前面的程序中,我们向using运算符传递了三个 lambda。在第一个 lambda(注释一)中,我们创建了一个Resource实例并将其返回(在 lambda 中,最后一个语句充当返回,你不需要写它)。
第二个 lambda 将接受resource作为参数,并从它创建Observable以返回。
第三个 lambda 将再次以resource作为参数,并关闭它。
using运算符会返回你在第二个 lambda 中创建的Observable,以便你可以将其应用于 RxKotlin 链。
所以,这里有一个输出截图,如果你好奇的话:

因此,这就是资源管理变得简单。同时请注意,你可以创建并传递任意数量的资源到using操作符。我们为了便于理解实现了Closable接口,但这不是强制的;你可以轻松地创建并传递一个资源数组。
创建自己的操作符
到目前为止,我们已经使用了大量的操作符,但我们能确定它们能满足所有我们的需求吗?或者,我们是否总能找到适合每个我们面对的需求的操作符?不,这是不可能的。有时,我们可能必须为我们的需求创建自己的操作符,但如何做呢?
RxKotlin 始终致力于让你的生活更轻松。它有一个专门为此目的的操作符——lift操作符。lift操作符接收一个ObservableOperator的实例;因此,要创建自己的操作符,你必须实现该接口。
在我看来,学习某样东西的最佳方式是通过实践。那么,创建一个自定义操作符,为每个排放添加一个序列号,怎么样?让我们根据以下要求列表来创建它:
-
该操作符应该发射一个对,其中添加的序列号作为第一个元素。对中的第二个元素应该是实际的排放。
-
该操作符应该是泛型的,并且应该与任何类型的 Observable 一起工作。
-
与其他操作符一样,该操作符应该与其他操作符并发工作。
前面的点是我们的基本要求;根据前面的要求,我们必须使用AtomicInteger作为计数器(它将计算排放量,我们将这个计数作为序列号传递),这样操作符才能与任何Scheduler无缝工作。
每个自定义操作符都应该实现ObservableOperator接口,其外观如下:
interface ObservableOperator<Downstream, Upstream> {
/**
* Applies a function to the child Observer and returns a new
parent Observer.
* @param observer the child Observer instance
* @return the parent Observer instance
* @throws Exception on failure
*/
@NonNull
@Throws(Exception::class)
fun apply(@NonNull observer: Observer<in Downstream>):
Observer<in Upstream>;
}
Downstream和Upstream是这里的两个泛型类型。Downstream指定了将传递给操作符的Downstream的类型,而Upstream指定了操作符将从upstream接收的类型。
apply函数有一个名为Observer的参数,应该用来将排放传递给Downstream,并且函数应该返回另一个Observer,它将用于监听upstream的排放。
足够的理论了。以下是我们AddSerialNumber操作符的定义。请在这里仔细看看:
class AddSerialNumber<T> : ObservableOperator<Pair<Int,T>,T> {
val counter:AtomicInteger = AtomicInteger()
override fun apply(observer: Observer<in Pair<Int, T>>):
Observer<in T> {
return object : Observer<T> {
override fun onComplete() {
observer.onComplete()
}
override fun onSubscribe(d: Disposable) {
observer.onSubscribe(d)
}
override fun onError(e: Throwable) {
observer.onError(e)
}
override fun onNext(t: T) {
observer.onNext(Pair(counter.incrementAndGet(),t))
}
}
}
}
让我们从第一个特性开始描述——AddSerialNumber类的定义。这个类实现了ObservableOperator接口。根据我们的要求,我们保持了类的泛型性,即我们指定了Upstream类型为泛型T。
我们使用了一个AtomicInteger作为类的val属性,它应该在init块中初始化(因为我们是在类内部声明和定义属性,所以它会在创建类的实例时自动在init中初始化)。这个AtomicInteger,即counter,应该在每次发射时增加,并返回发射值的序列号。
在apply方法中,我创建并返回了一个Observer实例,它将被用来监听前面描述的upstream。基本上,每个操作符都会通过传递一个Observer给upstream来接收事件。
在那个observer内部,每当收到任何事件,我们都会将其回声到下游的Observer(在那里它作为一个参数被接收)。
在Upstream Observer的onNext事件中,我们增加了counter的值,将其作为Pair实例的第一个元素,将接收到的项目(作为onNext中的参数)作为第二个值添加,最后将其传递给下游的onNext——observer.onNext(Pair(counter.incrementAndGet(),t))。
那么,接下来是什么?我们创建了一个可以用作操作符的类,但我们如何使用它?很简单,看看这段代码:
fun main(args: Array<String>) {
Observable.range(10,20)
.lift(AddSerialNumber<Int>())
.subscribeBy (
onNext = {
println("Next $it")
},
onError = {
it.printStackTrace()
},
onComplete = {
println("Completed")
}
)
}
你只需要创建你操作符的一个实例,并将其传递给lift操作符;这就是你所需要的一切,我们现在已经创建了我们的第一个操作符。
看看下面的输出:

我们已经创建了我们的第一个操作符,坦白说,这非常简单。是的,一开始可能有点令人困惑,但随着我们继续前进,它变得更容易了。
正如你可能已经注意到的,ObservableOperator接口只有一个方法,因此我们可以显然用 lambda 替换类声明以及一切,如下所示:
fun main(args: Array<String>) {
listOf("Reactive","Programming","in","Kotlin",
"by Rivu Chakraborty","Packt")
.toObservable()
.lift<Pair<Int,String>> {
observer ->
val counter = AtomicInteger()
object :Observer<String> {
override fun onSubscribe(d: Disposable) {
observer.onSubscribe(d)
}
override fun onNext(t: String) {
observer.onNext(Pair(counter.incrementAndGet(), t))
}
override fun onComplete() {
observer.onComplete()
}
override fun onError(e: Throwable) {
observer.onError(e)
}
}
}
.subscribeBy (
onNext = {
println("Next $it")
},
onError = {
it.printStackTrace()
},
onComplete = {
println("Completed")
}
)
}
在这个例子中,我们使用了一个String列表来创建Observable,而不是使用Int范围。
下面的就是输出:

程序几乎与上一个类似,只是我们使用了一个 lambda,并使用Pair<Int,String>作为下游Observer的类型。
由于我们已经掌握了创建自定义操作符的技巧,让我们继续学习如何创建转换器——不,不是像电影系列中的机器人;它们只是 RxKotlin 转换器。它们是什么?让我们看看。
使用转换器组合操作符
因此,你已经学会了如何创建自定义操作符,但想想当你想要通过组合多个操作符来创建一个新操作符的情况。例如,我经常想要组合subscribeOn和observeOn操作符的功能,以便将所有计算推送到计算线程,当结果准备好时,我们可以在主线程上接收它们。
是的,可以通过将两个操作符一个接一个地添加到链中,来获得两个操作符的好处,如下所示:
fun main(args: Array<String>) {
Observable.range(1,10)
.map {
println("map - ${Thread.currentThread().name} $it")
it
}
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.io())
.subscribe {
println("onNext - ${Thread.currentThread().name} $it")
}
runBlocking { delay(100) }
}
尽管你已经知道了输出结果,以下截图如果你需要刷新记忆的话:

现在,假设我们项目中有subscribeOn和observeOn操作符的组合,所以我们想要一个快捷方式。我们想要创建自己的操作符,其中我们将传递两个Scheduler,我们想要subscribeOn和observeOn,并且一切都应该完美工作。
RxKotlin 提供了Transformer接口(ObservableTransformer和FlowableTransformer是两个Transformer接口)用于此目的。就像operator接口一样,它只有一个方法——apply。唯一的区别是,在这里,你处理的是Observable而不是Observers。所以,在这里,你直接在源上操作,而不是操作单个发射和它们的项。
这是ObservableTransformer接口的签名:
interface ObservableTransformer<Upstream, Downstream> {
/**
* Applies a function to the upstream Observable
and returns an ObservableSource with
* optionally different element type.
* @param upstream the upstream Observable instance
* @return the transformed ObservableSource instance
*/
@NonNull
fun apply(@NonNull upstream: Observable<Upstream>):
ObservableSource<Downstream>
}
接口签名几乎相同。与ObservableOperator的apply方法不同,这里的apply方法接收Upstream Observable并应返回应传递给Downstream的Observable。
所以,回到我们的主题,以下代码块应该满足我们的要求:
fun main(args: Array<String>) {
Observable.range(1,10)
.map {
println("map - ${Thread.currentThread().name} $it")
it
}
.compose(SchedulerManager(Schedulers.computation(),
Schedulers.io()))
.subscribe {
println("onNext - ${Thread.currentThread().name} $it")
}
runBlocking { delay(100) }
}
class SchedulerManager<T>(val subscribeScheduler:Scheduler,
val observeScheduler:Scheduler):ObservableTransformer<T,T> {
override fun apply(upstream: Observable<T>):
ObservableSource<T> {
return upstream.subscribeOn(subscribeScheduler)
.observeOn(observeScheduler)
}
}
在前面的代码中,我们创建了一个用于我们需求的类——SchedulerManager——它将接受两个Scheduler作为参数。第一个是要传递给subscribeOn操作符的,第二个是要传递给observeOn操作符的。
在apply方法内部,我们在对其应用两个操作符之后返回了Observable Upstream。
我们省略了输出截图,因为它与上一个截图相同。
就像lift操作符一样,compose操作符也可以使用 lambda 函数实现。让我们再举一个例子,我们将把Observable<Int>转换成Observable<List>。以下是代码:
fun main(args: Array<String>) {
Observable.range(1,10)
.compose<List<Int>> {
upstream: Observable<Int> ->
upstream.toList().toObservable()
}
.first(listOf())
.subscribeBy {
println(it)
}
}
在前面的代码中,我们使用了upstream.toList().toObservable()作为Observable$toList()操作符,因为它将Observable<T>转换为Single<List<T>>,所以我们需要toObservable()操作符将其转换回Observable。
这是输出截图:

在 RxKotlin 中,组合多个操作符以创建一个新的操作符也非常简单;只需给它添加一个扩展函数,就可以看到事情变得更加愉快。
摘要
这是一章关于 RxKotlin 中资源管理和自定义操作符的简短章节。你学习了如何(或应该)创建、使用和销毁资源。你学习了如何创建自定义操作符。你还学习了如何组合多个操作符以创建你想要的操作符。
这就是关于 RxKotlin 基础知识的最后一章。从下一章开始,我们将开始将我们获得的知识应用到实际场景和项目中。
在今天以应用驱动时代,编写 API 是一个基本要求;在下一章,你将开始学习 Kotlin 中的 Spring,这样你就可以为你的项目开发自己的 API。
第十章:为 Kotlin 开发者介绍使用 Spring 进行 Web 编程
Kotlin 是一种强大的语言,当与 Spring 框架结合使用时,其力量更是倍增。到目前为止,你已经学习了响应式编程的概念以及如何将这些概念应用到 Kotlin 中。到目前为止,我们开发和编写的代码是与控制台交互的,但在开发专业应用程序时,我们不会这样做。我们将构建在移动设备上运行的应用程序,或者构建 Web 应用程序或 REST API。至少,这些都是最常见的专业软件解决方案。
那么,如何构建它们?如何创建 RESTful Web API 和 Android 应用程序?让我们来探索。本书的最后一章将致力于构建 REST API 和 Android 应用程序,最重要的是,使它们具有响应性。Spring 是一个如此广泛的话题,以至于在一个章节中涵盖它是不可能的,因此我们将有两个章节专门介绍 Spring。
本章将从介绍 Spring 开始,到本章结束时,你应该足够熟练,能够使用 Spring 在 Kotlin 中编写 REST API。我们不会在本章中添加响应式功能,因为我们不想让你分心于 Spring 的概念和思想。我们希望你在继续进行使它们具有响应性之前,能够充分掌握 Spring 的概念和知识。
在本章中,我们将涵盖以下主题:
-
Spring 简介,Spring 的历史和起源
-
Spring IoC 和依赖注入
-
Spring 中的面向方面编程
-
Spring Boot 简介
-
使用 Spring Boot 构建 REST API
那么,我们还在等什么呢?让我们开始,熟悉 Spring。
Spring,Spring 的历史和起源
Spring 是什么?我们无法给出简短的答案。用一句话或两句话来定义 Spring 真的是很难。许多人可能会说 Spring 是一个框架,但这对于 Spring 来说也是一种低估,因为它也可能被称为“框架的框架”。Spring 为你提供了许多工具,例如 DI(依赖注入)、IoC(控制反转)和 AOP(面向方面编程)。虽然我们可以在几乎任何类型的 Java 或 Kotlin JVM 应用程序中使用 Spring,但在基于 Java EE 平台开发 Web 应用程序时,它最有用。在深入了解 Spring 之前,我们应该首先了解 Spring 的起源和为什么它会出现,以及它是如何演变的。
Spring 的起源和历史
自 Java 诞生以来已经超过二十年了(大约 22 年)。对于企业级应用开发,Java 引入了一些重量级且非常复杂的技术。
在 2003 年,Rod Johnson 创建了 Spring,作为一种替代重量级和复杂的 Enterprise Java 技术(EJB)的方案,以便在 Java 中轻松开发企业应用程序。由于 Spring 轻量级、灵活且易于使用,它很快获得了人气。随着时间的推移,EJB 和 Java 企业版(当时称为 J2EE)演变为支持以 POJO 为导向的编程模型,如 Spring。不仅如此,可以说受到 Spring 的启发,EJB 也开始提供 AOP、DI 和 IoC。
然而,Spring 从未回头。随着 EJB 和 Java EE 开始包括受 Spring 启发的想法,Spring 开始探索更多非常规和未探索的技术领域,如大数据、云计算、移动应用开发,甚至反应式编程,将 EJB 和 Java EE 远远抛在后面。
在年初的 2017 年 1 月,Spring 通过宣布支持 Kotlin(是的,他们甚至在 Google 之前宣布了 Kotlin 支持)并发布了一些 Kotlin API,让所有人都感到惊讶。而且,当 Kotlin 的力量与已经强大的 Spring 框架结合时,两者都变得更加强大。他们添加 Kotlin 支持的原因如下:
Kotlin 的一个关键优势是它提供了与用 Java 编写的库非常好的互操作性。但是,有方法可以更进一步,允许在开发下一个 Spring 应用程序时编写完全符合 Kotlin 习惯的代码。除了 Kotlin 应用程序可以利用的 Spring 框架对 Java 8 的支持,如函数式 Web 或 Bean 注册 API 之外,还有额外的 Kotlin 专用功能,这应该允许你达到新的生产力水平。
正因如此,我们在 Spring 框架 5.0 中引入了专门的 Kotlin 支持。
由 Pivotal Spring 团队 spring.io/blog/2017/01/04/introducing-kotlin-support-in-spring-framework-5-0。
因此,让我们首先创建并设置我们的 Spring 项目。
依赖注入和 IoC
控制反转(IoC)是一种编程技术,其中对象耦合在运行时由一个组装对象绑定,通常在编译时通过静态分析是不可知的。IoC 可以通过依赖注入来实现。我们可以这样说,IoC 是一个理念,而依赖注入是其实现。那么,什么是依赖注入呢?让我们来了解一下。
依赖注入是一种在实例化时一个组件为另一个组件提供依赖的技术。这个定义听起来很复杂,对吧?让我们用一个例子来解释它。考虑以下接口:
interface Employee {
fun executeTask()
}
interface Task {
fun execute()
}
上述程序的一个常见实现如下。
Employee 类如下:
class RandomEmployee: Employee {
val task = RandomTask()
override fun executeTask() {
task.execute()
}
}
Task 接口实现如下:
class RandomTask : Task {
override fun execute() {
println("Executing Random Task")
}
}
然后,我们将在 main 方法中创建并使用 RandomEmployee 类的实例,如下所示:
fun main(args: Array<String>) {
RandomEmployee().executeTask()
}
RandomTask类是一个简单的类,实现了名为Task的接口,该接口有一个名为execute的函数。另一方面,RandomEmployee类依赖于Task类。那么,我们所说的依赖是什么意思呢?通过依赖,我们指的是Employee类实例的输出依赖于Task类。
让我们看看以下输出:

前面的程序将正常工作,实际上,它是一个教科书程序。在大学/学院,当我们第一次学习编码时,我们学习了在构造函数内部或构建时初始化变量和/或属性的方法。
现在,试着回忆一下你之前几章学到的内容。我们应该测试我们所写的一切。现在,再次看看代码——这段代码是否可测试?或者甚至可维护?你将如何确保正确的Employee被分配了正确的Task?这是一个紧密耦合的代码。
你应该始终使用简洁的耦合。诚然,没有耦合我们无法取得很大成就。另一方面,紧密耦合的代码使得测试和维护变得困难。
而不是让对象在构建时创建它们的依赖关系,依赖注入在创建时通过某个第三方类为对象提供它们的依赖关系。这个第三方类也将与系统中的每个对象进行协调。以下图表展示了依赖注入背后的基本思想:

这张图片清楚地描述了依赖注入的流程。将有一个 Config 类(在 Spring 中,可以有一个 XML 配置文件,也可以有一个 Config 类)来创建和驱动 Bean 容器。该 Bean 容器将控制 bean 或 POJO 的创建,并将它们传递到所需的位置。
感到困惑?让我们动手编写代码并实现前面的概念。让我们从Employee接口的新实现开始,如下所示:
class SoftwareDeveloper(val task: ProgrammingTask) : Employee {
override fun executeTask() {
task.execute()
}
}
SoftwareDeveloper类只能执行ProgrammingTask。现在,看看下面的 XMLconfig文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="employee"
class="com.rivuchk.reactivekotlin.springdi.SoftwareDeveloper">
<constructor-arg ref="task"/>
</bean>
<bean id="task" class="com.rivuchk.reactivekotlin.
springdi.ProgrammingTask"/>
</beans>
ProgrammingTask类是Task接口的新实现,如下所示:
class ProgrammingTask: Task {
override fun execute() {
println("Writing Programms")
}
}
此文件应位于\src\main\resources\META-INF\employee.xml。现在,让我们尝试理解config文件。我们使用bean标签声明了每个 bean。然后,我们使用constructor-arg标签来指示该 bean 中的构造函数参数。
如果你想在 bean 中将另一个对象作为constructor-argref传递,你必须将该引用对象声明为 bean。或者,你可以像本章后面讨论的那样传递constructor-arg value。
更新的main函数将如下所示:
fun main(args: Array<String>) {
val context = ClassPathXmlApplicationContext( "META-INF/spring/employee.xml")//(1) val employee =
context.getBean(Employee::class.java)//(2)
employee.executeTask()
context.close()//(3)
}
在深入了解前面程序的细节之前,让我们先看看它的输出:

DI 与 XML 配置程序的裁剪输出
前几行红色的输出是 Spring 框架的日志。然后,我们可以看到输出为 Writing Programms。
现在,让我们尝试理解这个程序。ClassPathXmlApplicationContext是我们图中提到的 Bean 容器。它创建并记录 XML 文件中提到的所有 bean,并在需要时提供给我们。传递给ClassPathXmlApplicationContext构造函数的String是 XML 配置文件的相对路径。
在注释(2)中,我们使用了context.getBean()来获取Employee实例。这个函数接受一个类名作为参数,并根据 XML 配置创建该类的实例。
在注释(3)中,我们关闭了context。context作为一个 Bean 容器,始终为你携带配置,这会阻塞内存。为了清理内存,我们应该关闭context。
现在,既然我们已经对通过 XML 配置文件进行依赖注入有了些了解,我们应该转向基于注解的配置类,看看它是如何工作的。
Spring 注解配置
除了 XML 之外,我们还可以在 POJO 类中通过注解定义 Spring 配置,这个类不会被用作 bean。在上一节中,我们以Employee任务为例;现在让我们以Student-Assignment为例,一个类似的例子。然而,这次,我们不会使用接口;而是直接使用类。
因此,这是接受 lambda 作为构造函数参数的Assignment类:
class Assignment(val task:(String)->Unit) {
fun performAssignment(assignmentDtl:String) {
task(assignmentDtl)
}
}
这个类接受一个 lambda 作为task,在performAssignment()方法中稍后执行。以下是接受Assignment作为属性的Student类:
class Student(val assignment: Assignment) {
fun completeAssignment(assignmentDtl:String) {
assignment.performAssignment(assignmentDtl)
}
}
因此,Student将依赖于其Assignment,而Assignment将依赖于其任务定义(Lambda)。以下图描述了此示例的依赖流:

如何在代码中描述这个依赖流?使用注解配置就很容易。以下是我们所使用的Configuration类:
@Configuration
class Configuration {
@Bean
fun student() = Student(assignment())
@Bean
fun assignment()
= Assignment { assignmentDtl -> println
("Performing Assignment $assignmentDtl") }
}
简单直接,不是吗?这个类被注解了@Configuration,返回Student和Assignment对象的函数被注解了@Bean。
现在,如何使用这个类?很简单,就像之前的那个一样,看看这里的main函数:
fun main(args: Array<String>) {
val context = AnnotationConfigApplicationContext
(Configuration::class.java)
val student = context.getBean(Student::class.java)
student.completeAssignment("One")
student.completeAssignment("Two")
student.completeAssignment("Three")
context.close()
}
我们没有使用ClassPathXmlApplicationContext,而是使用了AnnotationConfigApplicationContext并传递了Configuration类。程序的其他部分保持不变。
这是程序的输出:

DI with Annotation Configuration 程序的裁剪输出
因此,我们学习了使用 Spring 的依赖注入。这真的很简单,不是吗?实际上,Spring 框架让一切变得简单;无论他们提供什么功能,他们都让它像从 POJO 类中调用方法一样简单。Spring 真正利用了 POJO 的力量。
因此,既然我们已经掌握了依赖注入,让我们继续学习面向切面的编程。
Spring – AOP
在学习如何使用 Spring 实现面向切面编程之前,我们首先应该了解什么是面向切面编程。面向切面编程的定义表明,它是一种旨在通过允许分离横切关注点来增加模块化的编程范式。它是通过向现有代码添加额外的行为(建议)来实现的,而不修改代码本身。
那么,我们所说的横切关注点是什么意思?让我们来探索一下。
在实际项目中,多个组件扮演着各自的角色。例如,如果我们考虑我们之前的场景,Student类本身就是一个组件,同样可能还有一个评估学生表现的教师组件。因此,让我们在我们的程序中添加一个教师。
Faculty类应该足够简单,只需要一个评估学生的方法。如下所示:
class Faculty {
fun evaluateAssignment() {
val marks = Random().nextInt(10)
println("This assignment is evaluated and given $marks points")
}
}
现在,教师应该如何评分学生?他/她必须以某种方式知道学生已经完成了作业。这种业务逻辑的常见实现方法是通过修改Student类,如下所示:
class Student(val assignment: Assignment,
val faculty: Faculty) {
fun completeAssignment(assignmentDtl:String) {
assignment.performAssignment(assignmentDtl)
faculty.evaluateAssignment()
}
}
Faculty实例将被传递给Student实例,一旦学生完成作业,它将调用Faculty实例并指示它评估作业。然而,再思考一下。这是否是一个合适的实现?为什么学生要指示他的/她的教师?评估学生的作业是教师的工作;它只需要以某种方式得到通知。
那正是所谓的横切关注点。Faculty和Student是程序的不同组件。在作业审查时,它们不应该有直接的交互。
AOP 让我们实现相同的功能。因此,在这里,Student类将恢复到几乎原始的状态:
open class Student(public val assignment: Assignment) {
open public fun completeAssignment(assignmentDtl:String) {
assignment.performAssignment(assignmentDtl)
}
}
你注意到了上一节中Student类实际代码中的差异吗?是的,在这里我们向类声明中添加了open关键字以及类的所有属性和函数。原因是,为了实现 AOP,Spring 会子类化我们的 bean 并覆盖方法(包括我们属性的 getter)。然而,在 Kotlin 中,除非你明确指定为open,否则一切都是最终的,这将阻止 Spring AOP 实现其目的。因此,为了使 Spring 工作,我们必须将每个属性和方法都指定为open。
main方法将类似,只是我们又回到了基于 XML 的配置。看看下面的代码片段:
fun main(args: Array<String>) {
val context = ClassPathXmlApplicationContext(
"META-INF/spring/student_faculty.xml"
)
val student = context.getBean(Student::class.java)
student.completeAssignment("One")
student.completeAssignment("Two")
student.completeAssignment("Three")
context.close()
}
唯一包含新内容的文件是配置文件。在我们解释它之前,看看配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="student" class="com.rivuchk.reactivekotlin.
springdi.aop_student_assignment.Student">
<constructor-arg ref="assignment"/>
</bean>
<bean id="assignment" class="com.rivuchk.reactivekotlin.springdi.
aop_student_assignment.Assignment" />
<bean id="faculty"
class="com.rivuchk.reactivekotlin.springdi.aop_student_assignment.
Faculty" /><!--1--> <aop:config><!--2--> <aop:aspect
ref="faculty"><!--3--> <aop:pointcut
id="assignment_complete"
expression="execution(* *.completeAssignment(..))"/><!--4--> <aop:after pointcut-ref="assignment_complete" method="evaluateAssignment" /><!--5-->
</aop:aspect>
</aop:config>
</beans>
因此,让我们解释一下配置。在注释(1)中,我们声明了一个名为faculty的新 bean,尽管这对你来说并不是什么新鲜事,你可能已经预料到了。我提到它是为了让你为接下来的几行代码做准备。
在评论 (2) 中,我们指出了 AOP 配置的开始。在评论 (3) 中,我们指出了这个 AOP 与 Faculty 类相关,因为 Faculty 类是应该得到通知的类。
在评论 (4) 中,我们声明了 pointcut。pointcut 就像方法上的书签,所以每当该方法被调用时,你的类都应该得到通知。id 字段表示该 pointcut 的 id,这样你就可以在代码中引用它。expression 字段表示我们应该为它创建 pointcut 的表达式。在这里,使用执行表达式,我们声明 pointcut 应该在 completeAssignment 方法的执行时触发。
在评论 (5) 中,我们声明了在 Faculty 类中应该调用的方法,该方法应在 pointcut 表达式执行后调用。我们也可以使用 aop:before 声明一个在 pointcut 之前执行的方法。
现在,让我们看看以下输出:

使用 Spring AOP 程序的 DI 剪影输出
如你所见,每次我们调用 completeAssignment 方法时,都会从 Faculty 类中调用 evaluateAssignment 方法,显然,这是没有代码,只有配置。
Spring Boot 简介
因此,我们现在已经熟悉了 Spring,特别是 Spring DI 和 AOP。Spring Boot 使开发者的生活变得更简单。到目前为止,我们已经看到了如何仅通过使用 POJO 类和 Spring 配置来执行各种操作。如果告诉你我们可以进一步减少配置,你会作何反应?你会震惊吗?那么,请做好准备,因为这是真的。使用 Spring Boot,你可以通过最少的配置和几个步骤来准备你的代码。
那么,Spring Boot 是什么?它是一个为 Spring 框架提供 RAD(快速应用开发)功能的 Spring 模块。它旨在简化新 Spring 应用的启动和开发。该框架采用了一种有观点的配置方法,使开发者免于定义样板配置,从而进一步减少开发时间。
那么,让我们开始吧。如果你使用的是 IntelliJ IDEA Ultimate 版本,你可以按照以下步骤创建一个 Spring Boot 应用程序:
-
开始一个新项目。
-
从新项目对话框中,选择 Spring Initializr,定义项目 SDK,然后点击下一步,如图所示:

- 在下一屏中,定义 Group、Artifact、Type(Gradle 或 Maven)、Language(Java/Kotlin)、Packaging(Jar/War)、Java Version、Name 和项目的根包,如图所示:

- 下一屏允许你选择多个 Spring 依赖项。确保在此屏幕上将 Spring Boot 版本设置为 2.0.0 M6 或更高版本。对于 AOP 和 DI,你需要在核心部分下选择 Aspects,如图所示:

- 提供项目名称和位置,然后点击完成。
难道不是很简单吗?如果你没有 IntelliJ IDEA Ultimate,请不要沮丧。Spring Boot 是面向每个人的。按照以下步骤,使用你拥有的任何 IDE 创建一个新的 Spring Boot 项目:
-
前往
start.spring.io/。 -
提供以下详细信息,这些与 IntelliJ IDEA 类似:

- 点击生成项目。项目将被下载到你的机器上。
难道不是足够简单了吗?让我们尝试使用 Spring 创建 API。
使用 Spring Boot 创建 Rest API
我们已经看到了 Spring 和 Spring Boot 的力量。所以,让我们毫不拖延地使用它。我们将构建一个 RESTful 网络服务,该服务将返回一个 Todo 对象。我们将在下一章进一步增强这个项目,我们将添加 Todo 并从数据库中获取 Todo 列表。我们将为此目的使用 JPA 和 Hibernate,以及 Spring。
当我们完成这个示例时,我们应该得到以下响应:

浏览器输出的裁剪截图
那么,让我们先创建一个新的项目。你可以使用 start.spring.io/,或者你也可以使用 IntelliJ IDEA 来创建一个新项目。
在你创建了新项目之后,你会看到有一个 Application 类;不要过多关注它,它在几乎所有的 Spring Boot 应用程序中都有。我们需要创建一个新的类用于 Todo,如下所示:
data class Todo (
var id:Int = 0,
var todoDescription:String,
var todoTargetDate:String,
var status:String
)
REST API 要求我们创建 RestController,这将作为 API 请求的端点,所以这里是我们的 RestController:
@RestController@RequestMapping("/api")
class TodoController {
@RequestMapping("/get_todo")
fun getTodo() = Todo(1,"TODO Project","31/11/2017","Running")
}
仔细研究这个小类。首先,我们用 @RestController 和 @RequestMapping 注解了我们的类。它们的目的很简单:@RestController 表示这个类将作为 Controller,也就是说,所有的 API 请求都应该通过这个类,@RequestMapping("/api") 表示这个类的 URL 将在基本 URL 后添加一个 /api 后缀(注意截图中的 URL 是 http://127.0.0.1:8080/api/get_todo)。如果我们想省略第二个注解,也是可以的。
然后,我们有 getTodo() 函数;这个方法需要 @RequestMapping 注解,因为它将定义端点。这个方法也很简单——它只是返回一个静态创建的 Todo 新对象。
什么?你期待更多吗?很抱歉让你失望,但我们已经完成了 API。你只需运行项目,然后访问 http://127.0.0.1:8080/api/get_todo 来获取以下 JSON 响应:
{"id":1,"todoDescription":"TODO
Project","todoTargetDate":"31/11/2017","status":"Running"}
这不是足够简单了吗?
摘要
在本章中,你通过 Kotlin 了解了 Spring。我们学习了依赖注入和面向切面编程。我们学习了如何通过 Spring 框架,一个简单的 POJO 类可以展现出巨大的力量。在本章中,我们还学习了如何使用 Spring 创建简单的 API。
在下一章中,我们将专注于使用 JPA 和 Hibernate 来增强我们的 API,使其成为一个功能齐全的 API,以便与 MySQL 数据库协同工作。我们还将学习如何使用 Spring 实现响应式编程。
因此,不要等待!立即前往下一章。我们的 API 仍然不完整。
第十一章:使用 Spring JPA 和 Hibernate 的 REST APIs
在上一章中,我们学习了如何轻松地创建 REST API。我们学习了如何利用 Spring、Hibernate 和 JPA 的力量,用一行代码就能创建 REST API。那些是强大的 REST API,但它们不是反应式的。本书的主要关注点是教您如何使一切具有反应性,并教您如何创建非阻塞的应用程序和 API。
那么,让我们继续前进。让我们使我们的 REST API 具有反应性。由于 Spring 的强大功能,这一章将会很短。我们将涵盖以下主题:
-
Spring Boot 与 JPA 和 Hibernate
-
使用 Reactor 进行反应式编程
那么,让我们开始使用 Reactor 框架吧。
使用 Spring Boot、Hibernate 和 JPA 的 REST API
在上一章中,我们看到了如何创建静态的 RESTful API。现在,我们将学习如何根据 API 请求操作数据库记录。我在这个项目中使用了 MySQL 作为数据库。
在这个项目中,我们将使用 JPA。您可以启动一个新的项目,并将 JPA 作为其中一个依赖项添加。或者,您可以将此添加到您的 Gradle 依赖项列表中:
compile('org.springframework.boot:spring-boot-starter-data-jpa')
注意:您不需要在此处放置版本和工件,它们将由 Spring Gradle 插件和 Spring Boot 自动管理。
现在,由于您添加了依赖项,您必须添加 application.properties 文件。转到资源文件夹,添加一个名为 application.properties 的文件,并包含以下内容:
## Spring DATASOURCE (DataSourceAutoConfiguration &
DataSourceProperties)
spring.datasource.url = jdbc:mysql://localhost:3306/tododb
spring.datasource.username = root
spring.datasource.password = password
## Hibernate Properties
# The SQL dialect makes Hibernate generate better
SQL for the chosen database
spring.jpa.properties.hibernate.dialect =
org.hibernate.dialect.MySQL5Dialect
# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update
将 tododb 替换为您的数据库名称,将 root 替换为您的数据库用户名,将 password 替换为您的数据库密码。请注意,您必须在运行此应用程序之前使用提供的数据库名称(在本例中为 tododb)创建一个空数据库。
我们对 Todo 类进行了一点点修改。请看以下代码片段:
@Entity
data class Todo (
@Id @GeneratedValue(strategy = GenerationType.AUTO)
var id:Int = 0,
@get: NotBlank
var todoDescription:String,
@get: NotBlank
var todoTargetDate:String,
@get: NotBlank
var status:String
) {
constructor():this(
0,"","",""
)
}
是的,我们刚刚添加了注解和一个空构造函数,这是 Spring Data 所必需的。那么,让我们看看这些注解及其用途:
@Entity:这定义了数据库中的一个新实体,即对于每个用 @Entity 注解的类,数据库中都会创建一个表。
@Id:这个注解定义了一个表的主键(或多个主键的复合键)。@GeneratedValue 注解表示字段值应该自动生成。JPA 有三种 ID 生成策略,如下所述:
-
GenerationType.TABLE:这表示主键应该由底层表生成以确保唯一性,即创建一个只有一个列和一个行的表,该表将持有下一个主键值,列名为next_val。每次在目标表(用我们的实体创建的表)中插入一行时,主键将被分配next_val的值,而next_val将递增。 -
GenerationType.SEQUENCE:这表示主键应该由底层数据库序列生成。 -
GenerationType.IDENTITY:这表示主键应该由底层数据库的标识符生成。 -
GenerationTypeenum:这也提供了一个额外的选项——GenerationType.AUTO,表示应该自动选择适当的自动生成策略。
下一个注解是@get: NotBlank,表示表中的字段不应为空。
因此,我们完成了对Todo类的更改。我们还需要创建一个Repository接口。请看以下接口:
@Repository
interface TodoRepository: JpaRepository<Todo,Int>
是的,如此简短。@Repository注解表示此接口应作为项目的存储库(DAO类)使用。我们在该接口中实现了JpaRepository,它声明了操作表的方法。此接口的第一个泛型参数是Entity,第二个是ID字段类型的参数。
我们还创建了一个新的类,ResponseModel,以结构化我们的响应 JSON。在此处找到类定义:
data class ResponseModel (
val error_code:String,
val error_message:String,
val data:List<Todo> = listOf()
) {
constructor(error_code: String,error_message:
String,todo: Todo)
:this(error_code,error_message, listOf(todo))
}
此响应模型包含error_code和error_message属性。让我们描述一下;如果在处理 API 请求时出现错误,error_code将包含非零值,而error_message将包含描述该错误的消息。error_message属性也可以包含通用消息。
data属性将包含一个Todo对象的列表,该列表将在响应 JSON 中转换为 JSON 数组。data属性是可选的,因为此响应模型将用于本项目的所有 API,并且并非所有 API 都返回Todo对象的列表或单个Todo对象(例如,编辑、添加和删除待办事项的 API 不需要发送Todo)。
此 API 的最后一部分是controller类。以下是定义:
@RestController
@RequestMapping("/api")
class TodoController(private val todoRepository: TodoRepository) {
@RequestMapping("/get_todo", method =
arrayOf(RequestMethod.POST))
fun getTodos() = ResponseModel("0","", todoRepository.findAll())
@RequestMapping("/add_todo", method =
arrayOf(RequestMethod.POST))
fun addTodo(@Valid @RequestBody todo:Todo) =
ResponseEntity.ok().body(ResponseModel
("0","",todoRepository.save(todo)))
@RequestMapping("/edit_todo", method =
arrayOf(RequestMethod.POST))
fun editTodo(@Valid @RequestBody todo:Todo):ResponseModel {
val optionalTodo = todoRepository.findById(todo.id)
if(optionalTodo.isPresent) {
return ResponseModel("0", "Edit
Successful",todoRepository.save(todo))
} else {
return ResponseModel("1", "Invalid Todo ID" )
}
}
@RequestMapping("/add_todos", method =
arrayOf(RequestMethod.POST))
fun addTodos(@Valid @RequestBody todos:List<Todo>)
= ResponseEntity.ok().body(ResponseModel
("0","",todoRepository.saveAll(todos)))
@RequestMapping("/delete_todo/{id}", method =
arrayOf(RequestMethod.DELETE))
fun deleteTodo(@PathVariable("id") id:Int):ResponseModel {
val optionalTodo = todoRepository.findById(id)
if(optionalTodo.isPresent) {
todoRepository.delete(optionalTodo.get())
return ResponseModel("0", "Successfully Deleted")
} else {
return ResponseModel("1", "Invalid Todo" )
}
}
}
因此,除了get_todo端点之外,我们还添加了add_todo、edit_todo、delete_todo和add_todos端点。我们将逐一仔细研究它们。然而,首先关注TodoController类的构造函数。它接受一个TodoRepository参数,该参数将由 Spring 注解注入。我们在所有 API 中使用todoRepository属性来读取/写入数据库。
现在,更仔细地看看get_todo API。它使用TodoRepository的findAll方法从数据库中获取所有待办事项。以下是该 API 的 JSON 响应(注意,此响应将根据数据库和Todo表的状态而变化):
{
"error_code": "0",
"error_message": "",
"data": [
{
"id": 1,
"todoDescription": "Trial Edit",
"todoTargetDate": "2018/02/28",
"status": "due"
},
{
"id": 2,
"todoDescription": "Added 2",
"todoTargetDate": "2018/02/28",
"status": "due"
},
{
"id": 3,
"todoDescription": "Edited 3",
"todoTargetDate": "2018/02/28",
"status": "due"
},
{
"id": 4,
"todoDescription": "Added 4",
"todoTargetDate": "2018/02/28",
"status": "due"
},
{
"id": 5,
"todoDescription": "Added 5",
"todoTargetDate": "2018/02/28",
"status": "due"
},
{
"id": 7,
"todoDescription": "Added 7",
"todoTargetDate": "2018/02/28",
"status": "due"
}
]
}
下一个 API 是add_todo API:
@RequestMapping("/add_todo", method = arrayOf(RequestMethod.POST))
fun addTodo(@Valid @RequestBody todo:Todo) =
ResponseEntity.ok().body(ResponseModel
("0","",todoRepository.save(todo)))
此 API 从POST请求体中获取一个Todo对象,将其存储并返回一个成功的ResponseModel。以下 Postman 截图显示了发送到 API 的请求:

在 JSON 请求中,我们发送了Todo的所有详细信息,除了 ID,因为id字段将自动生成。
API 的响应如下:
{
"error_code": "0",
"error_message": "",
"data": [
{
"id": 8,
"todoDescription": "Added 8",
"todoTargetDate": "2018/02/28",
"status": "due"
}
]
}
add_todos API 几乎与add_todo API 相同,只是在这里它接受任意数量的要添加到数据库中的Todos。
delete_todo API 与这个项目中所有其他 API 都不同。在这里仔细看看这个 API:
@RequestMapping("/delete_todo/{id}", method =
arrayOf(RequestMethod.DELETE))
fun deleteTodo(@PathVariable("id") id:Int):ResponseModel {
val optionalTodo = todoRepository.findById(id)
if(optionalTodo.isPresent) {
todoRepository.delete(optionalTodo.get())
return ResponseModel("0", "Successfully Deleted")
} else {
return ResponseModel("1", "Invalid Todo" )
}
}
除了POST请求之外,这个 API 接受所有其他 API 的DELETE请求(原因很简单,它只是删除Todo)。
它还从路径变量中获取todo的 ID,而不是RequestBody;同样,原因很简单,我们只需要在这个 API 中获取一个字段,即要删除的Todo的 ID。因此,没有必要将整个 JSON 作为请求体发送。相反,路径变量将非常适合这个 API。
向此 API 发送的示例请求将是此 URL——http://localhost:8080/api/delete_todo/7。API 将检查是否存在指定 ID 的Todo,如果存在,则删除Todo;否则,它将只返回一个错误。
这里是这个 API 的两个理想响应示例:
{
"error_code": "0",
"error_message": "Successfully Deleted",
"data": []
}
如果找到并删除了Todo,您将得到以下响应:
{
"error_code": "1",
"error_message": "Invalid Todo",
"data": []
}
如果找不到指定 ID 的Todo。
现在,既然我们对 Spring 有了些了解,让我们开始学习Reactor,这是由Pivotal——Spring 的守护者——开发的第四代响应式编程库。
使用 Reactor 进行响应式编程
就像ReactiveX框架一样,Reactor也是一个第四代响应式编程库。它允许您编写非阻塞的响应式应用程序。然而,与ReactiveX相比,它有一些显著的不同,如下所示:
-
与支持多个平台和语言的 ReactiveX(例如,RxSwift for Swift、RxJava for JVM、RxKotlin for Kotlin、RxJS for JavaScript、RxCpp for C++等)不同,Reactor 只支持 JVM。
-
如果您有 Java 6+,则可以使用 RxJava 和 RxKotlin。但是,要使用 Reactor,您需要 Java 8 及以上版本。
-
RxJava 和 RxKotlin 不提供与 Java 8 功能 API(如 CompletableFuture、Stream 和 Duration)的直接集成,而 Reactor 则提供了。
-
如果您计划在 Android 中实现响应式编程,您必须使用 RxAndroid、RxJava、RxKotlin(统称为 ReactiveX)或 Vert.X,除非您的最小 SDK 为 Android SDK 26 及以上版本,并且没有官方支持。因为 Reactor 项目在 Android 上没有官方支持,并且它只在 Android SDK 26 及以上版本上运行。
除了这些差异之外,Reactor 和 ReactiveX API 相当相似,所以通过将 Reactor 添加到您的 Kotlin 项目中开始吧。
将 Reactor 添加到您的项目中
如果您使用 Gradle,请将以下依赖项添加到您的项目中:
compile 'io.projectreactor:reactor-core:3.1.1.RELEASE'
如果您使用 Maven,请将以下依赖项添加到POM.xml文件中:
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>
您还可以从central.maven.org/maven2/io/projectreactor/reactor-core/3.1.1.RELEASE/reactor-core-3.1.1.RELEASE.jar下载 JAR 文件。
对于更多选项,请查看mvnrepository.com/artifact/io.projectreactor/reactor-core/3.1.1.RELEASE。
因此,在我们将 Reactor Core 添加到我们的项目之后,让我们开始学习Flux和Mono,Reactor 中的生产者。
理解 Flux 和 Mono
正如我说的,Reactor 是另一个像 ReactiveX 一样的第四代响应式库。它最初是 Rx 的一个轻量级版本;然而,随着时间的推移,它逐渐发展,如今它的重量几乎与 ReactiveX 相当。
它也包含生产者和消费者模块,就像 Rx 一样。它有Flux,类似于Flowable,以及Mono作为Single和Maybe的组合。
注意,当描述Flux时,我说的是 Flowable,而不是 Observable。你可能能猜到原因。是的,所有 Reactor 类型都启用了背压。基本上,所有 Reactor 类型都是 Reactive Streams Publisher API 的直接实现。
Flux 是一个可以发出N个发射并可以成功终止或带有错误的 Reactor 生产者。同样,Mono可能或可能不会发出单个项目。那么,我们还在等什么呢?让我们开始学习Flux和Mono。
考虑以下代码示例:
fun main(args: Array<String>) {
val flux = Flux.just("Item 1","Item 2","Item 3")
flux.subscribe(object:Consumer<String>{
override fun accept(item: String) {
println("Got Next $item")
}
})
}
输出如下:

输出以及程序都与 RxKotlin 非常相似,不是吗?唯一的区别是我们使用Flux而不是Flowable。
那么,让我们来看一个 Mono 的例子。看一下以下示例:
fun main(args: Array<String>) {
val consumer = object : Consumer<String> {//(1)
override fun accept(item: String) {
println("Got $item")
}
}
val emptyMono = Mono.empty<String>()//(2)
emptyMono
.log()
.subscribe(consumer)
val emptyMono2 = Mono.justOrEmpty<String>(null)//(3)
emptyMono2
.log()
.subscribe(consumer)
val monoWithData = Mono.justOrEmpty<String>("A String")//(4)
monoWithData
.log()
.subscribe(consumer)
val monoByExtension = "Another String".toMono()//(5)
monoByExtension
.log()
.subscribe(consumer)
}
在我们逐行描述程序之前,让我们首先关注每个订阅中的log操作符。Reactor 框架理解开发者对记录事物的需求,这就是为什么他们提供了一个操作符,以便我们可以在 Flux 或 Mono 中记录每个事件。
在注释(1)中,在这个程序中,我们创建了一个Consumer实例,用于所有订阅。在注释(2)中,我们使用Mono.empty()工厂方法创建了一个空的 Mono。正如其名称所描述的,这个工厂方法创建了一个空的 Mono。
在注释(3)中,我们使用Mono.justOrEmpty()创建了一个另一个空的Mono;这个方法使用传递的值创建Mono,如果传递的值为 null,则创建一个空的Mono。
在注释(4)中,我们使用相同的工厂方法创建了Mono,但这次传递了一个String值。
在注释(5)中,我们使用toMono扩展函数创建了Mono。
这里是程序的输出:

因此,既然你已经学习了 Spring,你也学习了使用 Reactor 的响应式编程;你愿意自己做一些研究并使我们的 API 变得响应式吗?作为一个帮助的举动,我想建议你稍微学习一下 WebFlux。你也可以阅读由 Oleh Dokuka 和 Igor Lozynskyi 编写的《Spring 5.0 中的响应式编程》(Reactive Programming in Spring 5.0) (www.packtpub.com/application-development/reactive-programming-spring-50)。
摘要
在本章中,我们学习了如何使用 Spring JPA、Hibernate 和 Spring Boot 快速创建 REST API。我们还学习了 Reactor 及其用法。我们为项目创建了 RESTful API,将在下一章创建 Android 应用时使用。
下一章,也就是本书的最后一章,是关于使用 Kotlin 和响应式编程创建 Android 应用。
你即将完成这本书——完成学习《Kotlin 中的响应式编程》。接下来还有一章。所以,快速翻页吧。
第十二章:反应式 Kotlin 和 Android
因此,我们关于 Kotlin 中反应式编程的学习几乎已经完成。我们已经到达了本书的最后一章,但可能是最重要的一章。Android 可能是 Kotlin 最大的平台。在最近的 Google IO—Google IO 17 上,Google 宣布了对 Kotlin 的官方支持,并将 Kotlin 添加为 Android 应用开发的第一个公民。现在,Kotlin 是除了 Java 之外唯一官方支持的 Android 应用开发语言。
反应式编程已经在 Android 中存在——Android 中大多数顶级库都支持反应性。因此,在名为 Reactive Programming in Kotlin 的书中,我们必须涵盖 Android。
从零开始教授 Android 开发超出了本书的范围,因为这是一个庞大的主题。如果您想从头学习 Android 开发,可以找到很多书籍。本书假设您对 Android 应用开发有一些基本知识,并且可以与 RecyclerView、Adapter、Activity、Fragment、CardView、AsyncTask 等一起工作。如果您不熟悉所提到的任何主题,可以阅读 Prajyot Mainkar 的 Expert Android Programming。
那么,您想知道本章为您准备了什么吗?请查看以下我们将涵盖的主题列表:
-
在 Android Studio 2.3.3 和 3.0 中设置 Kotlin
-
在 Android 和 Kotlin 中开始
ToDoApp的开发 -
使用 Retrofit 2 进行 API 调用
-
设置 RxAndroid 和 RxKotlin
-
使用 RxKotlin 与 Retrofit 2
-
开发我们的应用
-
RxBinding 简介简要
那么,让我们开始设置 Android Studio 中的 Kotlin。
在 Android Studio 中设置 Kotlin
我们强烈建议您使用 Android Studio 3.0 进行 Android 开发,无论您是否使用 Kotlin。Android Studio 3.0 是 Android Studio 的最新版本,包含许多错误修复、新功能和改进的 Gradle 构建时间。
对于 Android Studio 3.0,您在创建新项目时只需选择“包含 Kotlin 支持”即可,无需进行任何设置。以下是供您参考的截图:

我们已经突出了 Android Studio—创建 Android 项目对话框中的“包含 Kotlin 支持”部分。
然而,如果您正在使用 Android Studio 2.3.3,请按照以下步骤操作:
-
前往 Android Studio | 设置 | 插件。
-
搜索
Kotlin(查看以下截图)并按照以下步骤安装该插件:

-
开始一个新的 Android 项目。
-
要将 Kotlin 插件应用到项目中,打开项目级别的
build.gradle并修改内容,如下所示:

- 打开您模块中的
build.gradle文件(或者我们也可以说是应用级别的build.gradle文件)并添加以下dependencies:
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
您现在已准备好开始在 Android Studio 中编写 Kotlin 代码。
然而,在开始编写 Kotlin 代码之前,让我们首先回顾一下我们的build.gradle。我之前为 Android Studio 2.3.3 展示的代码对 Android Studio 3.0 同样有效,你只需要不需要手动添加,因为 Android Studio 3.0 会自动为你添加。然而,这些行的目的是什么?让我们检查一下。
在项目级别的build.gradle文件中,ext.kotlin_version = "1.1.51"这一行在 Gradle 中创建了一个名为kotlin_version的变量;这个变量将持有String类型的值,1.1.51(这是撰写本书时的最新版本)。我们在变量中写入这个版本,因为这个版本在项目级别和应用程序级别的build.gradle文件中的多个地方都需要。如果我们只声明一次并在多个地方使用它,那么将会有一致性,并且不会有人为错误的机会。
然后,在同一个文件(项目级别的build.gradle文件)中,我们将添加classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"。这将定义 Gradle 用于在添加它们作为依赖项时搜索kotlin-jre的类路径。
在应用级别的build.gradle文件中,我们将编写implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"。
那么,让我们开始编写 Kotlin 代码。正如我们在上一章中提到的,我们将创建一个ToDoApp。将会有三个屏幕,一个用于ToDo List,一个用于创建ToDo,另一个用于编辑/删除ToDo。
在 Android 上开始使用 ToDoApp
如前所述,我们在这个项目中使用的是 Android Studio 3.0(稳定版)。下面的截图展示了我们使用的项目结构:

在这个项目中,我们使用按功能划分的包,我确实更喜欢在 Android 开发中使用按功能划分的包,主要是因为其可扩展性和可维护性。此外,请注意,在 Android 中使用按功能划分的包是一种最佳实践;尽管如此,你显然可以使用你喜欢的模型。你可以在hackernoon.com/package-by-features-not-layers-2d076df1964d了解更多关于按功能划分的包的信息。
现在,让我们了解在这个应用程序中使用的包结构。这里的根包是com.rivuchk.todoapplication,它是应用程序的包,与applicationId相同。根包包含两个类——ToDoApp和BaseActivity。ToDoApp类扩展了android.app.Application,这样我们就可以有自己的Application类实现。现在,什么是BaseActivity?BaseActivity是在这个项目中创建的一个抽象类,这个项目中的所有活动都应该扩展BaseActivity;因此,如果我们想在项目中的所有活动中实现某些功能,我们可以在BaseActivity中编写代码,并且可以放心,所有活动现在都会实现相同的代码。
接下来,我们有一个apis包,用于与 API 调用相关的类和文件(我们将使用 Retrofit),以及datamodels用于模型(POJO)类。
我们有Utils包用于CommonFunctions和Constants(一个单例Object,用于存储如BASE_URL等常量变量)。
addtodo、tododetails和todolist是三个基于功能的包。todolist包包含用于显示待办事项列表的Activity和Adapter。tododetails包包含负责显示待办事项详细信息的Activity。我们也将使用相同的Activity来编辑。addtodo包包含用于实现添加待办事项功能的Activity。
在开始活动布局之前,我希望你先看看BaseActivity和ToDoApp的内部结构,所以这里是ToDoApp.kt文件中的代码:
class ToDoApp:Application() {
override fun onCreate() {
super.onCreate()
instance = this
}
companion object {
var instance:ToDoApp? = null
}
}
确实是一个小类;它只包含一个companion object来为我们提供实例。随着我们继续本章的学习,这个类将会逐渐增长。我们在清单文件中声明了ToDoApp为这个项目的application类,如下所示:
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:name=".ToDoApp">
....
</application>
BaseActivity现在也很小。与ToDoApp一样,它也会在本章的进程中逐渐增长:
abstract class BaseActivity : AppCompatActivity() {
final override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onCreateBaseActivity(savedInstanceState)
}
abstract fun onCreateBaseActivity(savedInstanceState: Bundle?)
}
目前,BaseActivity只隐藏了Activity类的onCreate方法,并提供了一个新的抽象方法——onCreateBaseActivity。这个类还强制要求我们在子类中重写onCreateBaseActivity,这样我们就可以在所有活动的onCreate方法中实现任何需要的功能,而无需在其他地方实现。
那么,让我们开始处理todolist。这个包包含了显示待办事项列表所需的所有源代码。如果你仔细查看前面的截图,你应该会注意到这个包包含两个类——TodoListActivity和ToDoAdapter。
那么,让我们从TodoListActivity的设计开始;当完成时,这个Activity应该看起来像下面的截图:

如截图所示,我们需要一个FloatingActionButton和一个RecyclerView来构建这个Activity,所以这里是这个示例的 XML 布局文件——activity_todo_list.xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.rivuchk.todoapplication.
todolist.TodoListActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView android:id="@+id/rvToDoList"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="LinearLayoutManager"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<android.support.design.widget.FloatingActionButton android:id="@+id/fabAddTodo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:srcCompat="@drawable/ic_add" />
</android.support.design.widget.CoordinatorLayout>
看一下前面的布局。在RecyclerView的声明中,我们将其layoutManager设置为LinearLayoutManager,并将方向设置为从布局本身开始的垂直,所以我们就不需要在代码中设置它了。
我们使用了一个FloatingActionButton来添加新的待办事项。我们还使用了AppBarLayout作为操作栏。
是时候继续前进,看看TodoListActivity的onCreateBaseActivity方法了,如下所示:
lateinit var adapter: ToDoAdapter
private val INTENT_EDIT_TODO: Int = 100
private val INTENT_ADD_TODO: Int = 101
override fun onCreateBaseActivity(savedInstanceState: Bundle?) {
setContentView(R.layout.activity_todo_list)
setSupportActionBar(toolbar)
fabAddTodo.setOnClickListener { _ ->
startActivityForResult(intentFor<AddTodoActivity>
(),INTENT_ADD_TODO)
}
adapter = ToDoAdapter(this,{
todoItem->
startActivityForResult(intentFor<TodoDetailsActivity>
(Pair(Constants.INTENT_TODOITEM,todoItem)),INTENT_EDIT_TODO)
})
rvToDoList.adapter = adapter
fetchTodoList()
}
在前面的程序中,我们创建了一个 ToDoAdapter 实例,将其设置为 rvToDoList 的适配器,rvToDoList 是我们将显示待办事项列表的 RecyclerView。在创建 ToDoAdapter 实例时,我们传递了一个 lambda;当点击 rvToDoList 中的项目时,应该调用这个 lambda。
我们还在 onCreateBaseActivity 方法的末尾调用了一个 fetchTodoList() 函数。正如其名称所示,它负责从 REST API 获取待办事项列表。我们将在稍后查看该方法的定义和细节,但现在,让我们看看 Adapter:
class ToDoAdapter(
private val context:Context, //(1) val onItemClick:(ToDoModel?)->Unit = {}//(2)
):RecyclerView.Adapter<ToDoAdapter.ToDoViewHolder>() {
private val inflater:LayoutInflater =
LayoutInflater.from(context)//(3) private val
todoList:ArrayList<ToDoModel> = arrayListOf()//(4) fun
setDataset(list:List<ToDoModel>) {//(5)
todoList.clear()
todoList.addAll(list)
notifyDataSetChanged()
}
override fun getItemCount(): Int = todoList.size
override fun onBindViewHolder(holder: ToDoViewHolder?,
position: Int) {
holder?.bindView(todoList[position])
}
override fun onCreateViewHolder
(parent: ViewGroup?, viewType: Int): ToDoViewHolder {
return ToDoViewHolder
(inflater.inflate(R.layout.item_todo,parent,false))
}
inner class ToDoViewHolder(itemView:View):
RecyclerView.ViewHolder(itemView) {
fun bindView(todoItem:ToDoModel?) {
with(itemView) {//(6)
txtID.text = todoItem?.id?.toString()
txtDesc.text = todoItem?.todoDescription
txtStatus.text = todoItem?.status
txtDate.text = todoItem?.todoTargetDate
onClick {
this@ToDoAdapter.onItemClick(todoItem)//(7)
}
}
}
}
}
仔细研究前面的代码。这是完整的 ToDoAdapter 类。我们取了一个 context 实例作为注释 (1) 构造函数参数。我们使用那个 context 来获取一个 Inflater 实例,然后在该实例中用于 onCreateViewHolder 方法中的布局填充。我们创建了一个空的 ToDoModel ArrayList。我们使用那个列表来获取适配器的 getItemCount() 函数的项目数,并在 onBindViewHolder 函数中将其传递给 ViewHolder 实例。
我们还在 ToDoAdapter 构造函数内部将 lambda 作为 val 参数——onItemClick(注释 (2))。这个 lambda 应该接收一个 ToDoModel 实例作为参数,并返回 unit。
我们在 ToDoViewHolder 的 bindView 中使用了那个 lambda,在 itemView 的 onClick(注释 (7))中。所以,每次我们点击一个项目时,都会调用 onItemClick lambda,它是由 TodoListActivity 传递的。
现在,关注注释 (5) 中的 setDataset() 方法。此方法用于将一个新的列表分配给适配器。它将清除 ArrayList—TodoList 并将传递的列表中的所有项目添加到其中。这个 setDataset 方法应该在 TodoListActivity 中的 fetchTodoList() 方法中调用。那个 fetchTodoList() 方法负责从 REST API 获取列表,并将该列表传递给适配器。
我们将在稍后查看 fetchTodoList() 方法,但让我们集中关注 REST API 和 Retrofit 2 用于 API 调用。
Retrofit 2 用于 API 调用
Retrofit by Square 是 Android 中最著名和最广泛使用的 REST 客户端之一。它内部使用 OkHTTP 进行 HTTP 和网络调用。REST 客户端这个词使其与其他 Android 网络库不同。虽然大多数网络库(Volley、OkHTTP 等)专注于同步/异步请求、优先级、有序请求、并发/并行请求、缓存等,但 Retrofit 更注重使网络调用和解析数据更像方法调用。它简单地将你的 HTTP API 转换为 Java 接口。而且它甚至不尝试自己解决网络问题,而是将此委托给内部的 OkHTTP。
那么,它是如何将 HTTP API 转换为 Java 接口的呢?Retrofit 简单地使用一个转换器将 POJO(普通的 Java 对象)类序列化和反序列化为 JSON 或 XML。现在,什么是转换器?转换器是那些为你解析 JSON/XML 的辅助类。转换器通常使用Serializable接口内部进行 JSON/XML 和 POJO 类(在 Kotlin 中的数据类)之间的转换。它具有可插拔性,为你提供了许多转换器的选择,如下所示:
-
Gson
-
Jackson
-
Guava
-
Moshi
-
Java 8 转换器
-
Wire
-
Protobuf
-
SimpleXML
我们将使用 Gson 来处理我们的书籍。要使用 Retrofit,你需要以下三个类:
-
一个
Model类(POJO 或数据类) -
一个类,通过
Retrofit.Builder()提供 Retrofit 客户端实例 -
一个
Interface,定义可能的 HTTP 操作,包括请求类型(GET 或 POST)、参数/请求体/查询字符串,以及最终的响应类型
那么,让我们从Model类开始。
在创建类之前,我们首先需要了解 JSON 响应的结构。我们在上一章中看到了 JSON 响应,但为了快速回顾,以下是GET_TODO_LIST API 的 JSON 响应:
{
"error_code": 0,
"error_message": "",
"data": [
{
"id": 1,
"todoDescription": "Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Integer tincidunt quis lorem id rhoncus. Sed
tristique arcu non sapien consequat commodo. Nulla dolor
tellus, molestie nec ipsum at, eleifend bibendum quam.",
"todoTargetDate": "2017/11/18",
"status": "complete"
}
]
}
error_code表示是否存在错误。如果error_code是非零值,则必须存在错误。如果是零,则没有错误,你可以继续解析数据。
如果有错误,error_message将包含信息。如果error_code为零,则error_message将为空。
data键将包含待办事项列表的 JSON 数组。
这里需要注意的一点是,error_code和error_message将与我们项目中的所有 API 保持一致,因此如果我们为所有 API 创建一个基类,然后在需要时扩展该类会更好。
这是我们BaseAPIResponse类:
open class BaseAPIResponse (
@SerializedName("error_code")
val errorCode:Int,
@SerializedName("error_message")
val errorMessage:String): Serializable
在这个类中,我们有两个val属性——errorCode和errorMessage;注意@SerializedName注解。这个注解由 Gson 用来声明属性的序列化名称;序列化名称应该与 JSON 响应相同。如果你有与 JSON 响应相同的变量名,你可以轻松地避免这个注解。如果变量名不同,序列化名称用于匹配 JSON 响应。
现在,让我们继续进行GetToDoListAPIResponse;以下是这个类的定义:
open class GetToDoListAPIResponse(
errorCode:Int,
errorMessage:String,
val data:ArrayList<ToDoModel>
):BaseAPIResponse(errorCode,errorMessage)
这里,我们跳过了@SerializedName注解的data,因为我们使用的是与 JSON 响应相同的名称。剩余的两个属性是由BaseAPIResponse类声明的。
对于数据,我们使用ToDoModel的ArrayList;Gson 将负责将 JSON 数组转换为ArrayList。
现在,让我们看一下ToDoModel类:
data class ToDoModel (
val id:Int,
var todoDescription:String,
var todoTargetDate:String,
var status:String
):Serializable
Retrofit 的builder类很简单,如下所示:
class APIClient {
private var retrofit: Retrofit? = null
fun getClient(): Retrofit {
if(null == retrofit) {
val client = OkHttpClient.Builder().connectTimeout(3,
TimeUnit.MINUTES)
.writeTimeout(3, TimeUnit.MINUTES)
.readTimeout(3,
TimeUnit.MINUTES).addInterceptor(interceptor).build()
retrofit = Retrofit.Builder()
.baseUrl(Constants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
}
return retrofit!!
}
fun getAPIService() =
getClient().create(APIService::class.java)
}
getClient()函数负责创建并提供 Retrofit 客户端。getAPIService()函数帮助你将 Retrofit 客户端与定义的 HTTP 操作配对,并创建接口的实例。
我们使用OkHttpClient和Retrofit.Builder()来创建Retrofit实例。如果你不熟悉它们,你可以访问www.vogella.com/tutorials/Retrofit/article.html。
现在,让我们创建 HTTP 操作的接口——APIService——如下所示:
interface APIService {
@POST(Constants.GET_TODO_LIST)
fun getToDoList(): Call<GetToDoListAPIResponse>
@FormUrlEncoded
@POST(Constants.EDIT_TODO)
fun editTodo(
@Field("todo_id") todoID:String,
@Field("todo") todo:String
): Call<BaseAPIResponse>
@FormUrlEncoded
@POST(Constants.ADD_TODO)
fun addTodo(@Field("newtodo") todo:String): Call<BaseAPIResponse>
}
我们为所有 API 创建了 API 接口。注意函数的返回类型。它们返回一个封装实际预期响应的Call实例。
那么,Call实例是什么?使用它的目的是什么?
Call实例是对 Retrofit 方法的一个调用,它向 web 服务器发送请求并返回响应。每个调用都产生它自己的 HTTP 请求和响应对。我们该如何处理Call<T>实例?我们必须用Callback<T>实例enqueue它。
因此,同样的拉取机制,同样的回调地狱。然而,我们应该是响应式的,不是吗?让我们这么做。
RxKotlin 与 Retrofit
在 Android 中,我们可以使用 RxAndroid 以及 RxKotlin 来增加 Android 风味和好处,并且 Retrofit 也支持它们。
那么,让我们首先修改我们的build.gradle以支持 ReactiveX。将以下依赖项添加到应用的build.gradle级别:
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0 '
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation 'io.reactivex.rxjava2:rxkotlin:2.1.0'
第一个提供了 Retrofit 2 适配器用于 RxJava 2,而接下来的两个添加了 RxAndroid 和 RxKotlin 到项目中。
注意,RxKotlin 是 RxJava 的包装器,所以 RxJava 2 的适配器将与 RxKotlin 2 完美兼容。
现在我们已经添加了依赖项,让我们继续修改我们的代码,使其与Observable/Flowable而不是Call一起工作。
这是修改后的APIClient.kt文件:
class APIClient {
private var retrofit: Retrofit? = null
enum class LogLevel {
LOG_NOT_NEEDED,
LOG_REQ_RES,
LOG_REQ_RES_BODY_HEADERS,
LOG_REQ_RES_HEADERS_ONLY
}
/**
* Returns Retrofit builder to create
* @param logLevel - to print the log of Request-Response
* @return retrofit
*/
fun getClient(logLevel: Int): Retrofit {
val interceptor = HttpLoggingInterceptor()
when(logLevel) {
LogLevel.LOG_NOT_NEEDED ->
interceptor.level = HttpLoggingInterceptor.Level.NONE
LogLevel.LOG_REQ_RES ->
interceptor.level = HttpLoggingInterceptor.Level.BASIC
LogLevel.LOG_REQ_RES_BODY_HEADERS ->
interceptor.level = HttpLoggingInterceptor.Level.BODY
LogLevel.LOG_REQ_RES_HEADERS_ONLY ->
interceptor.level =
HttpLoggingInterceptor.Level.HEADERS
}
val client = OkHttpClient.Builder().connectTimeout(3,
TimeUnit.MINUTES)
.writeTimeout(3, TimeUnit.MINUTES)
.readTimeout(3,
TimeUnit.MINUTES).addInterceptor(interceptor).build()
if(null == retrofit) {
retrofit = Retrofit.Builder()
.baseUrl(Constants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory
(RxJava2CallAdapterFactory.create())
.client(client)
.build()
}
return retrofit!!
}
fun getAPIService(logLevel: LogLevel =
LogLevel.LOG_REQ_RES_BODY_HEADERS) =
getClient(logLevel).create(APIService::class.java)
}
这次,我们添加了一个 OkHttp Logging 拦截器(HttpLoggingInterceptor)以及一个 RxJava 适配器。这个 OkHttp Logging 拦截器将帮助我们记录请求和响应。回到 RxJava 适配器,看看高亮代码——我们添加了RxJava2CallAdapterFactory作为 Retrofit 客户端的CallAdapterFactory。
我们还需要修改APIService.kt文件,以便让函数返回Observable而不是Call,如下所示:
interface APIService {
@POST(Constants.GET_TODO_LIST)
fun getToDoList(): Observable<GetToDoListAPIResponse>
@POST(Constants.EDIT_TODO)
fun editTodo(
@Body todo:String
): Observable<BaseAPIResponse>
@POST(Constants.ADD_TODO)
fun addTodo(@Body todo:String): Observable<BaseAPIResponse>
}
现在所有的 API 都返回Observable而不是Call。最后,我们一切都准备好了,可以查看TodoListActivity中的fetchTodoList()函数。
private fun fetchTodoList() {
APIClient()
.getAPIService()
.getToDoList()
.subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onNext = { response ->
adapter.setDataset(response.data)
},
onError = {
e-> e.printStackTrace()
}
)
}
这个函数执行一个简单的任务;它订阅 API(来自 API 的Observable),并在数据到达时将其设置到适配器中。你应该考虑在这里添加逻辑来检查错误代码,但到目前为止它工作得相当好。这个活动的截图已经在本章的开头显示过了,所以我们在这里省略它。
使 Android 事件响应式
我们已经使我们的 API 调用响应式了,但我们的事件呢?记住 ToDoAdapter;我们在 TodoListActivity 中创建并传递了一个 lambda,并在 ToDoViewHolder 内部使用了它。这相当混乱。这些事件也应该响应式,不是吗?所以,让我们使事件也响应式。
Subject 在使事件响应式方面发挥着极好的作用。由于 Subject 是 Observable 和 Observer 的绝佳组合,我们可以在 Adapter 内部将其用作 Observer,并在 Activity 内部将其用作 Observable,从而使得传递事件变得容易。
因此,让我们按照以下方式修改 ToDoAdapter:
class ToDoAdapter(
private val context:Context, //(1)
val onClickTodoSubject:Subject<Pair<View,ToDoModel?>>//(2)
):RecyclerView.Adapter<ToDoAdapter.ToDoViewHolder>() {
private val inflater:LayoutInflater =
LayoutInflater.from(context)//(3)
private val todoList:ArrayList<ToDoModel> = arrayListOf()//(4)
fun setDataset(list:List<ToDoModel>) {//(5)
todoList.clear()
todoList.addAll(list)
notifyDataSetChanged()
}
override fun getItemCount(): Int = todoList.size
override fun onBindViewHolder(holder: ToDoViewHolder?,
position: Int) {
holder?.bindView(todoList[position])
}
override fun onCreateViewHolder
(parent: ViewGroup?, viewType: Int): ToDoViewHolder {
return ToDoViewHolder(inflater.inflate
(R.layout.item_todo,parent,false))
}
inner class ToDoViewHolder(itemView:View):
RecyclerView.ViewHolder(itemView) {
fun bindView(todoItem:ToDoModel?) {
with(itemView) {//(6)
txtID.text = todoItem?.id?.toString()
txtDesc.text = todoItem?.todoDescription
txtStatus.text = todoItem?.status
txtDate.text = todoItem?.todoTargetDate
onClick {
onClickTodoSubject.onNext(Pair
(itemView,todoItem))//(7)
}
}
}
}
}
现在适配器看起来更简洁了。我们在构造函数中有一个 Subject 实例,当 itemView 被点击时,我们调用 Subject 的 onNext 事件,并通过 Pair 将 itemView 和 ToDoModel 实例传递。
然而,它仍然看起来像缺少了什么。onClick 方法仍然是一个回调;我们能不能也使其响应式呢?让我们这么做。
在 Android 中引入 RxBinding
为了帮助 Android 开发者,Jake Wharton 创建了 RxBinding 库,它可以帮助您以响应式的方式获取 Android 事件。您可以在 github.com/JakeWharton/RxBinding 找到它们。让我们通过将其添加到项目中开始吧。
将以下依赖项添加到应用级别的 build.gradle 文件中:
implementation 'com.jakewharton.rxbinding2:rxbinding-kotlin:2.0.0'
然后,我们可以用以下代码行替换 ToDoViewHolder 内部的 onClick:
itemView.clicks()
.subscribeBy {
onClickTodoSubject.onNext(Pair(itemView,todoItem))
}
这很简单。然而,你可能正在想,使它们响应式有什么好处呢?这里的实现足够简单,但想想你有很多逻辑的情况。你可以轻松地将逻辑划分为操作符,特别是 map 和 filter 可以为你提供极大的帮助。不仅如此,RxBindings 还为您提供了一致性。例如,当我们需要观察 EditText 上的文本变化时,我们通常会在 TextWatcher 实例中编写大量的代码,但如果你使用 RxBindings,它将让你这样做:
textview.textChanges().subscribeBy {
changedText->Log.d("Text Changed",changedText)
}
是的,这真的很简单,也很容易。RxBinding 还为您提供了更多的好处。您可以查看 speakerdeck.com/lmller/kotlin-plus-rxbinding-equals 和 adavis.info/2017/07/using-rxbinding-with-kotlin-and-rxjava2.html。
因此,现在,多亏了 Jake Wharton,我们也可以使我们的视图和事件响应式。
Kotlin 扩展
在本章结束之际,我想向您介绍 Kotlin 扩展。不,确切地说,不是 Kotlin 扩展函数,尽管它们与 Kotlin 扩展函数非常相关。Kotlin 扩展是一份精心挑选的、在 Android 中最常用的扩展函数列表。
例如,如果您想要一个扩展函数,可以从View/ViewGroup实例创建位图(在添加 MapFragment 中的标记时特别有用),您可以从那里复制并粘贴以下扩展函数:
fun View.getBitmap(): Bitmap {
val bmp = Bitmap.createBitmap(width, height,
Bitmap.Config.ARGB_8888)
val canvas = Canvas(bmp)
draw(canvas)
canvas.save()
return bmp
}
或者,更常见的情况,当您需要隐藏键盘时,以下扩展函数将帮助您:
fun Activity.hideSoftKeyboard() {
if (currentFocus != null) {
val inputMethodManager = getSystemService(Context
.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow
(currentFocus!!.windowToken, 0)
}
}
这个在线列表为您提供了更多扩展函数,由 Ravindra Kumar 维护(Twitter,GitHub—@ravidsrk)。所以,下次您需要扩展函数时,在编写自己的函数之前,先看看 kotlinextensions.com/。
摘要
我们已经完成了这本书的最后一章。在这一章中,我们学习了如何为 RxKotlin 和 RxAndroid 配置 Retrofit,以及如何使我们的 Android 视图和事件以及自定义视图变得响应式。
我们学习了如何使用 RxJava2Adapter 和 Retrofit,以及如何使用Subject进行事件传递。我们还学习了如何使用 RxBindings。
在整本书中,我们试图深入探讨响应式编程,涵盖所有可能的概念,并尝试使所有代码都变得响应式。
如果您对此书有任何疑问,或者对此书有任何顾虑,请随时发送电子邮件至 rivu.chakraborty6174@gmail.com,并在邮件主题行中提及 Book Query - Reactive Programming in Kotlin。您也可以查看 Rivu Chakraborty 的网站 (www.rivuchk.com),他在那里定期发布关于 Kotlin、Google 开发者小组加尔各答和 Kotlin 加尔各答用户组聚会的文章。他还在那里撰写教程和博客,以及介绍他自己开发的 Android 插件的简介。此外,当他撰写其他地方的博客和文章时,他会在自己的网站上发布它们的 URL。
感谢您阅读这本书。祝您在 Kotlin 中进行愉快的响应式编程。


浙公网安备 33010602011771号