Kotlin-编程秘籍-全-

Kotlin 编程秘籍(全)

原文:zh.annas-archive.org/md5/730504f1e8ca1f47b56ed35f1322fc50

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Kotlin 烹饪秘籍将成为新 Kotlin 开发者遇到难题时的首选指南。除此之外,Kotlin 烹饪秘籍还将帮助开发者学习一些实用的技巧和概念,这些技巧和概念在他们编码时可能会用到。这本书还将帮助开发者挖掘出一种令人惊叹的编程语言的全部潜力,即 Kotlin。

本书从 Kotlin 的概述开始,然后介绍 Kotlin 提供的一些优秀简单概念和功能。从那里,它将转向面向对象的基础知识以及创建简单的 Android 应用程序。接下来将是更复杂概念(如网络、数据库、架构、文件 io 和测试)的食谱。它还将涵盖 Anko 的某些优秀功能,这些功能真正简化了 Android 开发中的复杂概念,使其更快、更有趣。最后是一些零散但极其有用的食谱,开发者可能不时需要。

本书面向对象

本书针对的是已经了解 Android 和 Java 开发的 Kotlin 初学者,他们具备良好的知识水平和对 Android 开发周期的理解。读者熟悉 Android 开发的概念,并理解测试代码的需求。他们希望通过学习高效的 Kotlin 技巧,使现有的 Android 开发过程更加高效和有趣。这不是一本 Kotlin 入门书,它假设读者对 Kotlin 有基本的了解。本书旨在帮助开发者解决他们在使用 Kotlin 时遇到的难题。

本书涵盖内容

第一章安装和与开发环境协同工作,将指导你从 Kotlin 项目开始。我们还将向你介绍 Gradle 构建系统,并帮助你设置开发环境。

第二章控制流,包含了 Kotlin 中控制流的食谱。Kotlin 为旧的控制流带来了很多功能,因为你现在可以用它们作为表达式。Kotlin 还引入了一个强大的 "when",这基本上是 Java 的 "switch" 改进。

第三章类和对象,指出类和对象是面向对象编程的必然组成部分。这一章将包括开发者面临的现实世界问题的解决方案和示例,以及 Kotlin 如何解决这些问题。这一章还将为即将到来的第 OOPS 编程与 Kotlin 章节奠定基础。

第四章函数,说明函数是面向对象编程中不可避免的部分。本章将包括开发者面临的现实世界问题的解决方案和 Kotlin 如何解决这些问题。

第五章面向对象编程,基于 第三章类和对象 的学习,并将包括帮助进行面向对象编程的食谱。

第六章集合框架,介绍了探索 Kotlin 集合框架全部潜能的食谱。

第七章在 Kotlin 中处理文件操作,涵盖了关于基本 I/O 和文件 I/O 的食谱。

第八章Anko Commons 和扩展函数,包含了如何使用 Kotlin 的 Anko 库进行高效和快速 Android 开发的食谱。

第九章Anko 布局,提供了如何使用 Kotlin 的 Anko 库进行高效和快速 Android 开发的食谱。

第十章数据库和依赖注入,深入探讨了与 Android 中的 SQLite 数据库一起工作的食谱。

第十一章网络和并发,讨论了帮助开发者进行网络调用和通过网络获取数据的食谱。

第十二章Lambda 表达式和委托,揭示了 Kotlin 中一些最佳(且困难)的特性,即 Lambda 表达式和委托。此部分包含帮助开发者入门的食谱。

第十三章测试,概述了在 Kotlin 中编写测试的概念,同时涉及单元测试、集成测试、仪器化和验收测试。

第十四章使用 Kotlin 开发 Web 服务,帮助开发者使用 Kotlin 语言编写 Web 服务。

要充分利用本书

本书假设您熟悉 Java 和 Android 开发。这不是学习 Kotlin 的入门书籍。读者必须使用 Android Studio,因为许多食谱将专注于 Android 开发。

下载示例代码文件

您可以从您的账户在 www.packtpub.com 下载本书的示例代码文件。如果您在其他地方购买了此书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packtpub.com 登录或注册。

  2. 选择支持选项卡。

  3. 点击代码下载与勘误表。

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明。

下载文件后,请确保使用最新版本解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

该书的代码包也托管在 GitLab 上,网址为 gitlab.com/users/aanandshekharroy/projects。我们还从丰富的书籍和视频目录中提供了其他代码包,可在 github.com/PacktPublishing/ 上找到。查看它们!

使用的约定

本书使用了许多文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理。以下是一个示例:“如果未使用默认约定,则应更新相应的 sourceSets 属性。”

代码块设置如下:

sourceSets {
   main.kotlin.srcDirs += 'src/main/myKotlin'
   main.java.srcDirs += 'src/main/myJava'
}

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

sourceSets {
 main.java.srcDirs += 'src/main/kotlin/'
}

任何命令行输入或输出都按以下方式编写:

$ kotlinc hello.kt -include-runtime -d hello.jar. $ java -jar hello.jar

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“在 Select Deployment Target 窗口中,选择您的设备,然后单击 OK。”

警告或重要注意事项如下所示。

小技巧和技巧如下所示。

部分

在本书中,您将找到一些频繁出现的标题(准备就绪如何做…它是如何工作的…还有更多…也见)。

要清楚地说明如何完成一个食谱,请按照以下方式使用这些部分:

准备就绪

本节告诉您在食谱中可以期待什么,并描述如何设置任何软件或任何为食谱所需的初步设置。

如何做…

本节包含遵循食谱所需的步骤。

它是如何工作的...

本节通常包含对前一个章节发生情况的详细解释。

更多信息...

本节包含有关食谱的附加信息,以便您对食谱有更深入的了解。

相关内容

本节提供了其他有用的信息链接,以帮助您了解食谱。

联系我们

我们始终欢迎读者的反馈。

一般反馈: 请通过feedback@packtpub.com发送电子邮件,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请通过questions@packtpub.com发送电子邮件给我们。

勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,我们将非常感谢。请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版: 如果您在互联网上发现任何形式的我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com与我们联系,并附上材料的链接。

如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

评论

请留下评论。一旦您阅读并使用过这本书,为何不在购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 团队可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

想了解更多关于 Packt 的信息,请访问packtpub.com

第一章:安装和与环境的交互

本章将涵盖以下内容:

  • 创建 Kotlin Android 项目

  • 如何使用 Gradle 运行 Kotlin 代码

  • 如何运行 Kotlin 编译后的类

  • 如何使用 Gradle 和 Kotlin 构建可执行的 jar 文件

  • 在 Kotlin 中读取控制台输入

  • 将 Java 代码转换为 Kotlin 以及相反操作

  • 如何使用 Kotlin 编写惯用日志记录器

  • 为 Kotlin 中的关键字转义 Java 标识符

  • 使用 "as" 关键字消除歧义,以局部重命名冲突实体

  • 在 Kotlin 中进行位操作

  • 将字符串解析为 Long、Double 或 Int

  • 在 Kotlin 中使用字符串模板

简介

Android 应用是一项迷人的技术。在 Android 上开发的应用具有全球的吸引力和受众。然而,这也给开发者带来了严峻的挑战。挑战在于更新 API、平台和多样化的设备功能。例如,如果你是一名 Android 开发者,如果你想支持 Android 中的所有 API 级别,你必须依赖 Java 6。Java 6 现在已经过时,甚至它的继任者 Java 7 也有些过时。对于 Android,现代语言的需求非常迫切,它围绕 Android 建立了一个价值万亿美元的行业,并影响了数十亿人的生活。诚然,我们现在有 Java 8,但我们只能用它来开发 API 级别 24 及以上的 Android 应用。然而,这相当于只针对 2017 年的 9% 的 Android 设备;显然,这不是正确的做法。

尽管如此,并没有失去一切,多亏了 JVM,我们可以使用任何在编译时产生 JVM 兼容字节码的语言来编写 Android 应用。所以从理论上讲,我们可以使用 Clojure、Groovy、Scala 和 Kotlin,但 Kotlin 是所有替代方案中最好的,为什么?因为它在 2017 年 4 月,谷歌宣布 Kotlin 为 Android 开发的官方语言。

一些最大的科技公司,如 Pinterest、Uber、Atlassian、Coursera 和 Evernote,现在正在它们的 Android 应用中使用 Kotlin。他们对此的广泛采用已经为 Kotlin 赢得了巨大的声誉。与 Android 和 Java 的 100% 兼容性有助于 Kotlin 的采用。与 Java 相比,Kotlin 更易于使用,除了 Android 应用,你还可以用它来构建 Web 应用。因此,本章将向您介绍 Kotlin,并帮助您开始使用这项令人惊叹的技术。

在本章中,我们将首先了解如何设置环境以开始使用 Kotlin。

创建 Kotlin Android 项目

使用 Kotlin 入门非常简单,尤其是在 Google 为该语言添加了官方支持之后。您可以直接使用 Kotlin 与 Android Studio 3 一起使用。撰写本书时,Android Studio 3 仍处于测试版本。使用 Kotlin 进行 Android 开发的最佳之处在于,它与现有的代码兼容,无论是 Java 还是 C++。在用 Kotlin 开发时,您会发现 Kotlin 代码简洁、可扩展且强大。它确实让 Android 开发变得更加有趣。让我们看看如何通过在 Android Studio 3 中首先创建 Kotlin 项目来开始使用 Kotlin 进行开发。

准备中

要开始此菜谱,您需要在您的计算机上安装 Android Studio。Android Studio 包含 Android SDK 和 Android 虚拟设备。请确保您已安装 Java 开发工具包。您需要一个安卓手机或模拟器来调试您的项目。如果您不使用安卓手机,您还需要至少安装一个符合您所需规格的 Android 虚拟设备。

因此,基本上,以下是需要安装的清单,在您进入下一节之前需要完成:

  • Java 开发工具包(使用最新版本)

  • Android Studio 3+

  • 安卓手机或模拟器

如何操作...

在 Android Studio 中创建项目非常简单,要在 Kotlin 中创建它只需多点击一次。以下是进行此操作的步骤:

  1. 在 Android Studio 中,在菜单中点击文件 | 新建 | 新建项目。或者,如果您刚刚打开 Android Studio 并看到 Android Studio 欢迎窗口,请点击开始新的 Android Studio 项目。

  2. 在向导中,添加您的应用程序名称和公司域名,并简单地勾选包含 Kotlin 支持的复选框。点击“下一步”:

  1. 在下一屏幕上,您将被要求选择目标设备和最低 SDK 支持。基本上,它会询问类似的问题:“您希望应用程序在手机和 Android Wear 上运行吗?”以及“您希望支持从 Jelly Bean 开始还是从 KitKat 开始?”

  1. 在下一屏幕上,您将被提示向项目中添加活动。您也可以跳过此步骤,稍后添加活动,但在此阶段,只需点击基本活动并点击“下一步”。如果您还选择了可穿戴设备或任何其他选项,您将被提示为这些组件添加活动:

  1. 接下来,您将被提示配置您添加的活动。基本上,您需要做的是提供活动名称、布局名称标题。完成这些后,点击“完成”,因为您已经完成了在 Kotlin 中创建第一个项目的操作。

  2. 在您的设备上运行项目:您需要按照以下步骤操作:

    1. 使用 USB 线缆将您的设备连接到您的开发机器。

    2. 通过转到设置 | 开发者选项来在您的设备上启用 USB 调试。

在 Android 4.2 及更高版本中,开发者选项默认隐藏。要使其可用,转到设置 | 关于手机,然后连续点击 Build number 七次。返回上一屏幕以找到开发者选项。

现在在你的 Android Studio 中,点击项目窗口中的 app 模块,然后选择运行(或在工具栏中点击运行)。

在选择部署目标窗口中,选择你的设备,然后点击 OK。过了一会儿,你将在你的手机或模拟器上看到应用程序正在运行。

还有更多...

在创建新项目窗口中点击完成按钮后,Android Studio 将配置一些设置并创建你的项目。如果你在步骤 4 中添加了活动,你将看到活动的样板代码。它看起来可能如下所示:

如何使用 Gradle 运行 Kotlin 代码

Gradle 现在已经成为 Android 的默认构建工具,并且非常强大。它非常适合自动化任务,同时不牺牲可维护性、可用性、灵活性、可扩展性或性能。在本菜谱中,我们将看到如何使用 Gradle 运行 Kotlin 代码。

准备工作

我们将使用 IntelliJ IDEA,因为它提供了 Gradle 与 Kotlin 的出色集成,并且是一个非常出色的 IDE 来进行工作。你也可以用它来使用 Android Studio。

如何做这件事...

在以下步骤中,我们将使用 Gradle 构建系统创建一个 Kotlin 项目。首先,从菜单中选择创建新项目选项。然后,按照以下步骤操作:

  1. 使用 Gradle 构建系统创建项目:

  1. 在你创建了项目之后,你将拥有 build.gradle 文件,它看起来可能如下所示:
version '1.0-SNAPSHOT'

buildscript {
  ext.kotlin_version = '1.1.4-3'

  repositories {
      mavenCentral()
  }
  dependencies {
      classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
  }
}

apply plugin: 'java'
apply plugin: 'kotlin'

sourceCompatibility = 1.8

repositories {
  mavenCentral()
}

dependencies {
  compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
  testCompile group: 'junit', name: 'junit', version: '4.12'
}

compileKotlin {
  kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
  kotlinOptions.jvmTarget = "1.8"
}
  1. 现在我们将创建一个 HelloWorld 类,它将有一个简单的 main 函数:

  1. 现在,能够直接运行此代码将非常酷。为了做到这一点,我们将使用 gradle run 命令。然而,在这样做之前,我们需要启用应用程序插件,这将允许我们直接运行此代码。我们需要在 build.gradle 文件中添加两行来设置它:
apply plugin: 'application'
mainClassName = "HelloWorldKt"
  1. 在此之后,你可以在终端中输入 gradle run 来执行此文件,你将看到方法的输出,如图所示:

还有更多...

当你在 IntelliJ 中创建新项目时,项目的默认结构如图所示:

project
   - src
       - main (root)
           - kotlin
           - java

如果你想要项目有不同的结构,你应该在 build.gradle 中声明它。你可以通过在 build.gradle 中添加以下行来实现:

如果不使用默认约定,应更新相应的 sourceSets 属性:

sourceSets {
   main.kotlin.srcDirs += 'src/main/myKotlin'
   main.java.srcDirs += 'src/main/myJava'
}

虽然你可以将 Kotlin 和 Java 文件放在同一个包下,但将它们分开是一个好的做法。

参见

查看本章中的 如何使用 Gradle 和 Kotlin 构建可执行 jar 菜单。

如何运行编译后的 Kotlin 类

使用任何语言的命令行编译器是更好地理解该语言的第一步之一,这种知识在很多情况下都很有用。在这个菜谱中,我们将使用命令行运行 Kotlin 程序,并且我们还会在 Kotlin 的交互式 shell 中玩一会儿。

准备工作

要能够执行此菜谱,你需要在你的开发机器上安装 Kotlin 编译器。每个 Kotlin 版本都附带一个独立编译器。你可以在 github.com/JetBrains/kotlin/releases 找到最新版本。

要手动安装编译器,将独立编译器解压缩到目录中,并可选地将 bin 目录添加到系统路径。bin 目录包含在 Windows、OS X 和 Linux 上编译和运行 Kotlin 所需的脚本。

如何做...

现在我们准备使用命令行运行我们的第一个程序。首先,我们将创建一个简单的应用程序,显示“Hello World!”,然后编译它:

  1. 创建一个名为 hello.kt 的文件,并在该文件中添加以下代码行:
fun main(args: Array<String>) {
    println("Hello, World!")
 }
  1. 现在我们使用以下命令编译文件:
$ kotlinc hello.kt -include-runtime -d hello.jar
  1. 现在我们使用以下命令运行应用程序:
$ java -jar hello.jar
  1. 假设你想创建一个可以与其他 Kotlin 应用程序一起使用的库;我们可以简单地编译相关的 Kotlin 应用程序为 .jar 可执行文件,而不使用 -include-runtime 选项,即新的命令如下:
$ kotlinc hello.kt -d hello.jar
  1. 现在,让我们看看 Kotlin 交互式 shell。只需不带任何参数运行 Kotlin 编译器即可获得交互式 shell。下面是它的样子:

图片

希望你注意到了我总是忽略的信息,那就是退出交互式 shell 的命令是 :quit,获取帮助的命令是 :help

你可以在交互式 shell 中运行任何有效的 Kotlin 代码。例如,尝试以下命令中的几个:

  • 3*2+(55/5)

  • println("yo")

  • println("check this out ${3+4}")

下面是运行前面代码的截图:

图片

如何工作...

-include-runtime 选项使得生成的 .jar 文件包含 Kotlin 运行时库,从而使其自包含并可运行。然后,我们使用 Java 运行生成的 .jar 文件。

命令中的 -d 选项表示编译器的输出应该被命名为什么,可能是类文件的目录名或 .jar 文件名。

更多...

Kotlin 也可以用于编写 shell 脚本。shell 脚本包含顶层可执行代码。

Kotlin 脚本文件具有 .kts 扩展名,而不是 Kotlin 应用程序的常用 .kt 扩展名。

要运行脚本文件,只需将 -script 选项传递给编译器:

$ kotlinc -script kotlin_script_file_example.kts

如何使用 Gradle 和 Kotlin 构建可执行的 JAR 文件

Kotlin 是创建小型命令行工具的绝佳选择,这些工具可以打包并作为正常的 JAR 文件分发。在本教程中,我们将看到如何使用 Gradle 构建系统来实现。Gradle 构建系统是最复杂的构建系统之一。它是 Android 的默认构建工具,旨在简化复杂、多语言构建的脚本编写,这些构建通常具有许多依赖项(大型项目的典型特征)。它通过自动化项目而不会牺牲可维护性、可用性、灵活性、可扩展性或性能来实现目标。我们将使用 Gradle 构建系统来创建自解压 JAR 文件。这个 JAR 文件可以在支持 Java 的任何平台上分发和运行。

准备工作

您需要一个集成开发环境(最好是 IntelliJ 或 Android Studio),并且需要告诉它 Kotlin 文件所在的位置。您可以通过在 build.gradle 文件中指定它来实现,添加以下内容:

sourceSets {
   main.java.srcDirs += 'src/main/kotlin/'
}

如果您的 Kotlin 文件与 Java 包分开,前面的这些行是必需的。这是可选的,您可以在 Java 包下继续使用 Kotlin 文件,但将它们分开是一个好的实践。

我们将创建一个非常简单的函数,当执行时只打印 Hello World!。由于它是一个简单的函数,我只是将其添加为一个顶级的 main() 函数。

如何操作...

让我们一步步来,这样我们可以创建一个可执行的 JAR 文件:

  1. 我们将创建一个简单的类 HelloWorld.kt,其中包含主函数,该函数只打印出 “Hello world!”:
fun main(args:Array<String>){
   println("Hello world")
}
  1. 现在我们需要配置一个 jar 任务,Gradle 构建过程会通过它来告知我们的项目入口。在一个 Java 项目中,这将是我们 main() 函数所在类的路径,因此您需要在 build.gradle 中添加此 jar 任务:
jar {
   manifest {
       attributes 'Main-Class': 'HelloWorldKt'
   }
   from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
}
  1. 在将前面的代码片段添加到 build.gradle 后,您需要运行以下 gradle 命令来创建 JAR 文件:
./gradlew clean jar
  1. 创建的 JAR 文件可以在 build/libs 文件夹中找到。现在您只需运行 java -jar demo.jar 命令来运行 JAR 文件。

在您完成之后,您可以在控制台中看到输出:

图片

它是如何工作的...

要创建可执行 JAR 文件,我们需要在 META-INF 目录中一个名为 MANIFEST.MF 的清单文件。对于我们的目的,我们只需要指定包含基于 Java 的提取程序 main() 方法的 Java 类的名称。

有些人可能会争论,尽管我们没有顶级类声明,但我们已经在 jar 任务中的代码中将其指定为 HelloWorldKt

manifest {
       attributes 'Main-Class': 'HelloWorldKt'
   }

将前面的代码块放在 jar 任务中的原因是 Kotlin 编译器会将所有顶级函数添加到相应的类中,以实现与 JVM 的向后兼容性。因此,Kotlin 编译器生成的类将具有文件名,加上 Kt 后缀,使其成为 HelloWorldKt

此外,我们在 jar 任务中添加 from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } 的原因是因为我们希望 Gradle 复制 JAR 的所有依赖项。这样做的原因是,默认情况下,当 Gradle(以及 Maven)将一些 Java 类文件打包到 JAR 文件中时,它假设这个 JAR 文件将被应用程序引用,其中所有依赖项都可以在加载应用程序的类路径中访问。因此,通过在 jar 任务中指定前面的行,我们告诉 gradle 将这个 JAR 的所有引用依赖项作为 JAR 本身的一部分复制。在 Java 社区中,这被称为 胖 JAR。在胖 JAR 中,所有依赖项最终都会出现在加载应用程序的类路径中,因此代码可以无问题地执行。创建胖 JAR 的唯一缺点是它们的文件大小会不断增长(这也解释了其名称),尽管在大多数情况下这并不是一个大问题。

在 Kotlin 中读取控制台输入

在许多应用中,用户交互是一个非常重要的部分,而实现这一点的最基本方式是读取用户输入的内容,并根据它给出输出。在这个菜谱中,我们将了解读取输入的不同方式,并在控制台中提供输出。

准备工作

您需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行来编译和运行您的 Kotlin 代码,这需要安装 Kotlin 编译器和 JDK。

如何做到这一点...

让我们通过以下步骤了解如何在 Kotlin 中读取控制台输入:

  1. 我们将从一个简单的打印一行输出到控制台开始,随着我们的前进,我们将逐步转向更高级的逻辑:
println("Just a line")
  1. 现在,我们将尝试从控制台读取字符串输入并将其再次输出:
println("Input your first name")
var first_name = readLine()
println("Your first name: $first_name")
  1. 好的,我们是否可以用 Int 重复这个过程?
println("Hi $first_name, let us have a quick math test. Enter two numbers separated by space.")
val (a, b) = readLine()!!.split(' ').map(String::toInt)
println("$a + $b = ${a+b}")
  1. 现在,让我们尝试一段复杂的代码,然后再开始解释:
fun main(args: Array<String>) {
   println("Input your first name")
   var first_name = readLine()
   println("Input your last name")
   var last_name = readLine()
   println("Hi $first_name $last_name, let us have a quick math test. Enter two numbers separated by space.")
   val (a, b) = readLine()!!.split(' ').map(String::toInt)
  println("what is $a + $b ?")
  println("Your answer is ${if (readLine()!!.toInt() == (a+b)) "correct" else "incorrect"}")
   println("Correct answer = ${a+b}")
 println("what is $a * $b ?")
   println("Your answer is ${if (readLine()!!.toInt() == (a*b)) "correct" else "incorrect"}")
   println("Correct answer = ${a*b}")
   println("Thanks for participating :)")
}

下面是编译和运行前面代码的截图:

它是如何工作的...

让我们尝试理解我们能够通过 Kotlin 读取输入的方法。

在幕后,Kotlin.io 使用 java.io 进行输入输出。所以 println 实际上是 System.out.println,但 Kotlin 通过使用字符串模板和 inline 函数提供了额外的功能,这使得编写代码变得极其简单和简洁。

这是从 Kotlin stdlib 中用于控制台 I/O 的实际代码的一部分:

/** Prints the given message and newline to the standard output stream. */
@kotlin.internal.InlineOnly
public inline fun println(message: Any?) {
   System.out.println(message)
}

将 Java 代码转换为 Kotlin 以及相反

Kotlin 最好的部分是它与 Java 的互操作性。此外,使用基于 IntelliJ 的 IDE,我们可以直接将我们的 Java 代码转换为 Kotlin。在这个菜谱中,我们将看到如何做到这一点。

准备工作

这个菜谱需要安装基于 IntelliJ 的 IDE,它可以编译和运行 Kotlin 和 Java。

如何做到这一点...

让我们看看将 Kotlin 文件转换为 Java 文件的步骤:

  1. 在您的 IntelliJ IDE 中,打开您想要转换为 Kotlin 的 Java 文件。

  2. 注意,它有一个 .java 扩展名。现在,在主菜单中,点击代码菜单并选择“将 Java 文件转换为 Kotlin 文件”选项。您的 Java 文件将被转换为 Kotlin,并且扩展名现在将是 .kt

这里是一个 Java 文件的示例:

转换为 Kotlin 后,这是我们的结果:

  1. Kotlin 文件可以转换为 Java,但最好避免这样做或找到其他替代方法。如果您必须绝对将 Kotlin 代码转换为 Java,请点击菜单中的“工具 | Kotlin | 显示 Kotlin 字节码”:

  1. 点击“显示 Kotlin 字节码”后,将打开一个标题为“Kotlin 字节码”的窗口:

  1. 点击“反编译”,将生成一个 .java 文件,其中包含从 Kotlin 代码反编译的 Java 字节码:

是的,它包含了很多在原始 Java 代码中不存在的多余代码,但这是反编译字节码的情况。目前,这是将 Kotlin 代码转换为 Java 的唯一方法。将反编译的文件复制到 .java 文件中,并删除多余的代码。

它是如何工作的...

Kotlin 是一种静态类型编程语言,它在 Java 虚拟机上运行,并编译成 JVM 兼容的字节码。这就是我们可以将 Java 代码转换为 Kotlin 并混合 Java 和 Kotlin 代码的原因。这也是为什么您可以从 Kotlin 中以某种方式获取 Java 代码(尽管输出并不完全符合预期)的原因。

如何在 Kotlin 中编写惯用的日志记录器

Kotlin 中包含了一些非常强大的功能,我们应该利用这些功能来改进我们的代码。这涉及到重新思考我们旧的编码最佳实践。我们许多旧的编码实践可以用 Kotlin 中的更好替代方案来替换。其中之一就是我们的日志记录器编写方式。尽管有许多库提供了日志功能,但我们将尝试在这个菜谱中仅使用惯用的 Kotlin 创建自己的日志记录器。

准备工作

我们将使用 IntelliJ IDE 来编写和执行我们的代码。

如何实现...

让我们按照给定的步骤在 Kotlin 中创建一个惯用的日志记录器:

  1. 首先,让我们看看在 Java 中是如何实现的。在 Java 中,使用 SLF4J,并且被认为是事实上的标准,以至于在 Java 语言中日志记录似乎是一个已经解决的问题。下面是一个 Java 实现的样子:
private static final Logger logger = LoggerFactory.getLogger(CurrentClass.class);
…
logger.info(“Hi, {}”, name);
  1. 显然,它也适用于 Kotlin,当然需要一些小的修改:
val logger = LoggerFactory.getLogger(CurrentClass::class)
…
logger.info(“Hi, {}”, name)

然而,除了这个之外,我们可以利用 Kotlin 的委托功能来增强 logger 的功能。在这种情况下,我们将使用 lazy 关键字来创建 logger。这样,我们只有在访问它时才会创建对象。委托是一种推迟对象创建直到使用它的好方法。这可以提高启动时间(这在 Android 中非常需要且受欢迎)。让我们探索 Kotlin 中使用懒委托的方法:

  1. 我们将内部使用 java.util.Logging,但这适用于你选择的任何 Logging 库。所以,让我们使用 Kotlin 的懒委托来获取我们的 logger:
public fun <R : Any> R.logger(): Lazy<Logger> {
   return lazy { Logger.getLogger(this.javaClass.name) }
}
  1. 现在在我们的类中,我们可以简单地调用方法来获取我们的 logger 并使用它:
class SomeClass {
  companion object { val log by logger() }

  fun do_something() {
      log.info("Did Something")
  }
}

当你运行代码时,你可以看到以下输出:

Sep 25, 2017 10:49:00 PM packageA.SomeClass do_something
INFO: Did Something

如输出所示,我们还可以得到类名和方法名(如果你是在方法内部访问 logger)。

它是如何工作的...

这里,需要注意的是,我们将 logger 放在了伴生对象中。这样做的原因很简单,因为我们希望每个类只有一个 logger 实例。

此外,logger() 返回一个委托对象,这意味着对象将在第一次访问时创建,并在后续访问时返回相同的值(对象)。

更多内容...

Anko 是一个使用 Kotlin 的 Android 库,它通过扩展函数使 Android 开发更加容易。它提供了Anko-logger,如果你不想自己编写 logger,可以使用它。它包含在 anko-commons 中,其中还有很多有趣的东西,使其值得将其包含在你的 Kotlin Android 项目中。

在 Anko 中,一个标准的 logger 实现看起来可能如下所示:

class SomeActivity : Activity(), AnkoLogger {
   private fun someMethod() {
       info("London is the capital of Great Britain")
       debug(5) // .toString() method will be executed
       warn(null) // "null" will be printed
   }
}

如你所见,你只需要实现 AnkoLogger,然后你就完成了。

每个方法都有两种版本:普通和懒(内联):

info("String " + "concatenation")
info { "String " + "concatenation" }

只有当 Log.isLoggable(tag, Log.INFO) 为 true 时,lambda 结果才会被计算。

相关内容

要了解更多关于委托属性的信息,请参考第三章使用委托属性的配方“与委托属性一起工作”类与对象

Kotlin 中关键字作为 Java 标识符的转义

Kotlin 的设计理念是互操作性。现有的 Java 代码可以无缝地从 Kotlin 代码中调用,但由于 Java 和 Kotlin 有不同的关键字,我们在调用与 Kotlin 关键字相似的 Java 方法时有时会遇到问题。Kotlin 中有一个解决方案,允许方法被调用时使用代表 Kotlin 关键字的名字。

准备工作

确保你有访问代码编辑器的权限,以便你可以编写和运行代码。

如何实现...

创建一个方法名等于任何 Kotlin 关键字的 Java 类。我使用 is 作为方法名,所以我的 Java 类如下所示:

public class ASimpleJavaClass {
   static void is(){
       System.out.print("Nothing fancy here");
   }
}

现在尝试从 Kotlin 代码中调用该方法。如果您使用的是具有自动完成功能的任何代码编辑器,它将自动将方法名称用反引号(` `)括起来:

fun main(args: Array<String>) {
   ASimpleJavaClass.`is`()   
}

Kotlin 中的其他关键字(在 Java 中是合格标识符)也有类似的情况。

它是如何工作的...

根据 Kotlin 的文档,一些 Kotlin 关键字在 Java 中也是有效的标识符:inobjectis 等。如果一个 Java 库使用 Kotlin 关键字作为方法,您仍然可以调用该方法,使用反引号(`)进行转义。

以下是在 Kotlin 中的关键字:

package as typealias class this super val
var fun for null true false is
in throw return break continue object if
try else while do when interface typeof

使用 "as" 关键字局部重命名冲突实体以消除歧义

消除歧义是指通过使某事物清晰来消除歧义。在代码中导入库或类是程序员的日常任务。多亏了现在的优秀代码编辑器,将文件导入代码在每种语言中都变得非常容易。

然而,如果您尝试将两个类导入到一个文件中会发生什么?尽管您应该始终为不同的类使用不同的名称,但有时这是不可避免的。例如,在库的类具有相同名称的情况下。在 Java 中,有一个解决方案;您必须使用完全限定符,看起来像这样:

class X {
   com.very.very.long.prefix.bar.Foo a;
   org.other.very.very.long.prefix.baz.Foo b;
   ...
}

脏,不是吗?现在,让我们看看 Kotlin 如何优雅地解决这个问题。

准备工作

确保您有一个代码编辑器,可以在其中编写和运行代码。为了测试,您可以创建两个具有相同名称但位于不同包中的类。请参考此处的示例:

图片

如何做到...

在以下步骤和示例中,我们将看到如何使用 Kotlin 的关键字消除具有相似名称的类的歧义。

  1. 在 Kotlin 中,您可以使用 as 关键字来消除歧义,局部重命名冲突实体。所以,在 Kotlin 中,它看起来会像这样:
import foo.Bar // Bar is accessible
import bar.Bar as bBar // bBar stands for 'bar.Bar'
  1. 然后,像这样访问它们的方法:
Bar.methodOfFooBar()
bBar.methodOfBarBar()

例如,让我们看看如何使用 as 关键字消除两个具有相同名称(SomeClass.kt)但位于不同包中的类的歧义:

SameClass.kt (packageA)

package packageA
class SameClass {
  companion object {
      fun methodA(){
          println("Method a")
      }
  }
}

SameClass.kt (packageB)

package packageB
class SameClass {
  companion object {
      fun methodB(){
          println("Method b")
      }
  }
}

HelloWorld.kt 是使用具有相似名称的类的类:

import packageA.SameClass as anotherSameClass
import packageB.SameClass
fun main(args: Array<String>) {
   anotherSameClass.methodA()
   SameClass.methodB()

}

在 Kotlin 中进行位操作

Kotlin 提供了几个函数(以中缀形式)来执行位和位移操作。在本节中,我们将通过示例学习如何在 Kotlin 中执行位级操作。

位和位移运算符仅用于两种整型——Int 和 Long——以执行位级操作。

准备工作

这是位操作(仅适用于 Int 和 Long)的完整列表:

  • shr(bits): 有符号右移(Java 的 >>)

  • ushr(bits): 无符号右移(Java 的 >>>)

  • and(bits): 按位与

  • or(bits): 按位或

  • xor(bits): 位异或

  • inv(): 位反转

如何做到...

让我们看看几个示例来理解位运算。

或者

or 函数比较两个值的对应位。如果两个位中的任意一个是 1,则返回 1,如果不是,则返回 0。

考虑以下示例:

fun main(args: Array<String>) {
  val a=2
  val b=3
  print(a or b)
}

下面是输出结果:

 3

下面是前面示例的解释:

2 = 10(二进制格式)

3 = 11(二进制格式)

2 和 3 的位或

二进制

10 OR 11

11 = 3(十进制格式)

and

and 函数比较两个值的对应位。如果两个位中的任意一个是 0,则返回 0,如果不是且两个位都是 1,则返回 1。

考虑以下示例:

fun main(args: Array<String>) {
  val a=2
  val b=3
  print(a and b)
}

这是输出结果:

 2

让我们看看解释:

2 = 10(二进制格式)

3 = 11(二进制格式)

2 和 3 的位与

二进制

10 AND 11

10 = 2(十进制格式)

xor

xor 函数比较两个值的对应位。如果对应位相同,则返回 0,如果不同,则返回 1。

看看这个示例:

fun main(args: Array<String>) {
  val a=2
  val b=3
  print(a xor b)
}

下面是输出结果:

 1

下面是解释:

2 = 10(二进制格式)

3 = 11(二进制格式)

2 和 3 的位异或

二进制

10 XOR 11

01 = 1(十进制格式)

inv

inv 函数简单地反转位模式。如果位是 1,则将其变为 0,反之亦然。

下面是一个示例:

fun main(args: Array<String>) {
    val a=2
   print(a.inv())}

这是输出结果:

 -3

下面是解释:

2 = 10(二进制格式)

2 的位补码为 01,但编译器显示的是该数的 2 的补码,即二进制数的负表示法。

整数 n 的 2 的补码等于 -(n+1)。

shl

shl 函数将位模式向左移动指定的位数。

考虑以下示例:

fun main(args: Array<String>) {
       println( 5 shl 0)
       println( 5 shl 1)
       println( 5 shl 2)
}

这是输出结果:

5
10
20

下面是解释:

5 = 101(二进制格式)

101 左移 0 位 = 101

101 左移 1 位 = 1010(十进制中的 10)

101 左移 2 位 = 10100(十进制中的 20)

shr

shr 函数将位模式向右移动指定的位数。

考虑以下示例:

fun main(args: Array<String>) {
       println( 5 shr 0)
       println( 5 shr 1)
       println( 5 shr 2)
}

这里是输出结果:

5
2
1

下面是解释:

5 = 101(二进制格式)

101 右移 0 位 = 101

101 右移 1 位 = 010(十进制中的 2)

101 右移 2 位 = 001(十进制中的 1)

ushr

ushr 函数将位模式向右移动指定的位数,用 0 填充最左边的位。

下面是一个示例:

fun main(args: Array<String>) {
       println( 5 ushr 0)
       println( 5 ushr 1)
       println( 5 ushr 2)
}

这将输出以下内容:

5
2
1

这是它的解释:

5 = 101(二进制格式)

101 右移 0 位 = 101

101 右移 1 位 = 010(十进制中的 2)

101 右移 2 位 = 001(十进制中的 1)

它是如何工作的...

Kotlin 中的位运算符不是像 Java 中的内置运算符,但它们仍然可以用作运算符。为什么?看看它的实现:

public infix fun shr(bitCount: Int): Int

你可以看到该方法具有 infix 表示法,这使得它可以作为 infix 表达式调用。

将字符串解析为 Long、Double 或 Int

Kotlin 使得将字符串解析为其他数据类型(如 Long、Integer 或 Double)变得非常容易。

在 JAVA 中,使用 Long.parseLong()Long.valueOf() 静态方法,它将字符串参数解析为有符号的十进制长整型并返回一个长整型值,对于其他数据类型如 Int、Double 和 Boolean 也是如此。让我们看看如何在 Kotlin 中实现它。

准备工作

您只需要一个 Kotlin 编辑器来编写和运行您的代码。我们将使用长整型的转换作为示例来讨论使用字符串的解析。转换到其他数据类型相当类似。

如何做到这一点...

要将字符串解析为长整型数据类型,我们使用字符串的 .toLong() 方法。它将字符串解析为一个长整型数字并返回结果。如果字符串不是数字的有效表示,则会抛出 NumberFormatException。稍后我们将看到这个示例的例子。

将字符串转换为 Long

这里有一个示例,展示了将字符串解析为长整型的过程:

fun main(args: Array<String>) {
  val str="123"
  print(str.toLong())
}

当您运行前面的代码时,您将看到以下输出:

123

如果您不想处理异常,可以使用 .toLongOrNull()。此方法将字符串解析为 Long 并返回结果,如果字符串不是数字的有效表示,则返回 null。

使用 string.toLongOrNull() 将字符串转换为 Long

在这个示例中,我们将看到如何使用 .toLongOrNull() 方法解析字符串:

fun main(args: Array<String>) {
  val str="123.4"
  val str2="123"
  println(str.toLongOrNull())
  println(str2.toLongOrNull())
}

运行前面的程序,将生成以下输出:

 null 123

使用特殊基数进行转换

所有的前面示例都使用了基数(基数)10。有些情况下,我们希望将字符串转换为 Long,但使用另一个基数。string.toLong()string.toLongOrNull() 都可以接收一个自定义的基数用于转换。让我们看看它的实现:

  • string.toLong(radix):

    • 这会将字符串解析为一个 [Long] 数字并返回结果

    • 如果字符串不是数字的有效表示,则抛出 NumberFormatException

    • [radix] 不是字符串转换为数字的有效基数时,抛出 IllegalArgumentException

  • string.toLongOrNull(radix):

    • 这会将字符串解析为一个 [Long] 数字并返回结果,或者如果字符串不是数字的有效表示,则返回 null

    • [radix] 不是字符串转换为数字的有效基数时,抛出 IllegalArgumentException

使用特殊基数解析字符串到 Long

在前面的示例中,我们使用基数 10 解析字符串,即十进制。默认情况下,基数取为 10,但在某些情况下我们需要不同的基数。例如,将字符串解析为二进制或八进制数字的情况。因此,现在我们将看到如何处理非十进制的基数。虽然您可以使用任何有效的基数,但我们将展示最常用的示例,如二进制和八进制。

  • 二进制:由于二进制数由 0 和 1 组成,因此使用的基数是 2:
fun main(args: Array<String>) {
       val str="11111111"
       print(str.toLongOrNull(2))   }

运行前面的程序,将生成以下输出:

 255
  • 八进制:八进制数制,简称八进制,是基数为 8 的数制,使用数字 0 到 7。因此,我们将使用 8 作为基数:
fun main(args: Array<String>) {
      val str="377"
       print(str.toLongOrNull(8))
   }

运行前面的程序,将生成以下输出:

 255
  • 十进制:十进制系统中有 10 个数字(0-9);因此,我们将使用 10 作为基数。请注意,没有基数参数的方法(.toLong() , .toLongOrNull())默认使用基数 10:
fun main(args: Array<String>) {
      val str="255"
       print(str.toLongOrNull(10))
   }

运行前面的程序,将生成以下输出:

 255

它是如何工作的...

Kotlin 使用如 .toLong()toLongOrNull() 这样的字符串扩展函数来简化操作。让我们深入了解它们的实现。

  • 对于 Long 类型,使用此方法:
public inline fun String.toLong(): Long = java.lang.Long.parseLong(this)

如您所见,它内部也调用了 Long.parseLong(string) Java 静态方法,并且与其他数据类型类似。

  • 对于 Short 类型,它是以下内容:
public inline fun String.toShort(): Short = java.lang.Short.parseShort(this)
  • 使用此方法进行 Int 解析:
public inline fun String.toInt(): Int = java.lang.Integer.parseInt(this)
  • 对于使用基数进行解析,请使用以下方法:
public inline fun String.toLong(radix: Int): Long = java.lang.Long.parseLong(this, checkRadix(radix))

checkRadix 方法检查给定的 [radix] 是否是字符串到数字和数字到字符串转换的有效基数。

还有更多...

让我们快速查看 Kotlin 提供的几个其他扩展函数,用于解析字符串:

  • toBoolean(): 如果此字符串的内容等于单词 true(忽略大小写),则返回 `true`,否则返回 `false`

  • toShort(): 将字符串解析为 [Short] 数字并返回结果。如果字符串不是数字的有效表示,则抛出 NumberFormatException

  • toShort(radix): 将字符串解析为 [Short] 数字并返回结果,如果字符串不是数字的有效表示,则抛出 NumberFormatException,如果 [radix] 不是字符串到数字转换的有效基数,则抛出 IllegalArgumentException

  • toInt(): 将字符串解析为 [Int] 数字并返回结果,如果字符串不是数字的有效表示,则抛出 NumberFormatException

  • toIntOrNull(): 将字符串解析为 [Int] 数字并返回结果,如果字符串不是数字的有效表示,则返回 `null`

  • toIntOrNull(radix): 将字符串解析为 [Int] 数字并返回结果,如果字符串不是数字的有效表示,则返回 `null`,如果 [radix] 不是字符串到数字转换的有效基数,则抛出 IllegalArgumentException

  • toFloat(): 将字符串解析为 [Float] 数字并返回结果,如果字符串不是数字的有效表示,则抛出 NumberFormatException

  • toDouble() : 将字符串解析为 [Double] 数字并返回结果,如果字符串不是数字的有效表示,则抛出 NumberFormatException

在 Kotlin 中使用字符串模板

Kotlin 将许多常用数据类型字符串的强大功能打包在一起。其中一个非常酷的特性是字符串模板。此功能允许字符串包含模板表达式。

在 Java 中,你必须使用 StrSubstitutor (commons.apache.org/proper/commons-text/javadocs/api-release/org/apache/commons/text/StrSubstitutor.html) 和相应的映射。Java 中的模板表达式将如下所示:

Map<String, String> valuesMap = new HashMap<String, String>();
valuesMap.put("city", "Paris");
valuesMap.put("monument", "Eiffel Tower");
String templateString ="Enjoyed ${monument} in ${city}.";
StrSubstitutorsub=newStrSubstitutor(valuesMap);
String resolvedString =sub.replace(templateString);

Kotlin 简化了编写模板表达式的痛苦,使其变得有趣、简洁,并且不那么冗长。

使用字符串模板,你可以在不进行字符串连接的情况下将变量或表达式嵌入到字符串中。所以,让我们开始吧!

如何做到这一点...

在接下来的步骤中,我们将学习如何使用字符串模板:

  1. 在 Kotlin 中,模板表达式以 $ 符号开始。

  2. 字符串模板的语法如下:

$variableName

或者,它也可以是这样的:

${expression}
  1. 让我们看看几个例子:
  • 考虑一个带有变量的字符串模板的例子:
fun main(args: Array<String>) {
    val foo = 5;
    val myString = "foo = $foo"
    println(myString)
 }

上述代码的输出将是foo = 5

  • 考虑一个带有表达式的字符串模板的例子:
fun main(arr: Array<String>){
  val lang = "Kotlin"
  val str = "The word Kotlin has ${lang.length} characters."
  println(str)
}
  • 考虑一个带有原始字符串的字符串模板的例子:

    • 原始字符串:一个由换行符组成且没有使用 \n 的任意字符串。它是一个原始字符串,并放置在三个引号(""")中:
fun main(args: Array<String>) {
    val a = 5
    val b = 6

    val myString = """
    ${if (a > b) a else b}
 """
    println("Bigger number is: ${myString.trimMargin()}")
 }

当你运行程序时,输出将是Bigger number is: 6

它是如何工作的...

使用变量名称的字符串模板的使用相当简单。以前,我们通常使用字符串连接,但现在我们只需在变量前指定 $ 符号即可。

当字符串模板用作表达式时,${..} 内的表达式首先被评估,然后将值与字符串连接。在先前的例子(带有原始字符串的字符串模板)中,${if (a > b) a else b} 表达式被评估,其值,即 6,与字符串一起打印。

还有更多...

字符串模板在字符串属性和函数中也很有用。以下是一个例子:

fun main(args: Array<String>) {
      val str1="abcdefghijklmnopqrs"
       val str2="tuvwxyz"
       println("str1 equals str2 ? = ${str1.equals(str2)}")
       println("subsequence is ${str1.subSequence(1,4)}")
       println("2nd character is ${str1.get(1)}")
   }

这里是输出结果:

str1 equals str2 ? = false
subsequence is bcd
2nd character is b

第二章:控制流

本章将涵盖以下内容:

  • 使用 if 关键字将结果分配给表达式

  • 使用 when 表达式与范围

  • 使用 when 与自定义对象

  • trycatch 作为表达式使用

  • 如何使用 also 函数在 Kotlin 中编写交换函数

  • 如何在 Kotlin 中抛出自定义异常

  • 如何在 Kotlin 中创建多条件循环

简介

控制流是每种编程语言的基本构建块。在 Kotlin 中有什么不同之处在于,您可以使用其中的一些控制流作为表达式,例如 trycatchifelsewhen 等。在本章中,我们将了解 Kotlin 提供的一些控制流,并学习如何使用它们。此外,我们还将了解它们如何比 Java 控制流提供更多的功能。所以,让我们开始吧!

使用 if 关键字将结果分配给表达式

在 Kotlin 中,if 是特殊的,因为它返回值。这就是为什么我们可以使用 if 语句来分配值。这消除了 Kotlin 中三元运算符的需要。让我们看看我们如何使用 if 语句来分配值。

准备工作

您需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行来完成此目的,为此您需要安装 Kotlin 编译器和 JDK。在这个菜谱中,我使用命令行来编译和运行我的 Kotlin 代码。

如何做...

创建一个文件,并将其命名为 ifWithKotlin.kt。您可以将其命名为任何名称;它不必与类名相同,因为它是在 Java 中。现在,为了开始,您应该始终声明 main 方法,因为 Java 虚拟机通过调用指定类的 main 方法来启动执行。

main 方法如下:

fun main(args: Array<String>) { }
  1. 让我们尝试一个基本的 if 语句,以了解它是如何工作的:
fun main(args: Array<String>) {
     var x:Int
     if(10>20){
         x = 5
     }
     else{
         x = 10
     }
     println("$x")
 }

在这个代码块中,我们在 ifelse 块中为 x 分配一个值,然后打印它。

  1. 现在,让我们以 Kotlin 的方式尝试同样的事情:
fun main(args: Array<String>) {
      var x:Int = if(10>20)  5  else  10
    println("$x")
 }

在这个代码块中,我们将 ifelse 块返回的值分配给 x。注意我们如何将 if 语句用作表达式右侧的表达式的一部分。

  1. 让我们看看我们还能做什么。在以下示例中,我们将尝试使用 if 语句从表达式中返回一些内容:
fun main(args: Array<String>) {
     var x:Int
    x = if(10>20) {
             doSomething()
             25
         }
         else if (12<13) {
             26
         }
         else{
             27
         }
         println("$x")
}
fun doSomething() {
     var a = 6
     println("$a")
 }

注意我们如何使用了整个 ifelse 块。在这种情况下,if 块返回块中的最后一个语句。

  1. 最后,让我们尝试一个更复杂的例子,使用嵌套的ifelse。这将帮助我们理解在嵌套的ifelse结构中值的返回方式:
fun main(args: Array<String>) {
    var x:Int
    x = if(10<20) {
        if(4 == 3){
            56
        }
        else{
            96
        }
    }
    else if (12>13) {
        26
    }
    else{
        27
    }
    println("$x")
}

//Output: 96

因此,如果我们嵌套一个ifelse块,并且该ifelse块的最后一个语句又是另一个ifelse语句,那么嵌套的ifelse返回的值将由外层的ifelse返回。正如你所看到的,96是由if(10<20)块内部的else块返回的。

  1. 如果ifelse块不是最后一个语句,会发生什么,就像这个例子一样:
 fun main(args: Array<String>) {
 var x:Int
 x = if(10<20) {

         if(4 == 3){
                 56
         }
         else{
                 96
         }
         565
     }
     else if (12>13) {
        26
     }
     else{
         27
     }
     println("$x")
}

很明显,嵌套的if-else返回的值没有被使用,Kotlin 编译器也警告了我们这一点。原因在于if-else块不是父if-else块的最后一个语句,这就是为什么返回的值没有被使用的原因。

图片

尝试调整值和逻辑,看看您还能用if-else做些什么。

总是要记住的关键点是if-else块的最后一个语句会被返回,这就是为什么它可以用来给任何变量赋值的原因。

还有更多...

我们在打印语句中使用了字符串模板。注意我们是如何在变量名前使用$符号来访问变量的:

println("$a is a number something”)

我们还可以在要评估并其结果连接到字符串的字符串中放入一段代码。在这种情况下,$后面跟着{},我们在其中放入我们的代码:

println("some variable whose value: ${if(a < 100) 25 else 29}")

使用when表达式与范围

在 Kotlin 中,when就像一个超级强大的switch控制语句。然而,这还不是它的全部。您可以用when语句构建很多令人惊叹的逻辑,其中一个例子就是使用范围与when语句。我们将在本菜谱中查看这一点。

准备工作

您需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行来完成此目的,为此您需要安装 Kotlin 编译器和 JDK。我在这个菜谱中使用命令行来编译和运行我的 Kotlin 代码。

如何做到这一点...

首先,让我们创建一个文件,命名为whenWithRanges.kt,并按照以下步骤操作:

  1. 让我们尝试一个基本的when语句来了解它是如何工作的:
fun main(args: Array<String>) {
     val x = 12
     when(x){
         12 -> println("x is equal to 12")
         4 -> println("x is equal to 4")
         else -> println ("no conditions match!")
     }
 }

所以基本上,这个代码块的工作方式就像一个switch语句,它也可以使用if语句来实现。

  1. 现在,让我们看看x是否位于110之间或之外:
fun main(args: Array<String>) {
     val x = 12
     when(x){
         in (1..10) -> println("x lies between 1 to 10")
         !in (1..10) -> println("x does not lie between 1 to 10")
     }
 }
  1. 让我们看看我们还能做什么。在下面的例子中,我们将处理可以在when语句内部使用的不同类型的条件:
fun main(args: Array<String>) {
     val x = 10
     when(x){
         magicNum(x) -> println("x is a magic number")
         in (1..10) -> {
             println("lies between 1 to 10, value: ${if(x < 20) x else 0}")
         }
         20,21 -> println("$x is special and has direct exit access")
         else -> println("$x needs to be executed")
     }
}
fun magicNum(a: Int): Int {
 return if(a in (15..25)) a else 0
 }
  1. 最后,让我们尝试一个更复杂的使用数据类的例子。在这个例子中,我们将看到如何使用对象与when结合:
fun main(args: Array<String>) {
     val x = ob(2, true, 500)
     when(x.value){
         magicNum(x.value) -> println("$x is a magic number and         ${if(x.valid) "valid" else "invalid"}")
         in (1..10) -> {
             println("lies between 1 to 10, value: ${if(x.value <           x.max) x.value else x.max}")
         }
         20,21 -> println("$x is special and has direct exit access")
         else -> println("$x needs to be executed")
     }
 }
 data class ob(val value: Int, val valid: Boolean, val max: Int)
 fun magicNum(a: Int): Int {
 return if(a in (15..25)) a else 0
 }

这是编译和运行程序后的样子:

图片

尝试调整值和逻辑,看看您还能用 Kotlin 中这样一小段代码的when做些什么。

它是如何工作的...

在前面的例子中,第一个例子是最基本的when语句;我们直接比较x的值与124,如果没有条件匹配,我们只是简单地执行else语句。这就像一个if else if else语句。

在第二个例子中,我们在 when 块的第一个语句中检查 x 是否位于 110 之间,在第二个语句中,我们检查 x 是否不位于 110 之间。这就是我们在 when 中处理范围的方式。基本上,在 when 中,我们可以使用 in 关键字检查 x 是否位于一个范围内或存在于一个集合中。语法如下:

when(x) {
    In collection_or_range -> // do something
}

在第三个例子中,我们使用一个函数来检查 x 是否等于表达式 magicNum(x) 的值。因此,我们也可以使用表达式和函数来代替常量和范围来比较 x

在第四个例子中,我们使用数据类而不是原始数据类型来探索 when 语句的强大功能。注意我们如何在 when 内部访问 x 的所有属性,并且还可以与之交互。

更多...

我们已经看到了如何在打印语句中使用带有表达式的字符串模板。记得我们是如何能够使用变量名前的 $ 符号来访问变量的,对吧:

println("$x is a magic number”)

我们还可以将一段代码放入一个字符串中,然后该字符串将被评估,其结果将连接到字符串中。在这种情况下,$ 后面跟着 {},我们在其中放入我们的代码:

println("lies between 1 to 10, value: ${if(x.value < x.max) x.value else x.max}")

使用 when 与自定义对象

在 Kotlin 中,when 已经非常强大了,但你是否知道你还可以在 when 中使用自定义对象?太棒了,对吧?让我们来实施它。

准备工作

您需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行来完成此目的,为此您需要安装 Kotlin 编译器和 JDK。我正在使用命令行来编译和运行我的 Kotlin 代码来完成这个菜谱。

如何实现...

创建一个文件,并将其命名为 whenWithObject.kt,然后,让我们尝试使用自定义对象来使用 when。在这个例子中,我们将创建一个具有一些属性的对象,并尝试在 when 语句中匹配它:

fun main(args: Array<String>) {
     val x = ob(2, true, 500)
     when(x){
         ob(2, true, 500) -> println("equals correct object")
         ob(12, false, 800) -> {
             println("equals wrong object")
         }
         else -> println("does not match any object")
     }
 }
data class ob(val value: Int, val valid: Boolean, val max: Int)

以下是前面代码块的结果:

图片

如果您尝试在 when 中比较不同的对象类型,它将抛出一个错误 error: incompatible types,因为我们正在尝试比较不同类型的对象。

它是如何工作的...

在 Kotlin 中,when 在后台基本上是通过相等性来工作的,因此只要它们的类型相同,我们就可以比较对象。

使用 try–catch 作为表达式

与 Java 相比,Kotlin 中的异常既有相似之处也有不同之处。在 Kotlin 中,Throwable 是所有异常的超类,每个异常都有一个堆栈跟踪、消息和一个可选的原因。

trycatch 的结构也与 Java 中使用的结构相似。在 Kotlin 中,这是一个 trycatch 语句的外观:

try {
 // some code to execute
 }
 catch (e: SomeException) {
 // exception handler
 }
 finally {
 // optional finally block 
 }

至少需要一个 catch 块,而 finally 块是可选的,因此可以省略。

在 Kotlin 中,trycatch 是特殊的,因为它允许它作为一个表达式使用。在这篇文章中,我们将看到我们如何使用 trycatch 作为表达式。

准备工作

你需要安装一个首选的开发环境,用于编译和运行 Kotlin。你也可以使用命令行来完成这个任务,你需要安装 Kotlin 编译器和 JDK。我使用 IntelliJ IDE 来编译和运行我的 Kotlin 代码来完成这个菜谱。

如何做...

让我们编写一个简单的程序,它接受一个数字作为输入并将其值赋给一个变量。如果输入的值不是一个数字,我们捕获NumberFormatException异常并将-1赋给该变量:

fun main(args: Array<String>) {
 val str="23"
 val a: Int? = try { str.toInt() } catch (e: NumberFormatException) { -1 }
 println(a)
 }

这是输出结果:

Output: 23

现在,让我们尝试一些疯狂的事情,故意尝试抛出异常:

fun main(args: Array<String>) {
 val str="abc"
 val a: Int? = try { str.toInt() } catch (e: NumberFormatException) { -1 }
 println(a)
 }

这是输出结果:

Output: -1

使用trycatch在边缘情况下会非常有帮助,因为它们可以用作表达式。

它是如何工作的...

我们可以使用trycatch作为表达式的原因是,在 Kotlin 中trythrow都是表达式,因此可以被赋值给变量。

当你使用trycatch作为表达式时,trycatch块的最后一行会被返回。这就是为什么在第一个例子中我们得到了返回值23,而在第二个例子中我们得到了-1

这里,需要注意的是,同样的情况并不适用于finally块——也就是说,编写finally块不会影响结果:

fun main(args: Array<String>) {
     val str="abc"
     val a:Int = try {
                    str.toInt()
                 } catch (e: NumberFormatException) {
                      -1
                 } finally {
                      -2
                 }
      println(a)
 }
Output: -1

如您所见,编写finally块并不会改变任何东西。

还有更多...

在 Kotlin 中,所有的异常都是未检查的,这意味着我们根本不需要使用trycatch。这与 Java 非常不同,在 Java 中,如果一个方法抛出异常,我们需要用trycatch包围它。

这里是一个 Kotlin 中的 IO 操作的示例:

fun fileToString(file: File) : String {
 //readAllBytes throws IOException, but we can omit catching it
 fileContent = Files.readAllBytes(file)
 return String(fileContent)
 }

如您所见,如果我们不想使用trycatch,我们不需要这样做。在 Java 中,如果我们不处理这个异常,我们无法继续进行。

如何在 Kotlin 中使用also函数编写交换函数

交换两个数字是你在编程中做的最常见的事情之一。大多数方法在本质上都很相似:要么你使用一个第三方变量,要么使用指针。

在 Java 中,我们没有指针,所以我们主要依赖于第三方变量。

你当然可以使用这里提到的方法,这只是 Java 代码的 Kotlin 版本:

var a = 1
var b = 2
run { val temp = a; a = b; b = temp }
println(a) // print 2
println(b) // print 1

然而,在 Kotlin 中,有一个非常快速且直观的方式来完成它。让我们看看如何做!

准备工作

你需要安装一个首选的开发环境,用于编译和运行 Kotlin。你也可以使用命令行来完成这个任务,你需要安装 Kotlin 编译器和 JDK。我使用 IntelliJ IDE 来编译和运行我的 Kotlin 代码来完成这个菜谱。

如何做...

在 Kotlin 中,我们有一个特殊的功能,also,我们可以用它来交换两个数字。以下是相应的代码:

var a = 1
var b = 2
a = b.also { b = a }
println(a) // print 2
println(b) // print 1

我们能够在不使用任何第三方变量的情况下实现相同的功能。

它是如何工作的...

为了理解前面的示例,我们需要了解 Kotlin 中的 also 函数。also 函数接受接收者,执行一些操作,并返回接收者。简单来说,它传递一个对象并返回相同的对象。

在对象上应用 also 函数就像对那个对象说“也这样做”一样。

因此,我们在 b 上调用了 also 函数,执行了一个操作(将 a 的值赋给 b),然后返回了作为参数得到的相同接收者:

var a = 1
 var b = 2
a = b.also {
       b = a  // p
       println("it=$it : b=$b : a=$a") // prints it=2:b=1:a=1
     }
println(a) // print 2
println(b) // print 1

还有更多...

apply 函数与 also 函数非常相似,但它们之间有一个细微的差别。为了理解这一点,让我们首先看看它们的实现:

  • also 函数:
public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }
  • apply 函数:
public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }

also 中,块被定义为 (T) -> Unit,但在 apply() 中被定义为 T.() -> Unit,这意味着在 apply 块内部有一个隐式的 this。然而,要在 also 中引用它,我们需要 it

所以,使用 also 的代码将看起来像这样:

val result = Dog(12).also { it.age = 13 }

使用 apply 的代码将看起来像这样:

val result2 =Dog(12).apply {age = 13 }

在这两种情况下,结果对象的时代将相同,即 13

如何在 Kotlin 中抛出自定义异常

有时,您可能想创建自己的异常。如果您创建自己的异常,它被称为自定义异常用户定义异常

这些用于根据特定需求定制异常,使用这些,您可以拥有自己的异常和消息。在这个菜谱中,我们将看到如何在 Kotlin 中创建和抛出自定义异常。

准备工作

您需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行来完成此目的,为此您需要安装 Kotlin 编译器和 JDK。我使用 IntelliJ IDE 来编译和运行我的 Kotlin 代码,以完成这个菜谱。

如何做到这一点...

所有的异常都有 Exception 作为它们的超类,因此我们需要扩展那个类。

这就是我们的自定义异常的样子:

class CustomException(message:String): Exception(message)

由于 Exception 超类有一个可以接受消息的构造函数,我们通过 CustomException 的构造函数的帮助传递了它。

现在,如果您必须 throw 一个 Exception,您只需做以下操作:

throw CustomException("Threw custom exception")

输出将类似于这样:

它是如何工作的...

让我们看看 Exception 类的实现:

public class Exception extends Throwable {
 static final long serialVersionUID = -3387516993124229948L;
public Exception() {
 }
public Exception(String var1) {
     super(var1);
 }.....

如您所见,我们有一个接受 String 参数的第二个构造函数。在我们的 CustomException 类中,我们通过将其消息传递给超类构造函数来提供它。此外,您还可以使用空构造函数创建自定义异常,因为 Exception 也有空构造函数。

如何在 Kotlin 中创建多条件循环

条件循环是任何编程语言都有的常见特性。如果你在循环中应用多个条件,那么它被称为多条件循环。这里以 Java 为例,展示了多条件循环的一个简单示例:

int[] data = {5,6,7,1,3,4,5,7,12,13};
 for(int i=0;i<10&&i<data[i];i++){
     System.out.println(data[i]);
 }

在执行之前,前面的代码将打印出567。让我们看看如何在 Kotlin 中使用多条件循环。我们将探讨 Kotlin 中相同问题的函数式方法。

准备工作

您需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行来完成此目的,为此您需要安装 Kotlin 编译器和 JDK。我使用 IntelliJ IDE 来编译和运行我的 Kotlin 代码,以完成此菜谱。

如何操作...

上述多条件循环可以用 Kotlin 这样写:

(0..9).asSequence().takeWhile {
     it<numbers[it]
 }.forEach 
    println("$it - ${data[it]}")
 }

它很整洁,干净,绝对不是一种视觉上的负担。

它是如何工作的...

我们使用了takeWhile,它返回一个包含满足给定谓词的第一个元素的序列(在这种情况下,i<data[i])。

虽然takeWhile返回满足给定谓词的第一个元素,但您可能会想它将首先评估整个范围,然后传递给forEach。如果不是我们使用了.asSequence(),这种情况就会发生。我们将范围转换为Sequence<T>,因此它是惰性评估的。简而言之,它不会使用.takeWhile { ... }处理整个集合,而是在.forEach { ... }准备处理下一个项目时逐个检查它们。

让我们通过一个示例来尝试理解这一点。首先,我们将对Iterable<T>进行贪婪评估。

这是贪婪版本,它会在移动到下一个函数之前先评估第一个函数:

(0..9).takeWhile {
     println("Inside takeWhile")
     it<numbers[it]
 }.forEach {
     println("Inside forEach")
 }

这是输出:

Inside takeWhile
 Inside takeWhile
 Inside takeWhile
 Inside takeWhile
 Inside forEach
 Inside forEach
 Inside forEach

如您所见,范围首先通过takeWhile(返回 0, 1, 2)进行处理,然后发送到forEach进行处理。

现在,让我们看看惰性版本:

(0..9).asSequence().takeWhile {
     println("Inside takeWhile")
     it<numbers[it]
 }.forEach {
     println("Inside forEach")
 }

这是输出:

Inside takeWhile
 Inside forEach
 Inside takeWhile
 Inside forEach
 Inside takeWhile
 Inside forEach
 Inside takeWhile

如您在先前的示例中所见,takeWhile仅在forEach用于处理项目时才会被评估。这是Sequence<T>的本质,它在可能的情况下执行惰性操作。

第三章:类和对象

本章将涵盖以下食谱:

  • 初始化构造函数的主体

  • 将一种数据类型转换为另一种类型

  • 如何检查一个对象的数据类型

  • 如何在 Kotlin 中使用抽象类

  • 如何在 Kotlin 中遍历类的属性

  • 如何使用内联属性

  • 如何使用嵌套类

  • 在 Kotlin 中获取类

  • 使用委托属性

  • 使用枚举

简介

在本章中,你将了解与 Kotlin 面向对象编程相关的食谱。使用面向对象的方法,你可以通过创建对象将复杂问题分解为更小的问题。与 Java 相比,Kotlin 的 OOP 风格有一些不同——例如,在 Kotlin 中,所有类默认都是封闭的(final),如果你想使它们可扩展,你需要使用open关键字。不仅对于类——默认情况下,方法也是 final 的,你需要使用open关键字。使用 Kotlin,处理类和对象所需的代码更少。哦!顺便说一句,我告诉你我们甚至不需要使用new关键字来创建对象了吗?所以,在 Kotlin 中创建新对象就像这样:

var person=Person()

上述代码将创建一个可变类型的Person对象,因为我们使用了var作为修饰符。可变对象意味着它可以改变其值。如果你想创建一个不可变对象,你可以使用val关键字。所以同样的例子将如下所示:

val person=Person()

因此,让我们开始查看一些有助于你在 Kotlin 中进行面向对象编程的食谱。

初始化构造函数的主体

在 Java 世界中,我们通常在构造函数中初始化类的字段,如下面的代码所示:

class Student{
 int roll_number;
 String name;
 Student(int roll_number,String name){
     this.roll_number =roll_number;
     this.name = name;
 }
}

因此,如果参数的名称与属性的名称相似(通常为了使代码更易读),我们需要使用this关键字。在这个食谱中,我们将看到如何在 Kotlin 中实现相同的功能(显然代码更少)。

准备工作

你需要一个 IDE 来编写和执行你的代码。我将使用 IntelliJ IDEA。我们将创建一个具有nameroll_number属性的Student类。

如何做到...

让我们看看初始化构造函数的步骤:

  1. Kotlin 提供了一种语法,可以用更少的代码初始化属性。以下是 Kotlin 中类初始化的示例:
class Student(var roll_number:Int, var name:String)
  1. 你甚至不需要定义类的主体,属性的初始化仅在主构造函数中发生(主构造函数是类头的一部分)。显然,你可以根据是否需要保持属性可变来选择varval。现在,如果你尝试创建一个对象,你可以用以下方式做到:
var student_A=Student(1,"Rashi Karanpuria")
  1. 为了确认,让我们尝试打印其属性以查看我们是否能够初始化它:
println("Roll number: ${student_A.roll_number} Name: ${student_A.name}")

这是输出:

 Roll number: 1 Name: Rashi Karanpuria
  1. 然而,如果你想的话,你还可以在构造函数中放置默认值:
class Student constructor(var roll_number:Int, var name:String="Sheldon")
  1. 然后,你可以创建如下对象:
var student_sheldon= Student(25)   // Object with name Sheldon and age 25

var student_amy=Student(25, "Amy")     // Object with name Amy and age 25
  1. 如果类有一个主构造函数,每个次级构造函数都需要委托给主构造函数,无论是直接还是通过另一个次级构造函数(s)间接地。

  2. 我们使用此关键字来委托给同一类的另一个构造函数:

class Person(val name: String) {
     constructor(name: String, lastName: String) : this(name) {
         // Do something maybe
     }
 }
  1. 我们也可能遇到需要在类中初始化其他事情的情况,而不仅仅是类的属性。这种情况可能是打开数据库连接,例如。在 Java 中,这是在构造函数中完成的,但在 Kotlin 中,我们有init块。初始化代码可以放入init块中:
class Student(var roll_number:Int,var name: String) {
 init {
         logger.info("Student initialized")
     }
 }
  1. 有时,我们也会通过依赖注入来初始化类的属性。如果你使用过 Dagger2,你一定熟悉对象被直接注入到类的构造函数中。为此,我们在构造函数关键字之前添加@Inject注解。每当构造函数有一个注解或可见性修饰符时,我们都需要有constructor关键字。下面是一个constructor关键字的示例:
class Student @Inject constructor(compositeDisposable: CompositeDisposable) { ... }
  1. 在这里,我们正在将CompositeDisposable类型的对象注入到构造函数中,由于我们使用注解(@Inject)来这样做,我们需要应用构造函数关键字。

  2. 当你扩展一个类时,你需要初始化超类。在 Kotlin 中,这也非常简单。如果你的类有一个主构造函数,基类型必须在那里使用主构造函数的参数进行初始化。以下是一个相同的示例:

class Student constructor(var roll_number:Int, var name:String):Person(name)
  1. 然而,有时一个类可能没有主构造函数。在这种情况下,每个次级构造函数必须使用super关键字初始化基类型,或者可以委托给另一个执行此操作的构造函数。此外,不同的次级构造函数可以调用基类型的不同构造函数:
class Student: Person {
 constructor(name: String) : super(name)
constructor(name: String, roll_number: Inte) :super(name)
 }

将一种数据类型转换为另一种类型

在 Java 中,我们通常通过在变量前添加所需类型来进行类型转换,如下所示:

String a = Integer.toString(10)

此外,在 Java 中,数值可以直接转换为更大的数值类型,但在 Kotlin 中,这个特性因为类型安全而不存在——那么在 Kotlin 中如何将一个类型的对象转换为另一个类型的对象呢?我们将在本食谱中看到。

准备工作

你需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。你也可以使用命令行来完成这个任务,这需要安装 Kotlin 编译器和 JDK。我正在使用try.kotlinlang.org/上的在线 IDE 来编译和运行我的 Kotlin 代码,用于本食谱。

如何做到这一点...

让我们按照以下步骤了解如何将一种数据类型转换为另一种类型:

  1. 让我们尝试一个非常基础的例子——尝试将Int转换为LongFloat
fun main(args: Array<String>) {
    var a = 1
    var b: Float = a.toFloat()
    var c = a.toLong()
    println("$a is Int while $b is Float and $c is Long")
}
  1. 类似地,Long可以转换为FloatInt,如下所示:
fun main(args: Array<String>) {
    var a = 1000000000000000000L
    var b: Float = a.toFloat()
    var c = a.toInt()
    println("$a is Long while $b is Float and $c is Integer")
}

上述代码的输出如下所示:

1000000000000000000 is Long while 9.9999998E17 is Float and -1486618624 is Integer
  1. 让我们尝试一个更有趣的转换,使用ByteIntStrings
fun main(args: Array<String>) {
    var a = 15623
    var b: Byte = a.toByte()
    var c = a.toString()
    println("$a is Int while $b is Byte and $c is String")
}

下面是一个可以使用于 Kotlin 中类型转换的方法列表:

  • toByte(): Byte

  • toShort(): Short

  • toInt(): 整数

  • toLong(): 长整数

  • toFloat(): 浮点数

  • toDouble(): 双精度浮点数

  • toChar(): 字符

  • toString(): 字符串

它是如何工作的...

基本上,Kotlin 是一种类型安全的语言,并确保类型不能在语言中直接转换。此外,StringString 不一样?正如预期的那样,没有方法可以将变量转换为布尔类型。从较大的类型转换为较小的类型是可能的,但可能会截断结果值。

如何检查对象的类型

经常需要检查对象在运行时是否为特定类型。在 Java 中,我们使用 instanceof 关键字;在 Kotlin 中,它是 is 关键字。

准备工作

您需要安装一个首选的开发环境,用于编译和运行 Kotlin。您也可以使用命令行,为此您需要安装 Kotlin 编译器和 JDK。我正在使用 try.kotlinlang.org/ 上的在线 IDE 来编译和运行我的 Kotlin 代码,以完成这个菜谱。

如何做...

让我们看看如何按以下步骤检查对象的类型:

  1. 让我们尝试一个非常基础的例子,尝试使用 is 与字符串和整数。在这个例子中,我们将检查一个字符串和一个整数的类型:
fun main(args: Array<String>) {
    var a : Any = 1
    var b : Any = "1"
    if (a is String) {
        println("a = $a is String")
    }
    else {
        println("a = $a is not String")
    }
    if (b is String) {
        println("b = $b is String")
    }
    else {
        println("b = $b is not String")
    }
}

  1. 同样,我们可以使用 !is 来检查对象是否不是 String 类型,如下所示:
fun main(args: Array<String>) {
    var b : Any = 1
    if (b !is String) {
        println("$b is not String")
    }
    else {
        println("$b is String")
    }
}

如果你记得 Kotlin 中 when 的用法,我们就不需要使用 is 关键字,因为 Kotlin 有智能转换的功能,如果比较的对象不是同一类型,则会抛出错误。

它是如何工作的...

基本上,is 操作符用于在 Kotlin 中检查对象的类型,而 !isis 操作符的否定。

Kotlin 编译器跟踪不可变值,并在需要时安全地进行转换。这就是智能转换的工作原理;is 是一个安全转换操作符,而不安全的转换操作符是 as 操作符。

还有更多...

让我们尝试一个使用 as 操作符的例子,它是 Kotlin 中的类型转换操作符。这是一个不安全的转换操作符。以下代码示例会抛出 ClassCastException,因为我们不能将整数转换为字符串:

fun main(args: Array<String>) {
   var a : Any = 1
   var b = a as String
}

另一方面,以下代码由于变量 aAny 类型,可以成功运行,因此可以被转换为 String

fun main(args: Array<String>) {
    var a : Any = "1"
    var b = a as String
    println(b.length)
}

如何在 Kotlin 中使用抽象类

抽象类是不能实例化的类,这意味着我们不能创建抽象类的对象。使用抽象类的主要灵感是我们可以从它们继承。当一个类从抽象类继承时,它实现了父类的所有抽象方法。

准备工作

您需要安装一个首选的开发环境,用于编译和运行 Kotlin。您也可以使用命令行,为此您需要安装 Kotlin 编译器和 JDK。我正在使用 try.kotlinlang.org/ 上的在线 IDE 来编译和运行我的 Kotlin 代码,以完成这个菜谱。

如何做...

现在,让我们看看如何按以下步骤使用 abstract 类:

  1. abstract 关键字用于声明一个 abstract 类。让我们创建一个抽象类并尝试从它继承:
abstract class Mammal {
    abstract fun move(direction: String)
}
  1. 要使一个类成为 Mammal 类的子类,我们使用 : 操作符,如下例所示。请注意在超类方法实现之前使用的 override 关键字:
class Dog : Mammal() {
    override fun move(direction: String) {
        println(direction)
    }
}
  1. 如果我们不希望子类实现某个方法,我们不应将其声明为 abstractopen,如下例所示:
fun main(args: Array<String>) {
    var x = Dog()
    x.move("North")
    println(x.show(123))
}
class Dog : Mammal() {
    override fun move(direction: String) {
        println(direction)
    }
}
abstract class Mammal {
    fun show(y: Int): String {
        return y.toString()
    }
    abstract fun move(direction: String)
}
  1. 如果我们在每个类中声明 init 块,如下所示,我们将得到一个输出,其中超类的 init 块首先被调用:
fun main(args: Array<String>) {
    var x = Dog()
    x.move("North")
    println(x.show(123))
}
class Dog : Mammal() {
    init {
        println ("Hey from Dog")
    }
    override fun move(direction: String) {
        println(direction)
    }
}
abstract class Mammal {
    init {
        println ("Hey from Mammal")
    }
    fun show(y: Int): String {
        return y.toString()
    }
    abstract fun move(direction: String)
}

最终程序的输出如下:

Hey from Mammal
Hey from Dog
North
123

它是如何工作的...

Dog 类是 Mammal 类的子类,并继承其所有方法。声明为 abstract 的方法应由 Dog 类实现。show() 方法在 Mammal 类中,但可以通过 Dog 对象调用,因为创建的对象是 Mammal 类型。

超类的 init 块在子类之前被调用。

如何在 Kotlin 中遍历类的属性

Kotlin 中的反射允许我们在运行时对程序的结构进行自省。这也使我们能够自省类修饰符、方法和属性。

在这个菜谱中,我们将看到如何遍历 Kotlin 类的属性。那么,让我们开始吧!

准备工作

我们将使用 IntelliJ IDEA IDE 进行编码。我们将创建一个 Student 类,它将具有 roll_numbername 属性。然后我们将看到如何遍历其属性。

如果您不使用 IntelliJ IDE 或 Android Studio,您可能需要在类路径中包含反射库。前往 kotlinlang.org/docs/reference/reflection.html 了解更多关于反射的信息。

如何做到这一点...

在以下步骤中,我们将看到如何遍历一个类的属性:

  1. 这里是我们的 Student 类,具有 roll_numberfull_name 属性:
class Student constructor(var roll_number:Int, var full_name:String)
  1. 现在,我们将使用 for 语句,因为我们想遍历一个类可以拥有的多个属性:
fun main(args: Array<String>) {
    var student=Student(2013001,"Aanand Shekhar Roy")
    for (property in Student::class.memberProperties) {
        println("${property.name} = ${property.get(student)}")
    }
}

这是输出结果:

full_name = Aanand Shekhar Roy
roll_number = 2013001

它是如何工作的...

实现相当简单。我们能够实现对类属性的检查,因为我们使用了反射,而 memberProperties 只是 KClass 的许多函数之一。

需要注意的一点是,memberProperties 返回此类及其所有超类中声明的所有非扩展属性。假设我们有一个 Person 类,如下所示:

open class Person{
     val isHuman:Boolean=true
}

此外,我们通过 Person 类扩展我们的 Student 类,然后使用 memberProperties 方法之前相同的代码将产生如下输出:

full_name = Aanand Shekhar Roy
roll_number = 2013001
isHuman = true

因此,如果您只想遍历 Student 类中声明的字段,您将需要使用 declaredMemberProperties 方法。以下是一个使用 declaredMemberProperties 的示例:

for (property in Student::class.declaredMemberProperties) {
    println("${property.name} = ${property.get(student)}")
}

这是输出结果:

full_name = Aanand Shekhar Roy
roll_number = 2013001

前面的例子是关于 Kotlin 的KClass。假设你想遍历Java Class<T>的属性——你可以使用 Kotlin 扩展属性来获取 Kotlin 的KClass<T>,然后你可以继续操作,例如something.javaClass.kotlin.memberProperties

还有更多...

查看 Kotlin 反射库提供的方法列表(kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-class/index.html),借助它你可以在运行时执行许多内省操作。

如何处理内联属性

Kotlin 的一个优点是高阶函数,它允许我们将函数作为其他函数的参数使用。然而,它们是对象,因此它们会带来内存开销(因为每个实例都会在堆中分配空间,我们还需要方法来调用函数)。我们可以通过使用内联函数来改善这种情况。内联注解意味着特定的函数以及函数参数将在调用位置展开;这有助于减少调用开销。

类似地,内联关键字可以与没有后置字段的属性和属性访问器一起使用。让我们看看在这个菜谱中是如何做到这一点的。

准备工作

你需要安装一个首选的开发环境,用于编译和运行 Kotlin。你也可以使用命令行,这需要安装 Kotlin 编译器和 JDK。我正在使用try.kotlinlang.org/在线 IDE 来编译和运行我的 Kotlin 代码,以完成这个菜谱。你也可以使用 IntelliJ IDEA 作为开发环境。

如何操作...

让我们看看如何在这些步骤中处理内联属性:

  1. 让我们尝试一个例子,在 Kotlin 中内联一个属性的访问器:
var x.valueIsMaxedOut: Boolean
inline get() = x.value == CONST_MAX
  1. 在这个例子中,我们只是使用了inline关键字与get访问器。我们也可以通过使整个属性内联来声明getset访问器为内联,如这个代码片段所示:
inline var x.valueIsMaxedOut: Boolean
get() = x.value == CONST_MAX
set(value) {
    // set field here
    println(“Value set!”)
}

在前面的代码片段中,两个访问器都被内联了。

  1. 然而,需要注意的是,如果属性有一个后置字段或访问器不引用后置字段,内联就不会与属性或访问器一起工作。这里的代码是一个我们无法使用inline的场景的例子:
var x.valueIsMaxedOut: Boolean = true
get() = x.value == CONST_MAX
set(value) {
    // set field here
    println(“Value set!”)
}

另一点需要记住的是,尽管内联属性通过仅在调用位置展开来减少调用开销,但它们也会增加整体字节码,因此内联不应与大型函数或访问器一起使用。

它是如何工作的...

因此,基本上,当我们希望减少内存开销时,我们会使用内联。就像内联函数一样,我们也可以将属性声明为内联或属性的访问器作为内联。然而,需要注意的是,内联会增加字节码的量,因此建议不要内联具有大量代码逻辑的函数或访问器。

如何处理嵌套类

在这个菜谱中,我们将展示如何在 Kotlin 中使用嵌套类。嵌套类是其封装类的成员。

准备工作

您需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行,为此您需要安装 Kotlin 编译器和 JDK。我正在使用在线 IDE try.kotlinlang.org/ 来编译和运行我的 Kotlin 代码,以完成这个菜谱。

如何操作...

现在我们将分步骤展示如何使用嵌套类:

  1. 让我们尝试一个 Kotlin 嵌套类的例子:
fun main(args: Array<String>) {
    var a1 = outCl()
    a1.printAB()
    outCl.inCl().printB()
}
class outCl {
var a = 6
    fun printAB () {
    var b_ = inCl().b
    println ("a = $a and b = $b_ from inside outCl")
}

class inCl {
    var b = "9"
        fun printB() {
            println ("b = $b from inside inCl")
        }
    }
}

这是输出:

a = 6 and b = 9 from inside outCl
b = 9 from inside inCl
  1. 现在,让我们尝试一个 inner 类的例子。要将嵌套类声明为 inner,我们使用 inner 关键字。inner 类可以访问外部类的成员,因为它们携带指向外部类的引用:
fun main(args: Array<String>) {
    var a = outCl()
    a.printAB()
    a.inCl().printAB()
}
class outCl {
    var a = 6
    fun printAB () {
        var b_ = inCl().b
        println ("a = $a and b = $b_ from inside outCl")
    }
    inner class inCl {
        var b = "9"
        fun printAB() {
            println ("a = $a and b = $b from inside inCl")
        }
    }
}

上述代码的输出如下:

a = 6 and b = 9 from inside outCl
a = 6 and b = 9 from inside inCl

它是如何工作的...

嵌套类可以通过在另一个类内部声明嵌套类来创建。在这种情况下,要访问嵌套类,您创建一个静态引用,类似于 outerClass.innerClass(),您还可以使用此方法创建内部类的对象。

另一方面,inner 类是通过在嵌套类中添加 inner 关键字创建的。在这种情况下,我们像访问外部类的成员一样访问内部类,即使用外部类的对象,如下所示:

var outerClassObject = outerClass()
outerClassObject.innerClass().memberVar

嵌套类无法访问外部类的成员,因为它没有外部类对象的任何引用。另一方面,内部类可以访问外部类的所有成员,因为它有一个指向外部类对象的引用。

还有更多...

我们也可以在 Kotlin 中使用 object 关键字创建匿名内部类,如下所示:

val customTextTemplateListener = object:ValueEventListener{
    override fun onCancelled(p0: DatabaseError?) {
    }
    override fun onDataChange(dataSnapshot: DataSnapshot?) {
    }
}

在 Kotlin 中获取类

在这个菜谱中,我们将探讨在 Kotlin 中获取类引用的方法。主要,我们将使用反射。反射是一个库,它提供了在运行时而不是编译时检查代码的能力。在 Java 中,我们可以通过 getClass() 获取变量的类,例如 something.getClass()。让我们看看如何在 Kotlin 中解析变量的类。

如何操作...

  1. Java 中解析变量名称的等效方法是使用 .getClass() 方法,例如,something.getClass()。在 Kotlin 中,我们可以通过 something.javaClass 实现相同的功能。

  2. 要获取反射类的引用,我们以前在 Java 中使用 something.class,其 Kotlin 等价物是 something::class。这返回一个 KClass。这个 KClass 的特殊之处在于它提供了与 Java 反射类提供的功能相当的自省能力。

    注意,KClass 与 Java 的 Class 对象不同。如果您想从 Kotlin 的 KClass 获取 Java 的 Class 对象,请使用 .java 扩展属性:

val somethingKClass: KClass<Something> = Something::class
val a: Class<Something> = somethingKClass.java
val b: Class<Something> = Something::class.java
  1. 后者示例将被编译器优化,以避免分配中间的 KClass 实例。

    如果你使用 Kotlin 1.0,你可以通过调用 .kotlin 扩展属性将获得的 Java 类转换为 KClass 实例,例如,something.javaClass.kotlin

还有更多...

正如刚才所描述的,KClass 为你提供了反射能力。以下是一些 KClass 的方法:

  • isAbstract:如果此类是抽象的则为真

  • isCompanion:如果此类是伴随对象则为真

  • isData:如果此类是数据类则为真

  • isFinal:如果此类是最终类则为真

  • isInner:如果此类是内部类则为真

  • isOpen:如果此类是公开的则为真

查阅此链接以获取 KClass 提供的完整函数列表(kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-class/)。

使用代理属性进行操作

Kotlin 1.1 带来了许多更新;其中之一是代理属性。有三种类型的代理属性:

  • lazy:懒加载属性是首先评估的,并在之后返回相同的实例,就像缓存一样

  • observable:每当发生更改时都会通知监听器

  • map:属性存储在映射中,而不是每个字段中

在这个菜谱中,我们将看到如何使用这些代理。那么,让我们开始吧。

准备中

我们将处理 Android 代码,因此我们需要 Android Studio 3。

如何做到这一点...

让我们看看代理属性的一个简单例子:

  1. 首先,我们将处理懒加载代理属性。简单来说,这个代理可以延迟对象的创建,直到我们第一次访问它。当你处理重量级对象时,这非常重要;它们需要很长时间才能创建——例如,创建数据库实例或可能是 dagger 组件。不仅如此,结果会被记住,并且对于此类代理属性的后续 getValue() 调用,将返回相同的值。让我们看一个例子:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
     val button by lazy { findViewById<Button>(R.id.submit_button) }                                              setContentView(R.layout.activity_main)
     button.text="Submit"
}
  1. 上述是活动的标准 onCreate 方法。如果你仔细观察,我们在 setContentView(..) 方法之前设置了 button 变量。当你运行它时,它运行得很好。如果你没有使用懒加载,它将给出一个 NullPointerException,类似于这样:
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.Button.setText(java.lang.CharSequence)' on a null object reference
  1. 按钮变量为空,因为我们是在 setContentView 之前调用它的。然而,这对于懒创建的 button 对象来说并不是问题,因为尽管我们在 setContentView 之前声明了它,但 button 对象是在第一次访问后创建的,即当我们尝试在它上设置属性时。

  2. 因此,使用懒加载构造,你不需要考虑初始化代码放置的位置,对象的初始化将延迟到第一次使用时。

另一个需要注意的关键点是,默认情况下,懒加载属性的评估将是同步的,这意味着值在一个线程中计算,其余的线程将看到相同的值。有三种初始化类型:

  • LazyThreadSafetyMode.SYNCHRONIZED:这是默认模式,确保只有一个线程可以初始化实例。

  • LazyThreadSafetyMode.PUBLICATION:在此模式下,多个线程可以执行初始化。

  • LazyThreadSafetyMode.NONE:当我们确定初始化只会在一个线程上发生时,使用此模式。例如,在 Android 的案例中,我们可以确定视图将由 UI 线程初始化。由于这并不保证线程安全,它具有更少的开销。

另一个有用的委托是可观察的委托。这个委托帮助我们观察属性的任何更改。例如,让我们看看 observable 委托的一个非常基本的实现:

fun main(args: Array<String>) {
    val paris=Travel()
     paris.placeName="Paris"
     paris.placeName="Italy"
}
class Travel {
     var placeName:String by Delegates.observable("<>"){
         property, oldValue, newValue ->
         println("oldValue = $oldValue, newValue = $newValue")
    }
}

这是输出:

oldValue = <>, newValue = Paris
oldValue = Paris, newValue = Italy

如我们所见,observable 委托接受两件事:一个默认值(我们指定为 <>)和一个处理程序,该处理程序在属性修改时被调用。

让我们现在处理 vetoable 委托。它与 observable 委托非常相似,但使用它可以“否决”修改。让我们看看一个例子:

fun main(args: Array<String>) {
    val paris=Travel()
    paris.placeName="Paris"
    paris.placeName="Italy"
    println(paris.placeName)
}
class Travel {
    var placeName:String by Delegates.vetoable("<>"){
        property, oldValue, newValue ->
            if(!newValue.equals("Paris")){
                return@vetoable false
            }
            true
    }
}

这是输出:

Paris

如前例所示,如果 newValue 不等于 "Paris",我们将返回 false,并且修改将被中止。如果您想进行修改,您需要从构造函数中返回 true

有时,您会根据动态值创建一个对象,例如,在解析 JSON 的情况下。对于这些应用程序,我们可以使用 map 实例本身作为委托属性的委托。让我们在这里看看一个例子:

fun main(args: Array<String>) {
    val paris=Travel(mapOf(
        "placeName" to "Paris"
    ))
    println(paris.placeName)
}
class Travel(val map:Map<String,Any?>) {
    val placeName: String by map
}

这是输出:

Paris

要使其适用于 var 属性,您需要使用 MutableMap,因此前面的示例可能看起来像这样:

fun main(args: Array<String>) {
    val paris=Travel(mutableMapOf(
        "placeName" to "Paris"
    ))
    println(paris.placeName)
}
class Travel(val map:MutableMap<String,Any?>) {
    var placeName: String by map
}

当然,输出将是相同的。

更多...

可观察的委托属性在适配器中可以广泛使用。适配器用于在某种列表中填充数据。通常,当数据更新时,我们只需更新适配器中的成员变量列表,然后调用 notifyDatasetChanged()。借助可观察的和 DiffUtils,我们只需更新实际更改的内容,而不是更改一切。这导致性能更加高效。

与枚举一起工作

枚举用于变量只能取一小组可能值的情况。一个例子是类型常量(方向:"North"、"South"、"East" 和 "West")。借助枚举,您可以避免传递无效常量的错误,并且还可以记录哪些值是可接受的。

在这个菜谱中,我们将了解如何在 Kotlin 中使用枚举。

准备工作

我们将使用 IntelliJ IDEA 来编写和运行代码。首先,我们将创建一个简单的类型安全的枚举 Direction,其成员包括 NORTH、SOUTH、EAST 和 WEST(代表四个方向)。

如何做到这一点...

让我们看看 enum 类的一个例子:

  1. 在这个例子中,我们将创建一个方向枚举。我们将假设只有四个方向:
enum class Direction {
    NORTH,SOUTH,EAST,WEST
}
fun main(args: Array<String>) {
    var north_direction=Direction.NORTH
    if(north_direction==Direction.NORTH){
        println("Going North")
    }else{
        println("No idea where you're going!")
    }
}
  1. 正如你所见,变量(north_direction)只需在enum类中预定义的常量中取值。

  2. 我们还可以使用默认值来初始化枚举:

enum class Direction(var value:Int) {
    NORTH(1),SOUTH(2),EAST(3),WEST(4)
}
fun main(args: Array<String>) {
    var north_direction=1
    if(north_direction==Direction.NORTH.value){
        println("Going North")
    }else{
        println("No idea where you're going!")
    }
}

//Output: Going North

还有更多...

强烈建议你不在你的 Android 项目中使用枚举。根据谷歌工程师的说法,添加一个枚举将使最终 DEX 文件的大小增加大约 13 倍。它还会产生运行时开销的问题,你的应用将需要更多的空间。

Android 文档中是这样说的:

"枚举通常需要的内存是静态常量的两倍以上。你应该严格避免在 Android 中使用枚举。"

然而,如果你想享受枚举的便利性,你可以使用 Android 的注解库,它有TypeDef注解——但是遗憾的是,在本书编写时,Kotlin 不支持这一点,所以我们希望它能在 Kotlin 的未来版本中得到添加。

第四章:函数

本章将涵盖以下内容:

  • 在函数中指定默认值

  • 在函数中使用命名参数

  • 在 Kotlin 中创建 RecyclerView 适配器

  • 在 Kotlin 中创建 getter setters

  • 将可变参数传递给函数

  • 将函数作为参数传递给另一个函数

  • 声明一个 static 函数

  • 在 Kotlin 中使用 use 关键字

  • 在 Kotlin 中使用闭包

  • 带接收者的函数字面量

  • 使用匿名函数

简介

函数是任何代码的构建块。它们帮助我们使程序更加模块化、安全且易于理解。函数在面向对象编程中至关重要,因为它们在抽象和封装(两个非常重要的设计原则)中扮演着重要角色。Kotlin 通过链式和 lambda 表达式的方式,为我们的函数使用方式带来了许多更新。它使函数式编程变得更加容易。在本章中,我们将学习一些有助于我们处理函数的技巧。那么,让我们开始吧!

在函数中指定默认值

如果你来自 Java 世界,你可能记得我们无法为方法指定默认值。这意味着我们无法在 Java 中这样做:

public void foo(int a, int b=10){
}

我们需要为它编写两个方法,这被称为 方法过载

public void foo(int a){
}

public void foo(int a, int b){
}

此外,假设你有一个具有三种不同类型参数的函数,例如这些:

public void foo (int a,double b, String c){
}

然后,你将拥有七个方法过载的实例:

public void foo (int a,double b, String c),
public void foo (int a,double b) ,
public void foo (double b, String c),
public void foo (int a, String c),
public void foo (int a),
public void foo (double b),
public void foo (String c)

Kotlin 通过提供默认值的方法为你提供,这样你可以防止方法过载的数量变得疯狂。有些人可能会说:“嘿,我们为什么不使用构建器模式而不是方法过载呢?”这些人是对的,但 Kotlin 的方法比这更容易。让我们看看吧!

准备工作

我们将使用 IntelliJ IDEA 来编写和执行我们的代码。你可以使用你感到舒适的任何开发环境。

如何做到这一点...

在 Kotlin 中,函数的参数可以有默认值,并且当省略相应的参数时,它们会被使用。这反过来又减少了过载的数量。前面的例子中,具有三种不同类型的参数可以在 Kotlin 中轻松解决,代码量也少得多:

  1. 让我们在编辑器中添加提到的代码,运行它,并检查输出:
fun main(args: Array<String>) {
    foo()    
    foo(1)
    foo(1,0.1)
    foo(1,0.1,"custom string")
}
fun foo(a:Int=0, b: Double =0.0, c:String="some default value"){
    println("a=$a , b=$b ,c = $c")
}

如果你运行前面的代码,你会看到以下输出:

Output:
a=0 , b=0.0 ,c = some default value
a=1 , b=0.0 ,c = some default value
a=1 , b=0.1 ,c = some default value
a=1 , b=0.1 ,c = custom string
  1. 如你所见,我们不必实现四个不同的方法,我们可以映射参数。当不通过提供显式参数来调用方法时,会使用默认参数,所以当你不传递任何参数时,它就使用所有默认参数。借助命名参数,我们可以进一步减少方法的数量,但我们将在这下一个技巧中介绍。

  2. 需要注意的一件事是,默认参数也会与构造函数一起工作。所以你可以有一个如下所示的类声明:

data class Event(var eventName: String? = "", var eventSchedule: Date? = Date(), var isPrivate: Boolean = false)

想要了解更多关于数据类的信息,请前往第十一章如何创建数据类食谱

  1. 然后,我们可以声明对象,如下所示:
Event("Celebration")
Event("Ceberation",Date())
Event("Ceberation",Date(),true)

如你所见,借助构造函数中的默认值,我们避免了实现多个构造函数的需要,这是我们以前在 Java 中经常做的。

记住,这里有一个陷阱。如果你在 Java 中创建对象,我们将无法这样做。这意味着以下代码所示的操作将不会被 Java 接受。现在我知道你会想“Java 的 100%互操作性去哪了?!”:

new Event("Celebration")
new Event("Celebration",Date())
new Event("Celebration",Date(),true)
  1. 如果我们想要向 Java 调用者公开多个重载,我们只需要进行一个小修改,即——即在具有默认值的构造函数和函数上添加@JvmOverloads,这样前面的类声明就变成了这样:
data class Event @JvmOverloads constructor (var eventName: String? = "", var date: Date? = Date(), var isPrivate: Boolean = false)
  1. 此外,我们的方法也变成了这样:
@JvmOverloads fun foo(a:Int=0, b: Double =0.0, c:String="some default value"){
 println("a=$a , b=$b ,c = $c")
 }

这是一点小小的代价,但@JvmOverloads注解帮助我们的构造函数和函数在 Java 世界中也能有默认值。

还有更多...

如果我们想让我们的代码只在 Kotlin 世界中工作,那么我们不需要@JvmOverloads注解,因为 Kotlin 有自己的规则,可以通过这些规则在构造函数和函数中使用默认值。添加@JvmOverloads注解会创建所有必要的重载。所以如果你反编译 Kotlin 的字节码,你会看到构造函数和函数的所有重载版本。

在函数中使用命名参数

这个食谱可以被视为对之前食谱的扩展,在函数中指定默认值。函数中的默认参数和命名参数一起可以大幅减少方法重载的数量。我们已经看到了如何在函数中使用默认参数;现在,让我们看看如何使用命名参数。

准备工作

我们将使用 IntelliJ IDEA 来编写和执行我们的代码。你可以使用你感到舒适的任何开发环境。

如何做到这一点...

为了减少重载数量并提高代码的可读性,我们还可以使用命名参数。让我们看看以下代码:

  1. foo函数的相同示例,以下是我们可以如何使用命名参数:
fun main(args: Array<String>) {
     foo(b=0.9)
     foo(a=1,c="Custom string")
}
 fun foo(a:Int=0, b: Double =0.0, c:String="some default value"){
     println("a=$a , b=$b ,c = $c")
}
  1. 运行上述代码将得到以下输出:
Output:
a=0 , b=0.9 ,c = some default value
a=1 , b=0.0 ,c = Custom string
  1. 命名参数防止我们出现重载,并且使我们的代码更加易于阅读。此外,我们不需要输入所有参数。我的意思是,如果你只有两个参数——ac——那么你可能需要这样做:
foo(1, 0.0, "Custom string")
  1. 你必须添加一个默认值来填充ac之间的空间。然而,使用命名参数,你能够使用foo(a=1,c="Custom string")而无需在中间添加默认参数。

  2. 一个需要注意的关键点是,当我们调用一个同时带有位置参数和命名参数的函数时,我们需要将位置参数放在第一个命名参数之前。例如,foo(1,b = 0.1) 调用是允许的,但 foo(a = 1, 0.1) 是不允许的。

默认值和命名参数可以将所需的函数重载数量降到最低,使代码量小,并提高代码的可读性。

在 Kotlin 中创建 RecyclerView 适配器

RecyclerView 是 Android 开发中最广泛使用的元素之一。它本质上用于通过适配器显示列表中的数据。在这个菜谱中,我们将学习如何利用 Kotlin 的强大功能使 RecyclerView 更加高效。我们还将使用 DiffUtils。它从 24.02 开始可用。根据文档:

DiffUtil 是一个实用类,可以计算两个列表之间的差异,并输出一个更新操作列表,将第一个列表转换为第二个列表。

定义是自我解释的。notifyDatasetChanged 是适配器的一个非常昂贵的操作。DiffUtils 只更新已更改的部分,而 notifyDatasetChanged 则更新整个列表。

准备工作

在 Android Studio 中创建一个新的 Android 项目。您也可以克隆 gitlab.com/aanandshekharroy/kotlin-cookbook 仓库并检出 1-recycler-view-in-kotlin 分支。

在这个应用中,我们将创建一个简单的列表,列出 Google 发布的不同 Android 风味,类似于这里所看到的:

如您所见,有一个浮动操作按钮;点击它将更新列表的顺序。我们将更新列表(RecyclerView),但我们将使用 DiffUtils 而不是 notifyDatasetChanged 方法来更新它。

如何做到这一点...

因此,现在让我们按照以下步骤创建我们刚才讨论的应用:

  1. 首先,我们需要创建一个 Android 风味的列表。因此,我们将首先创建一个数据类,它包含图像和风味的名称:
data class AndroidFlavours (var name:String, val image:Int)

我们将图像类型定义为 Int,因为我们将会使用可绘制项的 ID。在 drawable 文件夹中,我们将保存所有必需的图像。

  1. 接下来,我们将创建一个 Android 风味的列表:
val flavorList= listOf<AndroidFlavours>(
        AndroidFlavours("Cupcake",R.drawable.cupcake),
        AndroidFlavours("Donut",R.drawable.donut),
        AndroidFlavours("Eclair",R.drawable.eclair),
        AndroidFlavours("Froyo",R.drawable.froyo),
        AndroidFlavours("Gingerbread",R.drawable.gingerbread),
        AndroidFlavours("HoneyComb",R.drawable.honeycomb),
        AndroidFlavours("Icecream Sandwich",R.drawable.icecream),
        AndroidFlavours("Jellybean",R.drawable.jellybean),
        AndroidFlavours("KitKat",R.drawable.kitkat),
        AndroidFlavours("Lollipop",R.drawable.lollipop))
  1. 现在,我们将创建一个适配器。我们将命名为 AndroidFlavourAdapter
class AndroidFlavourAdapter:RecyclerView.Adapter<AndroidFlavourAdapter.FlavourViewHolder>() {
    var flavourItems:List<AndroidFlavours> by Delegates.observable(emptyList()){
        property, oldValue, newValue ->
        notifyChanges(oldValue,newValue)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FlavourViewHolder {
        return FlavourViewHolder(parent.inflate(R.layout.flavour_item))
    }

    override fun getItemCount(): Int =flavourItems.size

    override fun onBindViewHolder(holder: FlavourViewHolder, position: Int) {
        holder.name.text=flavourItems.get(holder.adapterPosition).name
        holder.image.loadImage(flavourItems.get(holder.adapterPosition).image)
    }

    inner class FlavourViewHolder(var view: View):RecyclerView.ViewHolder(view){
        var name:TextView = view.findViewById(R.id.textView)
        var image:ImageView = view.findViewById(R.id.imageView)
    }
}

上述代码对于 RecyclerView 的一般实现来说相当标准,除了两点。

其中之一是 loadImage 函数,它不是一个原生函数,而是一个扩展函数,其实现如下:

fun ImageView.loadImage(image: Int) {
    Glide.with(context).load(image).into(this)
}
  1. 另一件事是我们已经在适配器中定义了 AndroidFlavours 的列表。适配器中的 flavoursList 是一个 observable 属性。这意味着监听器会通知此属性的变化。因此,我们得到以下结构:
var flavourItems:List<AndroidFlavours> by Delegates.observable(emptyList()){
    property, oldValue, newValue ->
    notifyChanges(oldValue,newValue)
}

  1. 现在,每次我们尝试为 flavourItems 变量赋值时,{ .. } 块下的构造就会运行,如果我们想进行操作,我们将有旧值和新值。在这种情况下,我们将使用 notifyChanges 方法。让我们看看 notifyChanges 方法:
private fun notifyChanges(oldValue: List<AndroidFlavours>, newValue: List<AndroidFlavours>) {
    val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
        override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
            val oldFlavor=oldValue.get(oldItemPosition)
            val newFlavor=newValue.get(newItemPosition)
            val bundle=Bundle()
            if(!oldFlavor.name.equals(newFlavor.name)){
                bundle.putString("name",newFlavor.name)
            }
            if(!oldFlavor.image.equals(newFlavor.image)){
                bundle.putInt("image",newFlavor.image)
            }
            if(bundle.size()==0) return null
            return bundle
        }

        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            return oldValue.get(oldItemPosition)==newValue.get(newItemPosition)
        }

        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            return oldValue.get(oldItemPosition).name.equals(newValue.get(newItemPosition).name)&&oldValue.get(oldItemPosition).image.equals(newValue.get(newItemPosition).image)
        }

        override fun getOldListSize() = oldValue.size

        override fun getNewListSize() = newValue.size

    })

    diff.dispatchUpdatesTo(this)
}

我将在下一节解释前面的代码。

  1. 现在,让我们设置适配器:
mAdapter= AndroidFlavourAdapter()
flavour_list.layoutManager=LinearLayoutManager(this)
flavour_list.adapter=mAdapter
mAdapter.flavourItems=flavorList
shuffle.setOnClickListener {
    mAdapter.flavourItems=flavorList.shuffle()
}
  1. shuffle 函数将随机化 AndroidFlavours 列表的顺序。.shuffle() 函数不是 Kotlin 或 Java 提供的本地函数,而是一个扩展函数:
fun <E> List<E>.shuffle(): MutableList<E> {
    val list = this.toMutableList()
    Collections.shuffle(list)
    return list
}

它是如何工作的...

让我们深入了解 DiffUtilsDiffUtils 需要两个数组/列表,其中一个应该是旧列表,另一个应该是新列表。

有五个主要函数:

  • getNewListSize(): 这个方法返回新列表的大小。

  • getOldListSize(): 这个方法返回旧列表的大小。

  • areItemsTheSame(): 这个方法用于确定两个对象是否表示相同的项。

  • areContentsTheSame(): 这个方法用于确定两个对象是否包含相同的数据。在我们的实现中,如果两个对象都有相同的名称和图像,我们返回 true。

  • getChangePayload(): 当 areItemsTheSame() 返回 true 且 areContentsTheSame() 返回 false 时,DiffUtils 调用此方法以获取更改的有效载荷。

在我们实现前面的方法中,我们在有效载荷中添加了名称和图像的更改:

override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
    val oldFlavor=oldValue.get(oldItemPosition)
    val newFlavor=newValue.get(newItemPosition)
    val bundle=Bundle()
    if(!oldFlavor.name.equals(newFlavor.name)){
        bundle.putString("name",newFlavor.name)
    }
    if(!oldFlavor.image.equals(newFlavor.image)){
        bundle.putInt("image",newFlavor.image)
    }
    if(bundle.size()==0) return null
    return bundle
}

最后,在差异计算之后,DiffUtils 对象将更改派发到适配器。为此,我们调用 dispatchUpdatesTo 方法:

diff.dispatchUpdatesTo(this)

要更新来自有效载荷中的数据的更改,你需要重写 onBindViewHolder (holder: FlavourViewHolder, position: Int*, payloads: MutableList<Any>?):

override fun onBindViewHolder(holder: FlavourViewHolder, position: Int, payloads: MutableList<Any>?) {
    if (payloads != null) {
        if (payloads.isEmpty())
            return onBindViewHolder(holder,position)
        else {
            val o = payloads.get(0) as Bundle
            for (key in o.keySet()) {
                if (key == "name") {
                    holder.name.text=o.getString("name")
                } else if (key == "image") {
                    holder.image.loadImage(o.getInt("image"))
                }
            }
        }
    }
}

使用适配器的 notifyItemRangeChanged 方法将有效载荷中的更改派发。

还有更多...

文档指出,如果列表太大,DiffUtils 可能需要一些时间来处理两个列表之间的差异,因此这必须在后台线程上计算,例如,使用 RxJava

在 Kotlin 中创建 getter-setters

如果你曾经使用过 Java,你可能知道什么是 getter-setter。Java 有字段,getter-setters 是用于 访问(getter)和 修改(setter)成员变量的方法。它们是封装(设计原则之一)的必要部分。

然而,在 Kotlin 中,我们没有字段,而是有 属性。属性可以有自定义的访问器和修改器实现。在这个菜谱中,我们将看到我们如何实现自定义访问器和修改器。

准备工作

我们将使用 IntelliJ IDEA 来编写和执行我们的代码。你可以使用你感到舒适的任何开发环境。我们将使用示例来理解 Kotlin 的自定义 getter-setters。

如何做...

让我们按照以下步骤来了解 Kotlin 中自定义 getter-setters 的工作原理:

  1. Kotlin property的语法如下:
var <propertyName>[: <PropertyType>] [= <property_initializer>]  [<getter>]  [<setter>]

所以如果您使用val a =1,您将获得默认的gettersetter

  1. 现在,让我们看看如何创建一个自定义的getter。假设我们有一个属性,其值依赖于另一个属性:
fun main(args: Array<String>) {
    val sample=Sample()
    println(sample.isListBig)
}
class Sample{
    val array= mutableListOf<Int>(1,2,3)
    val isListBig:Boolean
        get()=array.size>2
}

如果您运行前面的代码,您将在控制台看到以下输出:

图片

  1. 如您所见,我们可以修改属性的get方法中的 getter。如果属性类型是从 getter 推断出来的,我们也可以这样做:
val isListBig get()=array.size>2

结果当然是一样的。

现在,让我们看看访问器:

  1. 在 Java 中,我们通常做如下操作:
public setIsListBig(boolean isListBig){
    this.isListBig=isListBig
}
  1. 如果我们尝试在 Kotlin 中实现这一点,它将类似于以下这样:

图片

  1. 如您所见,IDE 会提示我们这是一个递归调用。为什么?因为当您尝试使用.isListBig设置值时,您已经在设置器内部使用了设置器,因此形成了递归循环

  2. 为了避免递归调用并仍然实现 setter,您需要使用field关键字。因此,前面的实现将类似于以下这样:

var isListBig :Boolean = false
    set(value) {
        field= array.size>2
    }
  1. 当您在声明属性时初始化isListBig,值将分配给后端字段,而不调用 setter。field关键字用于访问后端字段,如果属性至少使用了一个访问器的默认实现,或者如果自定义访问器通过field标识符引用它,则会生成该字段。

  2. 如果您想限制 setter 的访问权限,您可以使用以下方式:

var isListBig :Boolean = false
    private set(value) {
        field= array.size>2
    }
  1. 此外,假设您正在使用某种形式的依赖注入。您可以使用以下方式实现:
var mPresenter:MainActivityMvpPresenter?=null
    @Inject set
  1. set类似,您也可以为get实现自定义实现。让我们看一个例子:
class SameClass {
    var name="aanand"
    get() = field.toUpperCase()
}
  1. 现在,让我们假设我们正在尝试访问name属性:
fun main(args: Array<String>) {
    var s=SameClass()
    println(s.name)
}

如果您运行前面的代码,您将看到以下输出:

图片

注意,我们在get()方法中也使用了field。它是我们之前解释过的同一个后端字段。

还有更多...

这里需要注意的是,您不能在构造函数中为您的属性实现自定义的 getter 或 setter。您需要在类的主体中声明属性:

class Student(val name: String, age: Int) {
  var age: Int = age
      set(value) {
        println("Setting age to $value")
        field = value
  }
}

这里需要注意的一个关键点是,您需要保持 getter 的可见性与属性的可见性完全相同:

protected var name="aanand"
protected get() = field.toUpperCase()

前面的代码是完全有效的,尽管再次放置相同的访问修饰符是多余的,因此最好省略它。

另一方面,setter 可以有一个比属性权限低的访问修饰符。考虑以下示例:

protected var name="aanand"
    private set

前面的代码是有效的,因为 setter 的访问修饰符private比属性的访问修饰符权限低:

protected var name="aanand"
    public set

然而,前面的代码是无效的,因为protected的权限比public低。

将可变参数传递给函数

在许多场景中,我们需要将可变参数传递给函数。在 Kotlin 中,我们可以使用 vararg 修饰符来实现这一点。在本菜谱中,我们将介绍所有实现方法。我们将通过一些示例来展示如何使用 Kotlin 的这一特性。

准备工作

您需要安装首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行来完成此任务,为此您需要安装 Kotlin 编译器和 JDK。我正在使用在线 IDE try.kotlinlang.org/ 来编译和运行我的 Kotlin 代码,以完成本菜谱。您也可以使用 IntelliJ IDEA 作为您的开发环境。

如何实现...

让我们通过以下步骤来了解,我们将演示如何将可变数量的参数传递给函数:

  1. 使用 vararg,我们可以将逗号分隔的参数传递给一个函数,其中我们已将方法中的单个参数定义为 vararg,如下例所示:
fun main(args: Array<String>) {
    someMethod("as","you","know","this","works")
}
fun someMethod(vararg a: String) {
    for (a_ in a) {
        println(a_)
    }
}
  1. 此外,如果您已经有一个值数组,您可以直接使用 * 展开操作符传递它:
fun main(args: Array<String>) {
    val list = arrayOf("as","you","know","this","works")
    someMethod(*list)
}
fun someMethod(vararg a: String) {
    for (a_ in a) {
        println(a_)
    }
}

因此,基本上,vararg 告诉编译器将传递的参数包装成一个数组。

  1. 另一方面,展开操作符简单地告诉编译器展开数组成员并将它们作为单独的参数传递。展开操作符——即 *——被放置在传递的数组名称之前。

  2. 然而,显然有时可能需要传递其他参数,如命名参数等。

    在下面的示例代码中,我们尝试传递除了 vararg 之外的另一个参数:

fun main(args: Array<String>) {
    val list = arrayOf("as","you","know","this","works")
    someMethod(3, *list)
}
fun someMethod(b: Int, vararg a: String) {
    for (a_ in a) {
        println(a_)
    }
}
  1. 在下一个示例中,第一个参数类似于 vararg 类型,但它可以工作:
fun main(args: Array<String>) {
    someMethod("3", "as","you","know","this","works")
}
fun someMethod(b: String, vararg a: String) {
    println("b: " + b)
    for (a_ in a) {
        println(a_)
    }
}

输出如下:

b: 3
as
you
know
this
works
  1. 因此,通常 vararg 是最后一个传递的参数,但如果我们想在 vararg 之后传递其他参数怎么办?我们可以这样做,但它们必须被命名。这就是为什么以下代码无法编译的原因:
// does not compile
fun main(args: Array<String>) {
    someMethod("3", "as","you","know","this","works", "what")
}
fun someMethod(b: String, vararg a: String, c: String) {
    println("b: " + b)
    for (a_ in a) {
        println(a_)
    }
    println("c: " + c)
}
  1. 这段代码无法编译,因为最后传入的字符串被认为是 vararg 的一部分,并且编译器抛出错误,因为我们没有传递 c 的值。

    要正确实现,我们需要将 c 作为命名参数传递,就像这里所示:

fun main(args: Array<String>) {
    someMethod("3", "as","you","know","this","works", c = "what")
}
fun someMethod(b: String, vararg a: String, c: String) {
    println("b: " + b)
    for (a_ in a) {
        println(a_)
    }
    println("c: " + c)
}

输出如下:

b: 3
as
you
know
this
works
c: what

它是如何工作的...

vararg 修饰符告诉编译器将所有以逗号分隔的参数包装成一个数组,而 *(即展开操作符)则展开数组元素并将它们作为参数传递。

还有更多...

如果我们希望第一个参数有一个默认值,就像这个例子一样:

fun main(args: Array<String>) {
    someMethod("3", "as","you","know","this","works")
}
fun someMethod(b: String = "x", vararg a: String) {
    println("b: " + b)
    for (a_ in a) {
        println(a_)
    }
}

我们希望所有参数都被视为 vararg 的一部分,但编译器将第一个参数读取为 b。在这种情况下,命名传递的参数可以解决这个问题:

fun main(args: Array<String>) {
    someMethod(a = *arrayOf("3", "as","you","know","this","works"))
}
fun someMethod(b: String = "x", vararg a: String) {
    println("b: " + b)
    for (a_ in a) {
        println(a_)
    }
}

在前面的代码中,编译器理解到 b 的值没有被传递,并采用了默认值。同样,如果您想在函数中拥有两个 vararg,您将需要传递命名参数。

将函数作为参数传递给另一个

Kotlin 赋予我们声明高阶函数的能力。在高阶函数中,我们可以将函数作为参数传递和返回。这是一个极其有用的特性,使得我们的代码更容易处理。实际上,许多 Kotlin 库的函数都是高阶的,例如map。在 Kotlin 中,我们可以声明函数和函数引用作为值,然后将其传递给函数。在本节中,我们将首先了解如何声明 lambda,然后了解如何将它们传递给函数。

准备工作

您需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行来完成此目的,为此您需要安装 Kotlin 编译器和 JDK。我正在使用try.kotlinlang.org/上的在线 IDE 来编译和运行我的 Kotlin 代码,以完成这个菜谱。您也可以使用 IntelliJ IDEA 作为开发环境。

如何实现...

让我们按照以下步骤来了解高阶函数的工作原理:

  1. 让我们先了解如何将函数声明为 lambda:
fun main(args: Array<String>) {
    val funcMultiply = {a:Int, b:Int -> a*b}
    println(funcMultiply(4,3))
    val funcSayHi = {name: String -> println("Hi $name")} 
    funcSayHi("John")
}
  1. 在前面的代码块中,我们声明了两个 lambda:一个(funcMultiply)接受两个整数并返回一个整数,另一个(funcSayHi)lambda 接受一个字符串并返回一个单元——也就是说,它不返回任何内容。

  2. 虽然在前面的例子中我们不需要声明参数类型和返回类型,但在某些情况下,我们需要显式地声明参数类型和返回类型。我们按照以下方式来做:

fun main(args: Array<String>) {
    val funcMultiply : (Int, Int)->Int = {a:Int, b:Int -> a*b}
    println(funcMultiply(4,3))
    val funcSayHi : (String)->Unit = {name: String -> println("Hi $name")} 
    funcSayHi("John")
}
  1. 现在我们已经对 lambda 的工作原理有了大致的了解,让我们尝试将一个 lambda 传递给另一个函数——也就是说,我们将尝试一个高阶函数。看看这个代码片段:
fun main(args: Array<String>) {
    val funcMultiply : (Int, Int)->Int = {a:Int, b:Int -> a*b}
    val funcSum : (Int, Int)->Int = {a:Int, b:Int -> a+b}
    performMath(3,4,funcMultiply)
    performMath(3,4,funcSum)
}
fun performMath(a:Int, b:Int, mathFunc : (Int, Int) -> Int) : Unit {
    println("Value of calculation: ${mathFunc(a,b)}")
}
  1. 是的,就这么简单——创建一个函数 lambda 并将其传递给函数。所以这只是一个高阶函数的一个方面——也就是说,我们可以将一个函数作为参数传递给另一个函数。

  2. 高阶函数的另一个用途是返回一个函数。考虑以下示例,我们需要一个根据某些条件转换订单总价的函数。有点像电子商务网站,但简单得多:

fun main(args: Array<String>) {
    val productPrice1 = 600; // free delivery of order above 499
    val productPrice2 = 300; // not eligible for free deliver
    val totalCost1 = totalCost(productPrice1)
    val totalCost2 = totalCost(productPrice2)

    println("Total cost for item 1 is ${totalCost1(productPrice1)}")
    println("Total cost for item 2 is ${totalCost2(productPrice2)}")
}
fun totalCost(productCost:Int) : (Int) -> Int{
    if(productCost > 499){
        return { x -> x }
    }
    else {
        return { x -> x + 50 }
    }
}
  1. 注意我们如何需要根据某些条件更改我们应用的函数,以便返回一个符合条件的函数。我们将返回的函数赋值给一个变量,然后我们只需在变量前加上append ()就可以将其用作函数,就像我们处理 lambda 一样。这是因为高阶函数本质上返回一个 lambda。

它是如何工作的...

在 Kotlin 中,我们可以将一个函数赋值给一个变量,然后我们可以将这个函数传递给另一个函数或从函数中返回它。这是因为它本质上就像一个变量一样声明。这是通过函数的 lambda 声明来实现的。

声明一个静态函数

静态函数非常有用,因为它们帮助我们避免在多个对象中复制相同的方法,这样我们就可以遵循不要重复自己DRY)的原则。当不需要创建对象实例时,它们也非常有用。在 Kotlin 中,我们没有像 Java 那样静态方法/函数和变量,但我们仍然可以实现相同的结果。让我们看看如何实现吧!

准备工作

我们将使用 IntelliJ IDEA 来编写和执行我们的代码。你可以使用你感到舒适的任何开发环境。我们将通过示例及其工作原理来学习静态函数。

如何实现...

静态方法的一个用例是,我们可以防止在不同类中多次复制相同的方法,并且我们不需要创建封装类的对象。

Kotlin 建议创建包级别的函数。如果你来自 Java 世界,这可能对你来说没有什么意义,因为在 Java 中不支持这一点。让我们看看 Kotlin 中是如何实现的:

  1. 你需要创建一个以.kt扩展名的 Kotlin 文件,并仅声明你将在许多地方使用的方法。我已经创建了一个SampleClass.kt文件,并添加了一个我们将从其他类中调用的方法:
package packageA
fun foo(){
    println("calling from boo method")
}
  1. 现在,我将从HelloWorld.kt中调用这个方法:
import packageA.*
fun main(args: Array<String>) {
    foo()
}
  1. 由于函数存在于packageA中,我们使用了import语句。这样,我们遵循了 DRY 原则,并且不需要创建任何类的实例。

  2. 另一种方法是,通过在对象声明中放置方法或变量来实现。因此,我们可以将SameClass.kt修改如下:

package packageA
object Foo{
    fun callFoo() = println("Foo")
    var foo="foo"
}
  1. 在对象声明下定义的任何方法或变量都将作为static方法或变量工作。为了访问它,我们可以这样做:
Foo.callFoo()

这与调用静态方法非常相似。

  1. 然而,假设你想要使用类名作为限定符并访问类的元素。你仍然可以使用companion关键字来实现。以下是它的样子:
fun main(args: Array<String>) {
    SampleClass.foo()
}
class SampleClass{
    companion object {
        fun foo()= print("In foo method")
    }
}
  1. 如果你想要调用companion对象下的方法,你需要像这样访问它:
SampleClass.Companion.foo();
  1. 如果Companion看起来让你感到不舒服,你可以使用@JvmStatic注解:
companion object {
    @JvmStatic
    fun foo()= print("In foo method")
}
  1. 然后,你可以像在 Kotlin 类中一样使用SampleClass.foo()来访问它。

在 Kotlin 中使用 use 关键字

有一些情况下,如果你使用资源(例如,文件),你必须注意其生命周期,以确保不会泄露资源。例如,如果你从文件中读取,使用后需要关闭它,否则你会将其留在不稳定的状态。Java 7 带来了一项更新,可以处理这种情况,而无需显式处理。Kotlin 也提供了这个功能,但方式更加简单。它是通过使用use方法来实现的。我们将在接下来的菜谱中了解这一点。那么,让我们开始吧!

准备工作

我们将使用 IntelliJ IDEA 来编写和执行我们的代码。你可以使用你感到舒适的任何开发环境。

如何实现...

让我们采取以下步骤来理解 Kotlin 的 use 函数:

  1. 要理解 use 关键字,我们需要回到 Java。在 Java 7 之前,管理需要关闭的资源有点繁琐。例如,看看以下代码:
private static void printFile() throws IOException {
    InputStream input = null;

    try {
        input = new FileInputStream("sampleFile.txt");
        // Some operation using input object
    } finally {
        if(input != null){
            input.close();
        // closing the resource
        }
    }
}
  1. 让我们检查前面的代码。我们知道在使用 input 对象时,try 块内部可能会抛出异常。然而,它也可能在 finally 块中抛出,因为我们正在尝试关闭 input 对象。现在,无论 try 块是否抛出异常,finally 块都会被调用。假设 tryfinally 块都抛出了异常——哪一个将会传播?答案是,异常将在 finally 块中抛出,即使 try 的异常在这里可能更有意义。

  2. Java 7 通过引入 try-with-resource 构造来更新了这个问题,其外观如下:

try(FileInputStream input = new FileInputStream("file.txt")) {
        int data = input.read();
        // operations on input object
    }
  1. try 块执行完毕后,FileInputStream 对象会自动关闭。此外,如果 input.read() 操作和关闭输入对象的操作都抛出异常,input.read() 抛出的异常将会传播。Kotlin 的 use 关键字做的是完全相同的工作。在本节中,我们将看到它是如何实现的。

  2. 在前面的例子中,如果我们实现 use 关键字,Java 中的代码将类似于以下内容:

FileInputStream("file.txt").use {
    input ->
    var data = input.read()
}

它是如何工作的...

use 接受一个函数字面量,并在可关闭实例上定义为一个扩展。它将在函数完成后关闭资源,就像 try-with-resources 构造一样。

已完成,无论是否抛出了异常。

处理闭包

MDN (developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) 说:

“闭包是一种特殊的对象,它结合了两样东西:一个函数,以及该函数被创建时的环境。环境包括在闭包创建时处于作用域内的任何局部变量。”

函数式编程中的 闭包 是那些 了解 自己周围环境的函数。通过这种方式,我的意思是闭包函数可以访问在外部作用域中定义的变量和参数。记住,在 Java 和传统的过程式编程中,变量是绑定到作用域的,一旦块执行完毕,局部属性就会被从内存中清除。Java 8 的 lambda 可以访问外部变量,但不能修改它们,这限制了你在 Java 8 中尝试进行函数式编程的能力。让我们看看一个在 Kotlin 中处理闭包的例子。

准备工作

我们将使用 IntelliJ IDEA 来编写和执行我们的代码。你可以使用你熟悉的任何开发环境。

如何做到这一点...

在这个例子中,我们将简单地创建一个整数数组并计算其总和:

fun main(args: Array<String>) {
    var sum=0
    var listOfInteger= arrayOf(0,1,2,3,4,5,6,7)
    listOfInteger.forEach {
        sum+=it
    }
    println(sum)
}

在前面的示例中,sum 变量是在外部作用域中定义的;尽管如此,我们仍然能够访问和修改它。

更多内容...

如果你想了解更多关于高阶函数或闭包的内容,请参阅本章的 将函数作为参数传递给另一个函数 食谱。

带接收者的函数字面量

函数字面量 是一种未声明但作为表达式传递的函数。Lambda 表达式和匿名函数都是函数字面量。在 Kotlin 中,我们可以使用接收者对象调用函数字面量,并且可以在函数字面量的主体内部调用接收者对象的方法,这与 Kotlin 中的扩展函数非常相似。在本食谱中,我们将学习如何使用带接收者的函数字面量。

准备工作

您需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行来完成此任务,这需要安装 Kotlin 编译器和 JDK。我在 try.kotlinlang.org/ 上使用在线 IDE 来编译和运行本食谱的 Kotlin 代码。您也可以使用 IntelliJ IDEA 作为开发环境。

如何操作...

按以下步骤理解函数字面量:

  1. 让我们从在 String 上的一个简单函数字面量开始,它返回一个添加到接收者字符串上的字符串:
fun main(args: Array<String>) {
    var str1 = "The start of a "
    val addStr = fun String.(successor: String): String {
        return this + successor 
    }
    str1 = str1.addStr("beautiful day.")
    println(str1)
}

函数字面量可以访问它被调用的接收者,并且可以访问与该接收者相关联的方法。

  1. 我们也可以在普通函数中将接收者作为参数传递,其中第一个参数用于接收者。这在需要使用普通函数的场景中可能很有用。

    因此 String.(String) -> Int(String, String) -> Int 是兼容的。查看以下示例:

fun main(args: Array<String>) {
    var str1 = "The start of a "
    val addStr = fun String.(successor: String): Int {
        return this.length + successor.length
    }
    var x = str1.addStr("beautiful day.")
    println(x)
    fun testIfEqual(op: (String, String) -> Int, a: String, b: String, c: Int) =
    assert(op(a, b) == c)

    testIfEqual(addStr, "The start of a ", "beautiful day.", str1.length + "beautiful    day.".length) // OK
}

如果可以推断接收者类型,则 lambda 可以用作函数字面量。

因此,基本上,我们可以在接收者对象上调用函数字面量,并在函数的主体内部访问和调用接收者对象的方法,这与 Kotlin 中的扩展函数类似。以下是这个语法的示例:

receiver.functionLliteral(arguments) -> ReturnType

使用匿名函数

在 Kotlin 中,我们可以通过创建 lambda 表达式来拥有函数表达式。Lambda 表达式是函数字面量——也就是说,它们不是作为声明而是作为表达式,并且可以作为参数传递。然而,我们无法在 lambda 表达式中声明返回类型。尽管 Kotlin 编译器在大多数情况下会自动推断返回类型,但对于那些无法自动推断或需要显式声明的情形,我们使用匿名函数。在本食谱中,我们将了解如何使用匿名函数。

准备工作

您需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行来完成此目的,为此您需要安装 Kotlin 编译器和 JDK。我正在使用try.kotlinlang.org/上的在线 IDE 来编译和运行我的 Kotlin 代码,以完成此菜谱。您也可以使用 IntelliJ IDEA 作为开发环境。

如何做到这一点...

在以下步骤中,我们将通过一些示例学习匿名函数:

  1. 让我们从将函数声明为 lambda 函数开始:
fun main(args: Array<String>) {
    val funcMultiply = {a:Int, b:Int -> a*b}
    println(funcMultiply(4,3))
    val funcSayHi = {name: String -> println("Hi $name")} 
    funcSayHi("John")
}

在前面的代码块中,我们声明了两个 lambda 函数:一个(funcMultiply)接受两个整数并返回一个整数,另一个(funcSayHi)lambda 函数接受一个字符串并返回一个单元——也就是说,它不返回任何内容。

  1. 虽然在前面的示例中我们不需要声明参数类型和返回类型,但在某些情况下,我们需要显式声明参数类型和返回类型。我们通过以下方式使用匿名函数来完成此操作:
fun main(args: Array<String>) {
    var funcMultiply = fun (a: Int, b: Int): Int {return a*b}
    println(funcMultiply(4,3))
    fun(name: String): Unit = println("Hi $name")
}
  1. 因此,现在我们已经对匿名函数的工作方式有了大致的了解。现在,让我们尝试将一个匿名函数传递给另一个函数——也就是说,我们将尝试一个高阶函数。查看以下代码片段:
fun main(args: Array<String>) {
    var funcMultiply = fun(a: Int, b: Int): Int { return a*b }
    var funcSum = fun(a: Int, b: Int): Int { return a+b }
    performMath(3,4,funcMultiply)
    performMath(3,4,funcSum)
}
fun performMath(a:Int, b:Int, mathFunc : (Int, Int) -> Int) : Unit {
    println("Value of calculation: ${mathFunc(a,b)}")
}
  1. 因此,基本上,匿名函数的声明方式与常规函数相同,但没有名称。主体可以是一个表达式,如以下示例所示,或者是一个代码块,如前面的示例所示。需要注意的是,在匿名函数的情况下,参数总是传递在括号内,这与 lambda 表达式不同:
fun main(args: Array<String>) {
    performMath(3,4,fun(a: Int, b: Int): Int = a*b )
    performMath(3,4,fun(a: Int, b: Int): Int = a+b )
}
fun performMath(a:Int, b:Int, mathFunc : (Int, Int) -> Int) : Unit {
    println("Value of calculation: ${mathFunc(a,b)}")
}
  1. Lambda 函数和无名函数之间另一个有趣的区别是,在 lambda 函数中,return 语句会从封装函数返回,而在匿名函数中,它只是简单地从函数本身返回。

  2. 如果可以自动推断,还可以省略匿名函数的参数类型和返回类型。

  3. 匿名函数可以访问和修改其闭包内的变量。

因此,基本上,可以像常规函数一样声明一个匿名函数,而不需要名称(因此得名匿名)。它可以是表达式,也可以是代码块。

第五章:面向对象编程

本章将涵盖以下内容:

  • 在 Kotlin 中使用接口

  • Kotlin 中如何实现具有多个重写方法复杂接口

  • Kotlin 中如何扩展类(继承和扩展函数)

  • Kotlin 中如何使用泛型

  • Kotlin 中如何实现多态

  • 限制类层次结构

简介

面向对象编程,也称为OOP,是一种基于对象的编程范式。在这个编程范式中,对象以字段的形式包含数据,以方法的形式包含代码,这些代码可以用来修改同一对象的数据。在某些面向对象的语言中,对象是类的实例(例如 Java 和 Kotlin)。在面向对象编程中,我们的代码由相互交互的对象组成。在本章中,我们将学习 OOPs 的一些关键组件,例如接口、类、类层次结构和泛型。

在 Kotlin 中使用接口

面向对象编程中的接口就像合同。它们定义了行为或规则。实现它们的类需要这样做,以便符合接口定义的行为。然而,这还不是全部。Kotlin 中的接口提供了更多。在 Java 8 之前,我们无法在接口中实现方法,但在 Kotlin 中,我们也可以这样做!在这个菜谱中,我们将了解如何处理 Kotlin 中的接口。

准备工作

我将使用 IntelliJ IDEA 来编写和执行代码。你可以自由地使用任何可以运行 Kotlin 代码的 IDE。

如何做到这一点...

正如我们刚才讨论的,Kotlin 中的接口可以有方法实现;让我们按照以下步骤来验证这一点:

  1. 让我们创建一个名为DemoInterface的接口:
interface DemoInterface {

    fun implementatedMethod() {
        println("From demo interface")
    }
}

在接口中定义带有实现的方法就像在类内部做的那样。

  1. 现在,让我们看看一个实现了前面接口的类:
class IntefaceImplementation: DemoInterface
  1. 然后,你可以这样调用方法:
fun main(args: Array<String>) {
    var interfaceImplementation= IntefaceImplementation()
    interfaceImplementation.implementatedMethod()
}

这是输出结果:

 From demo interface
  1. 这种新型接口的一个关键好处是,你可以拥有多个接口的行为,因为它允许方法实现:
fun main(args: Array<String>) {
    var interfaceImplementation= IntefaceImplementation()
    interfaceImplementation.foo()
    interfaceImplementation.bar()

}
interface A {

    fun foo() {
        println("foo from A")
    }
}
class IntefaceImplementation: A,B

interface B  {
    fun bar() {
        println("foo from B")
    }
}

如前所述的代码所示,使用多个接口,我们拥有了两个实体的行为。是的,这听起来可能像是多重继承。

  1. 假设你有两种类型的接口,并且它们都有相同名称的方法,如下所示:
interface A {
    fun foo() {
        println("foo from A")
    }
}
interface B  {
    fun foo() {
        println("foo from B")
    }
}
  1. 现在,如果你尝试将两个接口都实现到一个类中,编译器将会抛出一个错误:
Error:(24, 1) Kotlin: Class 'IntefaceImplementation' must override public open fun foo(): Unit defined in packageB.A because it inherits multiple interface methods of it
  1. 原因是直观的,因为它带来了调用哪个方法的歧义。因此,Kotlin 将要求你实现该方法,并在其中调用所需的方法,类似于以下这样:
class IntefaceImplementation: A,B {
    override fun foo() {
        super<A>.foo()
        super<B>.foo()
    }
}
  1. 现在,你将简单地调用foo方法,就像之前一样:
fun main(args: Array<String>) {
    var interfaceImplementation= IntefaceImplementation()
    interfaceImplementation.foo()
}

这是输出结果:

foo from A
foo from B

Kotlin 中的接口可以有方法实现,但不能有状态。这意味着你无法在接口中声明一个属性并将其用于存储状态。要么实现它的类需要覆盖它,要么你需要实现它的访问器。

例如,你无法在接口中拥有val a=23这样的声明,尽管你可以有类似以下的内容:

val a: Int
    get() = 2

或者,简单地定义它在接口中,并在实现类中覆盖它,如下所示:

class InterfaceImplementation: A,B {
    override val a: Int=25}

接下来,我们将探讨 Kotlin 中的接口委托:

  1. 委托模式,一个对象(en.wikipedia.org/wiki/Object_(computer_science))通过将请求委托给第二个对象来处理。让我们看看以下代码:
fun main(args: Array<String>) {
    var interfaceImplementation= InterfaceImplementation(object :A{
    })
    interfaceImplementation.someMethod()
}
class InterfaceImplementation(var a:A){
    fun someMethod(){
        a.foo()
    }
}
interface A {
    fun foo() {
        println("foo from A")
    }
}
  1. 在前面的例子中,我们将foo方法的调用委托给了实现了接口 A 的对象。虽然前面的代码是完美的,但 Kotlin 允许我们直接使用该函数。看看这段代码:
class InterfaceImplementation(var a:A):A by a{
    fun someMethod(){
        foo()
    }
}
  1. 如你所见,InterfaceImplementation类实现了A,但它将实现委托给接收到的作为参数的对象。

更多内容…

现在 Kotlin 支持在接口中实现方法,你可能会想,interfaceabstract方法之间有什么区别。

在接口中,你只能定义需要由实现类覆盖的属性。然而,在抽象类中,你可以有一个与状态一起工作的实现,这样它就不能在派生类中被覆盖。在抽象类中,你可以定义一些在派生类中将相同的状态和方法。

另一个关键的区别是,你可以在抽象类中拥有最终成员,但不能在接口中。此外,接口不支持protectedinternal修饰符。它只支持private

如何在 Kotlin 中实现具有多个覆盖方法的复杂接口

SOLID是一个记忆法缩写,用于定义五个基本的面向对象设计原则:

  • 单一职责原则

  • 开放封闭原则

  • 李斯克代换原则

  • 接口分离原则

  • 依赖倒置原则

接口分离原则ISP)指出,如果一个接口变得太长,最好是将其拆分成更小的部分(接口),这样客户端就不需要实现他们不感兴趣的接口。在这个菜谱中,我们将了解为什么这是重要的。

准备工作

我们将使用 Android Studio 3.0。请确保你有其最新版本。

如何去做…

让我们看看 ISP 如何能帮到我们的一个例子:

  1. 这是一个“胖”接口的简单例子:
button.setOnClickListener(object : View.OnClickListener {
    fun onClick(View v) {
       *// TODO: do some stuff...*

    }

    fun onLongClick(View v) {
        *// we don't need it*
    }

    fun onTouch(View v, MotionEvent event) {
        *// we don't need it
*    } 
});
  1. 如你所见,大接口的问题在于我们被迫实现方法,即使我们那里没有任何事情要做。

  2. 一个简单的解决方案是将该接口拆分成更小的接口,如下面的代码所示:

interface OnClickListener { 
    fun onClick( v:View )
} public interface OnLongClickListener { 
    fun onLongClick( v: View)
} interface OnTouchListener { 
    fun onTouch( v: View,  event: MotionEvent)
  1. 注意,现在我们已经将一个大接口分解成更小的接口,这些接口可以独立实现。

  2. Kotlin 还有一个强大的功能,允许你在接口本身编写方法的完整实现。让我们看一下以下代码来理解它:

fun main(args: Array<String>) {
    Simple().callMethod()
}
class Simple:A{
    fun callMethod(){
        bar()
    }
}
interface A{
    fun bar(){
        println("Printing from interface")
    }
}
  1. 如您所见,我们在接口中实现了整个方法,并且能够从实现了该接口的类中调用它。

  2. 此功能还可以用于遵循 ISP 原则,因为我们可以在接口本身放置一个常用方法;因此,我们不需要每次实现该接口时都实现它。

如何在 Kotlin 中扩展类(继承和扩展函数)

在这个菜谱中,我们将学习如何扩展类(继承)以及如何使用 Kotlin 的扩展函数扩展类的功能。

继承可能是你在面向对象编程中学习的第一个概念。它是一种机制,其中新类从现有类派生出来。通过这种方式,类可以继承或获取其他类的属性和方法。另一方面,扩展函数允许我们跳过创建功能包装器,并能够向类添加额外的函数。现在让我们看看这两个概念。

准备工作

由于我们将处理 Android 代码,建议您使用 Android Studio 作为 IDE。源代码可以在 gitlab.com/aanandshekharroy/kotlin-cookbook 仓库的 1-recycler-view-in-kotlin 分支中找到。

如何做……

从另一个类派生出来的类称为子类,而派生子类的类称为超类。在这个例子中,我们将创建一个超类 A 和一个子类 B。要扩展类 B,我们需要在类声明中使用 :,然后添加超类名称及其主构造函数。让我们看看以下步骤:

  1. 需要记住的关键一点是,Kotlin 中的类默认是 封闭 的,不允许扩展,因此我们需要在类声明前添加 open 关键字来打开它们。所以我们的超类 A 看起来是这样的:
open class A
  1. 然后,我们可以按照以下方式扩展我们的类 B:
class B:A()
  1. 现在,假设我们的类 A 有一个接受 String 变量的主构造函数,如下所示:
open class A(var str:String)

现在,如果我们想用 A 扩展 B,有两种方法可以实现:

  • 在 B 的主构造函数中初始化 A。在这种方法中,我们将通过从 B 的主构造函数传递参数来初始化 A。考虑以下示例:
class B(var randomString:String): A(randomString)
  • 如果 B 或任何类没有主构造函数,那么扩展类的每个次级类都需要使用 super 关键字来初始化超类。考虑以下示例:
class B: A{
    constructor(randomString:String) : super(randomString)
    constructor(randomString:String, randomInt:Int) : super(randomString)
  1. 我们通常通过扩展一个类来导入超类的功能,有时我们可能还想覆盖它们以实现自己的版本。与类类似,方法默认也是封闭的,我们需要使用 open 修饰符来“打开”它们:
open class A(var str:String){
    open fun foo(){
        println("foo from A")
    }
}
class B(var string: String): A(string) {
    override fun foo(){
        println("foo from B")
    }
}
  1. 你也可以将一个方法标记为“final”,以防止任何其他子类覆盖它。考虑以下示例:
open class A(var str:String){
    final fun foo(){
        println("foo from A")
    }
}
  1. 如果你用一个抽象类扩展你的类,你需要实现抽象类中定义的所有抽象方法。请注意,你不需要将它们标记为公开,以便扩展类可以覆盖它们。将它们设置为抽象本身就完成了这项工作,如下例所示:
class B(var string: String): C() {
    override fun methodC() {
        // Do something here
    }
}
abstract class C{
    abstract fun methodC()
    fun impl(){}
}

扩展函数

扩展函数很有用,因为它们允许我们扩展类的功能,而不必实际修改它。例如,如果你使用 Glide 或 Picasso 库在 Imageview 中放置图像,你一定熟悉以下代码:

Glide.with(context).load(image_url).into(imageView)

我们可以使用扩展函数使这个看起来更好。让我们在 imageView 上调用 loadImage(imageUrl) 函数。如果你这样做,你会看到一个错误——未解析的引用- loadImage:

你还会看到两个建议,其中一个就是创建扩展函数:

如果你点击创建扩展函数,你会得到一些选择,就像这个截图所示:

点击 ImageView,因为我们想在它上面创建一个扩展函数。

当你点击它时,在同一文件中创建了一个扩展函数,看起来像这样:

private fun ImageView.loadImage(image_url: String) {

}

在这里,我们可以放置我们的 Glide/Picasso 图像加载代码:

private fun ImageView.loadImage(image_url: String) {
    Glide.with(context).load(image_url).into(this)
}

因此,即使 loadImage 函数不在 ImageView 类中,我们也能扩展它,并像这个函数是 ImageView 的一部分一样使用它,而且我们甚至不需要修改 ImageView 类。扩展函数从外部扩展了 ImageView 的功能。

它是如何工作的…

扩展函数的前缀(点号前的名称)被称为接收器类型,即被扩展的类型。这个接收器对象在函数内部使用 this 关键字访问。扩展函数是静态解析的;就像调用一个静态方法。由于这是一个静态方法,它不需要在类下定义,但由于它是静态方法,很难进行测试。例如,Mockito(一个测试框架)无法测试静态方法,所以为了产生高质量的代码,只有当该函数不需要任何测试时才使用扩展函数。

还有更多…

当你创建一个与成员函数名称相似的扩展函数时会发生什么?例如,在以下代码中,如果我们调用 c.foo(),会发生什么?

fun main(args: Array<String>) {
    var c= C()
    c.foo()
}
class C{
    fun foo(){
        println("from member")
    }
}
private fun C.foo() {
    println("from extension")
}

这是我们的输出结果:

 from member

因此,如果调用具有相同名称的扩展函数,成员函数将获胜。

如何在 Kotlin 中使用泛型

泛型方法和类帮助我们使用相同的方法或类来处理各种类型。这提高了代码的可重用性。在这个菜谱中,我们将了解泛型以及如何在 Kotlin 中使用它。Kotlin 中的泛型与 Java 中的泛型非常相似,但 Kotlin 中有额外的特殊关键字,这使得 Kotlin 中的泛型更加直观。让我们深入探讨。

准备工作

您需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行,为此您需要安装 Kotlin 编译器和 JDK。我使用 IntelliJ IDE 来编译和运行我的 Kotlin 代码,以完成这个菜谱。

如何做到这一点...

现在,让我们按照以下步骤,通过一些示例来了解 Kotlin 中泛型的工作原理:

  1. 让我们从可以接受任何类型参数的通用类开始:
fun main(args: Array<String>) {
    val intgen: GenCl<Int> = GenCl<Int>(10)
    println(intgen.a)

    // We are letting Kotlin compiler infer type
    val strgen = GenCl("A string")
    println(strgen.a)
}

class GenCl<T>(t: T) {
    var a = t
}

该程序的输出如下:

10
A string
  1. 我们也可以像这样限制在通用类中允许的类型:
fun main(args: Array<String>) {
    val intgen: GenCl<Int> = GenCl<Int>(10)
    println(intgen.a)

    val flgen = GenCl(1.0)
    println(flgen.a)
}

// Restricting T to only be of type Number
class GenCl<T: Number>(t: T) {
    var a = t
}
  1. 如果我们尝试使用前面的类与不是 Number 类型的类型,例如 String,我们会得到以下错误:
Error:(8, 17) Type parameter bound for T in constructor GenCl<T : Number>(t: T)
 is not satisfied: inferred type String is not a subtype of Number
  1. 现在,让我们尝试一个泛型方法的示例:
fun main(args: Array<String>) {
    fun <T> addTwo(a: List<T>) {
        for(x in a) {
            println(x)
        }
    }

    addTwo(listOf(10,20,30,40))
    addTwo(listOf("a","b","c","d","e"))
}

上述代码的输出将如下所示:

10 
20 
30 
40 
a 
b 
c 
d 
e

还有更多...

Java 中的泛型类型是不变的,这意味着 List<String> 不是 List<Object> 的子类型。Java 有这样的设计,以便我们无法向包含 String 且类型为 ObjectList 中添加,比如说,一个 Float。在 Kotlin 中,我们有一个更好的解决方案,即使用通配符参数 ? extends E,它表示该方法接受 E 的子类型或 E 的集合,而不仅仅是 E 本身。这使我们能够从 E 的集合中读取,但不能写入,因为我们不知道可以接受哪些项。这使得 Kotlin 具有协变性。

如何在 Kotlin 中实现多态

多态是对象根据情况采取多种形式的能力。Kotlin 支持两种类型的多态:编译时多态运行时多态。在这个菜谱中,我们将尝试两者。让我们开始吧。

准备工作

您需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行,为此您需要安装 Kotlin 编译器和 JDK。我使用 IntelliJ IDE 来编译和运行我的 Kotlin 代码,以完成这个菜谱。

如何做到这一点...

在以下步骤中,我们将学习如何在 Kotlin 中使用多态:

  1. 让我们从编译时多态开始。在编译时多态中,函数名,即签名保持不变,但参数或返回类型不同。在编译时,编译器根据参数类型等确定我们试图调用的函数。查看以下示例:
fun main(args: Array<String>) {
    println(doubleOf(4))

    println(doubleOf(4.3))

    println(doubleOf(4.323))
}

fun doubleOf(a: Int): Int {
    return 2*a
}

fun doubleOf(a: Float): Float {
    return 2*a
}

fun doubleOf(a: Double): Double {
    return 2.00*a
}

这是上述代码的输出:

8
8.6
8.646
  1. 现在,让我们谈谈运行时多态。在运行时多态中,编译器在运行时解析对重写/重载方法的调用。我们可以通过方法重写来实现运行时多态。让我们尝试一个扩展超类并重写其成员方法的示例:
fun main(args: Array<String>) {
    var a = Sup()
    a.method1()
    a.method2()

    var b = Sum()
    b.method1()
    b.method2()
}

open class Sup {
    open fun method1() {
        println("Printing method 1 from inside Sup")
    }

    fun method2() {
        println("Printing method 2 from inside Sup")
    }
}

class Sum: Sup() {
    override fun method1() {
        println("Printing method 1 from inside Sum")
    }
}

上述代码的输出如下:

Printing method 1 from inside Sup
Printing method 2 from inside Sup
Printing method 1 from inside Sum
Printing method 2 from inside Sup

在这里,编译器在运行时确定要执行哪个方法。

限制类层次结构

在本食谱中,我们将学习如何在 Kotlin 中限制类层次结构。在我们继续之前,让我们了解为什么这是一个值得花费时间的原因。

准备工作

我将使用 Android Studio 来运行本食谱中描述的代码。

如何做到这一点...

当我们确定一个值或类只能具有有限的一组类型或子类数量时,那就是我们尝试限制类层次结构的时候。是的,这听起来可能像枚举类,但实际上,它远不止于此。枚举常量仅作为单个实例存在,而密封类的子类可以有多个实例,这些实例可以包含状态。让我们看看以下步骤中的示例:

  1. 我们将创建一个名为ToastOperation密封类。在同一个源文件中,我们将定义一个ShowMessageToast子类:
class ShowMessageToast(val message:String):ToastOperation()
  1. 此外,我们将定义一个ShowErrorToast对象:
object ShowErrorToast:ToastOperation()
  1. 如您所注意到的,我定义了一个对象而不是完整的类声明,因为ShowErrorToast对象没有任何状态。此外,通过这样做,我们已从when块中移除了is,因为只有一个实例。

现在,我们可以在when语句中使用它,如下所示:

fun doToastOperation(toastOperation: ToastOperation){
    when(toastOperation){
        is ShowMessageToast ->Toast.makeText(this,toastOperation.message,Toast.LENGTH_LONG).show()
        ShowErrorToast->Toast.makeText(this,"Error.. Grr!",Toast.LENGTH_LONG).show()
    }
}
  1. 关键好处是,我们不需要实现else块,当其他语句不符合要求时,它充当默认块。

根据文档,一个密封类可以有子类,但所有这些子类都必须与密封类本身在同一个文件中声明。然而,子类的子类不需要在同一个文件中定义。它本身是抽象的,您不能从它实例化对象。

这是我们的密封类结构:

sealed class ToastOperation {
}
object ShowErrorToast:ToastOperation()
class ShowMessageToast(val message:String):ToastOperation()

如您所见,我们将所有子类都放在定义了密封类的同一个源文件中。

它是如何工作的...

在前面的示例中,我们确信我们只能有两种类型的吐司:错误吐司和自定义消息的吐司。因此,我们创建了一个密封ToastOperation,并创建了ToastOperation的两个子类。请注意,如果我们不确定子类的类型,我们不会使用密封类;在这种情况下,枚举类可能更适合。

还有更多...

如果您使用的是 Kotlin 1.1 之前的版本,您需要在密封类内部实现子类,就像这样:

sealed class ToastOperation {
    object ShowErrorToast:ToastOperation()
    class ShowMessageToast(val message:String):ToastOperation()
}

注意,您也可以在新版本的 Kotlin 中使用前面提到的方法。

第六章:集合框架

本章将涵盖以下食谱:

  • 如何合并两个集合

  • 将原始集合拆分为一对集合

  • 按指定比较器对列表进行排序

  • 降序排序

  • 使用 Gson 解析 JSON 响应

  • 如何使用 lambda 表达式进行过滤和映射

  • 如何对对象列表进行排序并保持 null 对象在末尾

  • 如何在 Kotlin 中实现一个懒列表

  • 如何在 Kotlin 中填充字符串

  • 如何展平数组或映射

  • 如何在 Kotlin 中按多个字段对集合进行排序

  • 如何在 Kotlin 列表中使用 limit

  • 如何在 Kotlin 中创建二维数组

  • 如何在 Kotlin 中跳过前 N 个条目

简介

当我们想要处理集合中的项目时,集合框架非常有用。如果你使用过 Java,你可能熟悉集合框架。集合框架最常见的使用是映射、集合、列表等。Kotlin 也有自己的集合框架,但它比 Java 的集合框架更好,因为在 Kotlin 中,我们可以利用函数式编程方法使我们的代码更加简洁且易于使用。因此,让我们深入了解与 Kotlin 集合框架相关的食谱。

如何合并两个集合

在这个食谱中,我们将看到如何将两个或多个集合合并为一个。然而,在我们继续之前,我们需要了解可变类型和不可变类型之间的区别。不可变类型对象是一个不能被改变的对象。例如,如果我们定义一个不可变列表,我们就无法向其中添加其他对象。考虑到这一点,让我们开始这个食谱!

准备工作

我将使用 IntelliJ IDEA 进行编码。只要它能够编译和运行 Kotlin 代码,你可以使用你喜欢的任何 IDE。

如何做到这一点...

你可以使用 listOf 方法在 Kotlin 中创建一个列表。然而,此方法返回的列表是一个不可变列表,因此我们需要创建一个可变列表以便向其中添加对象。让我们查看提到的步骤:

  1. 让我们创建两个列表,listAlistB,如下所示:
var listA= mutableListOf<String>("a","a","b")
var listB= mutableListOf<String>("a","c")

如果类型声明是从 listOf/mutableListOf 方法内的对象中推断出来的,我们就不需要显式地声明类型声明。因此,前面的代码将被重写为 mutableListOf("a","a","b")

  1. 现在,我们将尝试将 listA 的内容添加到 listB 中。为此,我们将需要 addAll() 方法:
fun main(args: Array<String>) {
    val listA= mutableListOf<String>("a","a","b")
    val listB= mutableListOf<String>("a","c")
    listB.addAll(listA)
    println(listB)
}

这是输出结果:

[a, c, a, a, b]
  1. 合并两个列表的另一种方法是使用 union。这返回组合集合的唯一元素:
fun main(args: Array<String>) {
    val listA= mutableListOf<String>("a","a","b")
    val listB= mutableListOf<String>("a","c")
    val listC=listB.union(listA)
    println(listC)
}

这是输出结果:

[a, c, b]
  1. 同样,可变集合也可以合并,唯一的区别是集合中的 addAll 将类似于我们使用 union 方法将获得的结果;由于它是一个集合,只允许唯一值:
val setA= mutableSetOf<String>("a","b","c")
val setB= mutableSetOf<String>("a","b","c","d")
setB.addAll(setA)
println(setB)
println(setB.union(setA))

这是输出结果:

[a, b, c, d]
[a, b, c, d]

如果你想要合并两个映射,你需要 putAll() 方法,因为 addAllunion 对于 map 来说是不存在的:

val mapA= mutableMapOf<String,Int>("a" to 1, "b" to 2)
val mapB= mutableMapOf<String,Int>("a" to 2, "d" to 4)
mapA.putAll(mapB)
println(mapA)

这是输出结果:

{a=2, b=2, d=4}

注意,键 a 在两个映射中都定义了,但后来出现的那个(在这种情况下,mapB)是获胜者。

将原始集合拆分为一对集合

有时候,您可能希望只需将列表拆分为子列表,而不需要进入forwhile循环。Kotlin 为您提供了专门为此目的的函数。在本菜谱中,我们将了解如何根据某些标准拆分列表。

准备工作

我将使用 IntelliJ IDEA 来编写和运行 Kotlin 代码;您可以使用任何能够完成相同任务的 IDE。

如何实现它...

Kotlin 提供了一个partition函数。根据partition函数的文档,它执行以下操作:

将原始数组拆分为一对列表,其中第一个列表包含谓词返回 true 的元素,而第二个列表包含谓词返回 false 的元素。

让我们通过这个示例更清楚地理解它:

  1. 在此示例中,我们将创建一个数字列表,并希望将此列表拆分为两个子列表:一个包含奇数,另一个包含偶数:
fun main(args: Array<String>) {
    val listA= listOf(1,2,3,4,5,6)
    val pair=listA.partition {
        it%2==0
    }
    println(pair)
}

这是输出:

([2, 4, 6], [1, 3, 5])
  1. 如前一个示例所示,我们需要在partition块中将条件放入谓词中。返回的对象是一个Pair对象,包含两个子列表。

  2. partition函数也可以以类似的方式与set集合一起使用:

val setA= setOf(1,2,3,4,5,6)
val pair=setA.partition {
    it%2==0
}
println(pair)

这里是输出:

([2, 4, 6], [1, 3, 5])

它是如何工作的...

让我们看看 Kotlin 中partition函数的实现:

public inline fun <T> Iterable<T>.partition(predicate: (T) -> Boolean): Pair<List<T>, List<T>> {
    val first = ArrayList<T>()
    val second = ArrayList<T>()
    for (element in this) {
        if (predicate(element)) {
            first.add(element)
        } else {
            second.add(element)
        }
    }
    return Pair(first, second)
}

如您所见,partition函数只是一个抽象,它可以帮助您避免编写冗长的循环,但内部它仍然以相同的方式执行。

还有更多...

partition函数与数组一起使用时也以类似的方式工作。以下是它的不同用法。每个用法都类似,只是产生不同类型的列表:

// Produces two lists
inline fun <T> Array<out T>.partition(
    predicate: (T) -> Boolean
): Pair<List<T>, List<T>>
// Breaks original list of Byte and produces two lists of Byte
inline fun ByteArray.partition(
    predicate: (Byte) -> Boolean
): Pair<List<Byte>, List<Byte>>

// Breaks original list of Short and produces two lists of Short
inline fun ShortArray.partition(
    predicate: (Short) -> Boolean
): Pair<List<Short>, List<Short>>
// Breaks original list of Int and produces two lists of Int
inline fun IntArray.partition(
    predicate: (Int) -> Boolean
): Pair<List<Int>, List<Int>>
// Breaks original list of Long and produces two lists of Long
inline fun LongArray.partition(
    predicate: (Long) -> Boolean
): Pair<List<Long>, List<Long>>
// Breaks original list of Float and produces two lists of Float
inline fun FloatArray.partition(
    predicate: (Float) -> Boolean
): Pair<List<Float>, List<Float>>
// Breaks original list of Double and produces two lists of Double
inline fun DoubleArray.partition(
    predicate: (Double) -> Boolean
): Pair<List<Double>, List<Double>>
// Breaks original list of Boolean and produces two lists of Boolean
inline fun BooleanArray.partition(
    predicate: (Boolean) -> Boolean
): Pair<List<Boolean>, List<Boolean>>
// Breaks original list of Char and produces two lists of Char
inline fun CharArray.partition(
    predicate: (Char) -> Boolean
): Pair<List<Char>, List<Char>>

根据指定的比较器对列表进行排序

对列表进行排序是列表上执行的最常见操作之一。当我们尝试对自定义对象列表进行排序时,我们需要指定比较器。让我们看看我们如何根据指定比较器对列表进行排序。

准备工作

我将使用 IntelliJ IDEA 来编写和运行 Kotlin 代码;您可以使用任何能够完成相同任务的 IDE。

如何实现它...

在以下示例中,我们将尝试根据某些属性对对象进行排序。这将给我们一个关于如何根据指定比较器进行排序的思路:

  1. 让我们创建一个具有年龄属性的Person类。我们将根据年龄对人员对象列表进行排序:
fun main(args: Array<String>) {
    val p1=Person(91)
    val p2=Person(10)
    val p3=Person(78)
    val listOfPerson= listOf(p1,p2,p3)
    var sortedListOfPerson=listOfPerson.sortedBy {
        it.age
    }
}
class Person(var age:Int)
  1. 要根据指定的比较器对列表进行排序,我们需要使用sortedBy函数:
fun main(args: Array<String>) {
    val p1=Person(91)
    val p2=Person(10)
    val p3=Person(78)
    val listOfPerson= listOf(p1,p2,p3)
    var sortedListOfPerson=listOfPerson.sortedBy {
        it.age
    }
}
class Person(var age:Int)

  1. Kotlin 还提供了一个sortedWith方法,您可以在其中指定自己的比较器实现:
fun main(args: Array<String>)
{
  val p1=Person(91)
  val p2=Person(10)
  val p3=Person(78)
  val listOfPerson= listOf(p1,p2,p3)
  var sortedListOfPerson=listOfPerson
  .sortedWith<Person>(object:Comparator<Person>{
      override fun compare(p0: Person, p1: Person):Int {
        if(p0.age>p1.age){
              return 1
          }
          if(p0.age==p1.age){
              return 0
          }
          return -1
      }
  })
}
class Person(var age:Int)

它是如何工作的...

sortedBy函数是 Kotlin 提供的语法糖。内部,它调用接受比较器的sortedWith方法。

现在,让我们看看sortBy函数的实现:

public inline fun <T, R : Comparable<R>> Iterable<T>.sortedBy(crossinline selector: (T) -> R?): List<T> {
    return sortedWith(compareBy(selector))
}

sortBy函数在其内部调用sortedWith方法,如下所示:*

public fun <T> Iterable<T>.sortedWith(comparator: Comparator<in T>): List<T> {
    if (this is Collection) {
       if (size <= 1) return this.toList()
       @Suppress("UNCHECKED_CAST")
       return (toTypedArray<Any?>() as Array<T>).apply { sortWith(comparator) }.asList()
    }
    return toMutableList().apply { sortWith(comparator) }
}

降序排序

在上一个菜谱中,我们看到了如何使用指定的比较器对列表进行排序。我们提供比较器,然后它按相应的方式排序。有趣的是,Kotlin 还提供了一个方法来按降序对列表中的项目进行排序。在这个菜谱中,我们将看到如何按降序对原始对象以及自定义对象进行排序。所以,让我们开始吧!

准备工作

我将使用 IntelliJ IDEA 来编写和运行 Kotlin 代码;您可以使用任何可以完成相同任务的 IDE。

如何做…

现在,我们将通过一些示例来了解如何使用降序排序:

  1. 首先,我们将尝试对一个简单的整数列表进行排序:
val listOfInt= listOf(1,2,3,4,5)
var sortedList=listOfInt.sortedDescending()
sortedList.forEach {
    print("${it} ")
}

这是输出:

5 4 3 2 1
  1. 现在,让我们使用前面菜谱中的Person列表。为了按降序排序,我们将这样做:
val p1=Person(91)
val p2=Person(10)
val p3=Person(78)
val listOfPerson= listOf<Person>(p1,p2,p3)
val sortedListOfPerson=listOfPerson.sortedByDescending {
    it.age
}
sortedListOfPerson.forEach {
    print("${it.age} ")
}

这里是输出:

91 78 10

工作原理…

sortedByDescending的工作方式有点像sortedBy。内部,两者都使用sortedWith函数:

public inline fun <T, R : Comparable<R>> Iterable<T>.sortedByDescending(crossinline selector: (T) -> R?): List<T> {
    return sortedWith(compareByDescending(selector))
}

以下是对compareByDescending的实现:

@kotlin.internal.InlineOnly
public inline fun <T> compareByDescending(crossinline selector: (T) -> Comparable<*>?): Comparator<T> =
        Comparator { a, b -> compareValuesBy(b, a, selector) }

注意,只需反转变量的顺序即可产生降序。

使用 Gson 解析 JSON 响应

在这个菜谱中,我们将学习如何解析 JSON。JSON 是 API 响应中最广泛使用的数据类型。我们将使用 Google 的开源库 Gson。它速度快,即使响应很大也能很好地扩展。

准备工作

我将使用 Android Studio 来完成这个任务,JSONObject 由 Android SDK 提供。我们将使用 Gson 进行 JSON 解析。您可以通过将以下行添加到您的build.gradle文件中来将其添加到您的项目中:

compile 'com.google.code.gson:gson:2.8.0'

如何做…

现在,让我们按照以下步骤使用 Gson 解析 JSON 数据。例如,我们将在这里使用一个原始字符串来保持事情简单:

  1. 首先,我们将使用以下方式创建一个模拟的 JSON 数据,使用原始字符串:
val jsonStr="""
    {
     "name": "Aanand Shekhar",
     "age": 21,
     "isAwesome": true
    }
""".trimIndent()
  1. 接下来,我们将创建一个数据类来保存这些数据。以下是我们的数据类看起来像这样:
data class Information(val name:String,val age:Int, val isAwesome:Boolean)
  1. 最后,我们将使用Gson来解析 JSON 字符串:
val information:Information= Gson().fromJson<Information>(jsonStr,Information::class.java)

现在,您可以使用它就像一个 Kotlin 对象一样。

更多内容…

您可以使用一些 Android Studio 插件自动创建数据类。最广泛使用的插件之一是RoboPOJOGenerator (github.com/robohorse/RoboPOJOGenerator )。

如何使用 lambda 表达式进行过滤和映射

在这个菜谱中,我们将学习如何使用 Kotlin 中的map函数转换列表,以及如何根据我们喜欢的任何标准过滤列表。我们将使用 lambda 函数,它为函数式编程提供了一种很好的方式。所以,让我们开始吧。

准备工作

我将使用 IntelliJ IDEA 来编写和运行 Kotlin 代码;您可以使用任何可以完成相同任务的 IDE。

如何做…

首先,让我们看看如何在列表上使用filter函数。filter 函数返回一个包含所有匹配给定谓词的元素的列表。我们将创建一个数字列表,并根据偶数或奇数来过滤列表。

filter方法对于不可变集合很好,因为它不会修改原始集合,而是返回一个新的集合。在filter方法中,我们需要实现谓词。谓词,就像条件一样,基于被过滤的列表。

例如,我们知道偶数项将遵循it%2==0。因此,相应的过滤方法将如下所示:

val listOfNumbers=listOf(1,2,3,4,5,6,7,8,9)
var evenList=listOfNumbers.filter {
    it%2==0
}
println(evenList)

//Output: [2, 4, 6, 8]

过滤函数的另一个变体是filterNot,正如其名称所暗示的,它返回一个包含所有不匹配给定谓词的元素的列表。

另一个酷炫的 lambda 函数是map。它转换列表并返回一个新的列表:

val listOfNumbers=listOf(1,2,3,4,5,6,7,8,9)
var transformedList=listOfNumbers.map {
    it*2
}
println(transformedList)

//Output: [2, 4, 6, 8, 10, 12, 14, 16, 18]

map函数的一个变体是mapIndexed。它在其构造中提供了索引和项:

val listOfNumbers=listOf(1,2,3,4,5)
val map=listOfNumbers.mapIndexed { index, it
    -> it*index}
println(map)

//Output: [0, 2, 6, 12, 20]

如何对列表中的对象进行排序并保持 null 对象在末尾

我们已经看到如何使用比较器根据指定参数对列表进行排序。然而,到目前为止,我们一直在处理具有非 null 值的列表。在这个菜谱中,我们将看到如何对具有 null 属性(我们根据该属性进行排序)的对象列表进行排序。所以让我们开始吧。

准备工作

我将使用 IntelliJ IDEA 来编写和运行 Kotlin 代码;你可以自由使用任何可以完成相同任务的 IDE。

如何做到这一点…

现在,让我们遵循以下步骤来排序列表,同时保持 null 对象在末尾:

  1. 让我们创建一个具有年龄属性(可以是 null)的Person类:
class Person(var age:Int?)
  1. 现在,让我们创建一个Person对象列表:
val listOfPersons=listOf(Person(10), Person(20), Person(2), Person(null))
  1. 最后,我们希望按升序对它们进行排序,同时保持 null 项在末尾:
val sortedList=listOfPersons.sortedWith(compareBy(nullsLast<Int>(),{it.age}))
sortedList.forEach {
    print(" ${it.age} ")
}

输出如下:

2 10 20 null 

它是如何工作的…

我们使用了sortedWith方法。根据文档,sortedWith是这样做的:

返回一个序列,该序列根据指定的比较器生成此序列的元素。

除了那个之外,我们还使用了kotlin.comparisons包,它为我们提供了在前面解决方案中使用的主要两个函数:

  • public inline fun <T: Comparable<T>> nullsLast(): 这个方法提供了一个比较器,用于考虑可空可比较值,其中 null 值大于任何其他值。这就是我们能够在末尾获取 null 项的原因,因为它们被认为比任何其他值都大。

  • compareBy(comparator: Comparator<in K>, crossinline selector: (T) -> K): 这个函数接受一个比较器(例如nullsLast())和一个为比较器提供值的函数,然后将它们组合成一个新的比较器。

Kotlin 中实现懒列表的方法

如果一个元素或表达式的值在定义时没有被评估,而是在首次访问时才被评估,那么它被称为延迟评估。有许多情况它都很有用。例如,你可能有一个列表 A,你想要从它创建一个过滤后的列表,让我们称它为列表 B。如果你做如下操作,过滤操作将在 B 的声明期间执行:

val A= listOf(1,2,3,4)
var B=A.filter {
    it%2==0
}

这迫使程序在定义 B 时立即初始化它。虽然这对小列表来说可能不是什么大问题,但它可能导致大对象的延迟。此外,我们可以在第一次需要时再创建对象。在这个菜谱中,我们将学习如何实现一个惰性列表。

准备工作

我将使用 IntelliJ IDEA 来编写和运行 Kotlin 代码;您可以使用任何可以完成相同任务的 IDE。

如何实现它…

要创建一个惰性列表,我们需要将列表转换为序列。序列表示惰性评估的集合。让我们用一个例子来理解它:

  1. 在给定的例子中,让我们首先根据元素是奇数还是偶数来过滤列表:
fun main(args: Array<String>) {
    val A= listOf(1,2,3,4)
    var B=A.filter {
        println("checking ${it}")
        it%2==0
    }
}

这是输出:

checking 1
checking 2
checking 3
checking 4

在前面的例子中,filter 函数仅在对象被定义时被评估。

  1. 现在,让我们将列表转换为序列。将列表转换为序列只需一步之遥;您可以使用 .asSequence() 方法或通过 Sequence{ createIterator() } 将任何列表转换为序列:
fun main(args: Array<String>) {
    val A= listOf(1,2,3,4).asSequence()
    var B=A.filter {
        println("checking ${it}")
        it%2==0
    }
}
  1. 如果您运行前面的代码,您在控制台中将看不到任何输出,因为对象尚未创建。它将在列表 B 首次访问时创建:
fun main(args: Array<String>) {
    val A= listOf(1,2,3,4).asSequence()
    var B=A.filter {
        println("checking ${it}")
        it%2==0
    }
    B.forEach {
        println("printing ${it}")
    }
}

//Output:checking 1
 checking 2
 printing 2
 checking 3
 checking 4
 printing 4

当访问项目时,filter 函数被评估。这被称为惰性求值

它是如何工作的…

Kotlin 中的序列可能是无界的,并且当列表的长度事先未知时(类似于 Java 8 中的 Streams)会使用它。由于它可以无限大,因此需要惰性求值来处理这种类型的结构。考虑以下示例:

val seq= generateSequence(1){it*2}
seq.take(10).forEach {
    print(" ${it} ")
}

这里,generateSequence 生成一个无限数字的序列,但当我们调用 take(10) 时,只有 10 个项目被评估和打印。

如何在 Kotlin 中填充字符串

有时,为了保持字符串的长度,我们会用一些字符填充字符串。在许多通信协议中,保持有效载荷的标准长度至关重要。Kotlin 使得用任何字符和长度填充字符串变得非常容易。让我们看看如何使用它。

准备工作

我将使用 IntelliJ IDEA 来编写和运行 Kotlin 代码;您可以使用任何可以完成相同任务的 IDE。

如何实现它…

在这个菜谱中,我们将使用 Kotlin 的 kotlin.stdlib 库。具体来说,我们将使用 padStartpadEnd 函数。现在,让我们按照给定的步骤来了解如何使用这些函数:

  1. 让我们看看 padStart 函数的一个例子:
fun main(args: Array<String>) {
    val string="abcdef"
    val pad=string.padStart(10,'-')
    println(pad)
}

这是输出:

 ----abcdef
  1. 接下来,我们看看 padEnd 的一个例子:
val string="abcdef"
val pad=string.padEnd(10,'-')
println(pad)

这里是输出:

 abcdef----

它是如何工作的…

填充函数需要使用函数提供的字符将字符串扩展到一定长度。因此,如果填充后的字符串长度小于原始字符串,它将只返回相同的字符串。

另一个需要注意的关键点是,默认情况下,填充字符是空格字符。这是 padStart 函数的实现:

public fun String.padStart(length: Int, padChar: Char = ' '): String
        = (this as CharSequence).padStart(length, padChar).toString()

如您所见,padChar 的默认值是空格字符,并且它是在一个字符串对象上调用的。

如何展开数组或映射

在本章的前几个食谱中,我们学习了如何创建多维数组。在本食谱中,我们将看到如何将它们转换为 1D 列表,或 flatten 它们。

准备工作

我将使用 IntelliJ IDEA 编写和运行 Kotlin 代码;您可以使用任何可以完成相同任务的 IDE。

如何实现...

我们将使用 kotlin.stdlib 库的 .flatten 方法。它接受一个数组或集合,并返回一个包含给定集合/数组中所有元素的单一列表。

例如,使用数组数组:

[[1,2,3],[1,2,3],[1,2,3]] -> [1,2,3,1,2,3,1,2,3]

fun main(args: Array<String>) {
    val a= arrayOf(arrayOf(1,2,3),arrayOf(1,2,3),arrayOf(1,2,3))
    a.flatten().forEach { print(" ${it} ") }
}

//Output:  1 2 3 1 2 3 1 2 3 

例如,使用列表列表:

[[1,2,3],[1,2,3],[1,2,3]] -> [1,2,3,1,2,3,1,2,3]

fun main(args: Array<String>) {
    val a= listOf(listOf(1,2,3),listOf(1,2,3),listOf(1,2,3))
    a.flatten().forEach { print(" ${it} ") }
}

它是如何工作的...

让我们看看 flatten() 函数的实现:

public fun <T> Iterable<Iterable<T>>.flatten(): List<T> {
    val result = ArrayList<T>()
    for (element in this) {
        result.addAll(element)
    }
    return result
}

如您所见,这只是在新的列表中添加来自可迭代对象(数组或列表)的项目,并返回该列表。

如何在 Kotlin 中按多个字段排序集合

在本食谱中,我们将学习如何在 Kotlin 中按多个字段对集合进行排序。当我们想要在两个对象在特定属性上具有相等值时给予一个对象比另一个对象优先权时,这通常很有用。例如,我们可能有一个 Student 对象的列表,并希望按年龄升序排列它们,但如果两个学生的年龄相同,我们将根据他们的 GPA 排序。在本食谱中,我们将看到如何处理此类用例。那么,让我们开始吧!

准备工作

我将使用 IntelliJ IDEA 编写和运行 Kotlin 代码;您可以使用任何可以完成相同任务的 IDE。

如何实现...

现在,让我们按照以下步骤根据对象的多个字段进行排序:

  1. 首先,让我们创建 Student 类:
class Student(val age:Int, val GPA: Double)
  1. 然后,创建一个 Student 对象的列表:
val studentA=Student(11,2.0)
val studentB=Student(11,2.1)
val studentC=Student(11,1.3)
val studentD=Student(12,1.3)
val studentsList=listOf<Student>(studentA,studentB,studentC,studentD)
  1. 要按多个字段排序,我们只需这样做:
val sortedList=studentsList.sortedWith(compareBy({it.age},{it.GPA}))
  1. 如果我们现在打印它,我们将得到以下输出:
sortedList.forEach {
    println("age: ${it.age}, GPA: ${it.GPA} ")
}

//Output: age: 11, GPA: 1.3 
 age: 11, GPA: 2.0 
 age: 11, GPA: 2.1 
 age: 12, GPA: 1.3

它是如何工作的...

我们使用了 sortedWith 函数,它接受一个比较器。比较器由 compareBy 函数提供。compareBy 有一个可以接受多个函数的重载:

public fun <T> compareBy(vararg selectors: (T) -> Comparable<*>?):Comparator<T>

如您在前面代码中所见,vararg 允许我们在其构造函数中接受多个函数,并返回一个比较器,该比较器将数据传递给 sortedWith 函数。

注意,使用多个字段排序的工作方式是按字段 1 排序,然后按字段 2 排序,然后按字段 3 排序,依此类推。

如何在 Kotlin 列表中使用 limit

在本食谱中,我们将学习如何从列表中获取特定项。我们将为此目的使用 kotlin.stdlib 库。

准备工作

我将使用 IntelliJ IDEA 编写和运行 Kotlin 代码;您可以使用任何可以完成相同任务的 IDE。

如何实现...

我们将使用 take 函数及其变体来限制列表中的项。

take(n): 返回前 n 个元素的列表:

fun main(args: Array<String>) {
    val list= listOf(1,2,3,4,5)
    val limitedList=list.take(3)
    println(limitedList)
}

//Output: [1,2,3]

takeLast(n): 返回包含最后 [n] 个元素的列表:

fun main(args: Array<String>) {
    val list= listOf(1,2,3,4,5)
    val limitedList=list.takeLast(3)
    println(limitedList)
}

//Output: [3,4,5]

takeWhile{ predicate }: 返回包含满足给定 [谓词] 的第一个元素的列表:

val list= listOf(1,2,3,4,5)
val limitedList=list.takeWhile { it<3 }
println(limitedList)

//Output: [1,2]

takeLastWhile{谓词}:与 takeWhile 类似,但它从列表的末尾评估。

takeIf { 谓词 }:如果它满足给定的 [谓词],则返回 this 值,如果不满足,则返回 null

fun main(args: Array<String>) {
    val list= listOf(1,2,3,4,5)
    var limitedList=list.takeIf { it .contains(1) }
    println(limitedList)
}

//Output: [1,2,3,4,5]

注意,takeIf lambda 中的 it 代表列表本身,而不仅仅是列表的一个元素。

如何在 Kotlin 中创建二维数组

在某些情况下,如棋盘游戏、图像等,二维数组对于数据表示非常有用。在 Java 中,我们可以通过以下方式表示二维数组:

int[][] data = new int[size][size];

由于 Kotlin 带来了新的语法,让我们看看如何在 Kotlin 中处理二维数组。

准备工作

我将使用 IntelliJ IDEA 编写和运行 Kotlin 代码;你可以自由使用任何可以完成相同任务的 IDE。

如何操作…

现在让我们按照给定的步骤在 Kotlin 中创建一个二维数组:

  1. 我们可以使用以下语法在 Kotlin 中创建一个简单的二维数组:
val array = Array(n, {IntArray(n)})

在这里,n 代表数组的维度。在这里,我们使用了 Kotlin 的 Array 类,它代表一个数组(特别是当针对 JVM 平台时,是 Java 数组)。我们通过传递大小和初始化器来初始化 Array 对象:

public inline constructor(size: Int, init: (Int) -> T)
  1. 我们的维度是 n,作为初始化器,我们传递一个一维数组,然后它提供了一个二维数组的结构。如果你想要使用特定的值初始化二维数组,你需要将其传递给初始化器。考虑以下示例:
Array<IntArray>(10,{IntArray(10,{-1})})
  1. 上述二维数组将被初始化为所有 -1

  2. 我们还可以使用 arrayOf 构造函数通过传递两个一维数组来创建一个二维数组:

val even: IntArray = intArrayOf(2, 4, 6)
val odd: IntArray = intArrayOf(1, 3, 5)

val lala: Array<IntArray> = arrayOf(even, odd)
lala.forEach {
    it.forEach {
        print(" ${it} ")
    }
    println()
}

//Output: 2 4 6 
 1 3 5 

还有更多…

你也可以通过扩展 Kotlin 的代码来创建自己的函数。例如,创建一个方法,如下所示:

inline fun <reified inside> array2d(sizeOuter: Int, sizeInner: Int, noinline innerInit: (Int)->inside): Array<Array<inside>>
       = Array(sizeOuter) { Array<inside>(sizeInner, innerInit) }

这可以通过以下操作轻松创建二维数组:

array2d(10,10,{0})

你也可以以类似的方式创建一个列表的列表。以下是一个列表的列表的示例:

fun main(args: Array<String>) {
    val a= listOf(listOf(1,2,3), listOf(4,5,6), listOf(7,8,9))
    a.forEach {
        print(" ${it} ")
    }
}

这是它的输出:

[1, 2, 3] [4, 5, 6] [7, 8, 9] 

如何在 Kotlin 中跳过前 "n" 个条目

在本食谱中,我们将学习如何在集合中删除条目。首先,我们将看到如何删除前 n 个项目,然后我们将看到如何删除最后 n 个项目,最后,我们将看到如何在删除集合中的元素时使用谓词。

准备工作

我将使用 IntelliJ IDEA 编写和运行 Kotlin 代码;你可以自由使用任何可以完成相同任务的 IDE。

如何操作…

在以下步骤中,我们将学习如何跳过 Kotlin 列表中的前 n 个条目:

  1. 首先,让我们看看如何删除集合中的前 n 个项目。我们将使用列表,但它也可以与数组一起使用。此外,我们将使用 kotlin.stdlib,它包含本食谱中所需的函数。这里要使用的函数是 drop
fun main(args: Array<String>) {
    val list= listOf<Int>(1,2,3,4,5,6,7,8,9)
    var droppedList=list.drop(2)
    droppedList.forEach {
        print(" ${it} ")
    }
}

//Output: 3 4 5 6 7 8 9 
  1. 要跳过集合中的最后 n 个项目,你需要使用 dropLast 函数:
fun main(args: Array<String>) {
    val list= listOf<Int>(1,2,3,4,5,6,7,8,9)
    var droppedList=list.dropLast(2)
    droppedList.forEach {
        print(" ${it} ")
    }
}

//Output:  1 2 3 4 5 6 7 
  1. 这个 lambda 函数在谓词返回 true 时删除项目:
val list= listOf<Int>(1,2,3,4,5,6,7,8,9,1,2,3)
val droppedList=list.dropWhile { it<3 }
droppedList.forEach {
    print(" ${it} ")
}

//Output:  3 4 5 6 7 8 9 1 2 3 
  1. 此函数在满足条件的情况下删除末尾的项目。
fun main(args: Array<String>) {
    val list= listOf<Int&gt;(1,2,3,4,5,6,7,8,9,3,1,2)
    val droppedList=list.dropLastWhile { it<3 }
    droppedList.forEach {
        print(" ${it} ")
    }
}

//Output: 1 2 3 4 5 6 7 8 9 3 

它是如何工作的…

drop 函数通过跳过前 n 个元素返回一个新的列表。内部实现上,它只是使用了普通的 for 循环,并对输入是否为数组或列表进行了一些检查。

第七章:在 Kotlin 中处理文件操作

本章将涵盖以下内容:

  • 使用 InputReader 从文件中读取

  • 使用 InputReader 从文件中读取所有行

  • 使用 InputReader 逐行读取

  • 使用 BufferedReader 从文件中读取

  • 使用 BufferedReader 从文件中读取所有行

  • 使用 BufferedReader 逐行读取

  • 通过网络读取字符串和 JSON

简介

Kotlin I/O 用于输入和输出处理。Kotlin 提供了kotlin.io API 来处理文件和流。kotlin.io中使用的某些函数是java.io类的扩展。总的来说,使用kotlin.io从文件和流中读取和写入非常简单。

使用 InputReader 从文件中读取

Kotlin.io提供了一个干净、简洁的 API 来读取和写入文件。实现这一目标的一种方法是通过使用InputReader。我们将在本食谱中看到如何做到这一点。

准备中

您需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行来完成此目的,为此您需要安装 Kotlin 编译器和 JDK。您还可以使用 IntelliJ IDEA 作为开发环境。

如何做到这一点…

读取文件的方法有很多,但了解其背后的动机非常重要,这样我们才能为我们的目的选择正确的方法:

  1. 首先,我们将尝试获取文件的InputStream,并使用读取器来读取内容:
import java.io.File
import java.io.InputStream

fun main(args: Array<String>) {
    val inputStream: InputStream = File("lorem.txt").inputStream()
    val inputString = inputStream.reader().use { it.readText() }
    println(inputString)
}
  1. 在前面的代码块中,lorem.txt只是一个我们想要读取的文件。该文件位于我们的代码源文件相同的文件夹中。如果我们需要读取位于不同文件夹中的文件,它看起来类似于以下内容:
File("/path/to/file/lorem.txt")
  1. 这段代码只是将文件中的所有文本打印到控制台上。

  2. 读取文件内容的另一种方法是直接创建文件的读取器,就像我们在以下代码中所做的那样:

import java.io.File

fun main(args: Array<String>) {
  val inputString = File("lorem.txt").reader().use { it.readText() }
  println(inputString)
}
  1. 上述两个代码块输出的结果将简单地是文件中的文本,正如它本身那样。在我们的例子中,如下所示:
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nunc consequat eleifend mauris, eget congue ipsum consectetur id.
Proin hendrerit felis metus, vitae suscipit mi tempus facilisis.
Proin ut leo tellus. Donec nec lacus vel ante venenatis porttitor et sit amet purus.
Sed tincidunt turpis ac metus pharetra dapibus.
Integer sed auctor tellus. Morbi a metus luctus, viverra enim vel, imperdiet est.
Curabitur purus massa, hendrerit id ligula et, finibus elementum purus.
In ut consectetur lacus.
Suspendisse non mauris eget dolor faucibus pharetra quis sed turpis.
Vivamus eget lectus vel mi faucibus dignissim.
Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
Ut vitae velit non nunc consectetur imperdiet.
Nunc feugiat diam tellus, in pellentesque nisl dapibus quis.
Proin luctus sapien ac ante tempor, eget mollis odio aliquet.
  1. 现在,如果我们想逐行读取文件,因为我们要对每一行进行处理,那该怎么办呢?在这种情况下,我们使用useLines()方法来代替use()方法。

  2. 查看以下示例,其中我们从文件中获取输入流,并使用useLines()方法逐行读取:

import java.io.File
import java.io.InputStream

fun main(args: Array<String>) {
    val listOfLines = mutableListOf<String>()
    val inputStream: InputStream = File("lorem.txt").inputStream()
    inputStream.reader().useLines { lines -> lines.forEach { listOfLines.add(it)} }
    listOfLines.forEach{println("$ " + it)}
}
  1. 或者,如果我们希望直接在文件上使用读取器,我们这样做:
import java.io.File

fun main(args: Array<String>) {
    val listOfLines = mutableListOf<String>()

    File("lorem.txt").reader().useLines { lines -> lines.forEach { listOfLines.add(it)} }
    listOfLines.forEach{println("$ " + it)}
}

在这种情况下,输出将是以下内容:

$ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
$ Nunc consequat eleifend mauris, eget congue ipsum consectetur id.
$ Proin hendrerit felis metus, vitae suscipit mi tempus facilisis.
$ Proin ut leo tellus. Donec nec lacus vel ante venenatis porttitor et sit amet purus.
$ Sed tincidunt turpis ac metus pharetra dapibus.
$ Integer sed auctor tellus. Morbi a metus luctus, viverra enim vel, imperdiet est.
$ Curabitur purus massa, hendrerit id ligula et, finibus elementum purus.
$ In ut consectetur lacus.
$ Suspendisse non mauris eget dolor faucibus pharetra quis sed turpis.
$ Vivamus eget lectus vel mi faucibus dignissim.
$ Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
$ Ut vitae velit non nunc consectetur imperdiet.
$ Nunc feugiat diam tellus, in pellentesque nisl dapibus quis.
$ Proin luctus sapien ac ante tempor, eget mollis odio aliquet.

它是如何工作的…

你注意到我们使用了 use()useLines() 方法来读取文件吗?对 Closeable.use() 函数的调用将在 lambda 表达式执行结束时自动关闭输入。现在,我们当然可以使用 Reader.readText(),但这不会在执行后关闭流。除了 use() 之外,还有其他方法,如 Reader.readText() 等,可以用来读取流或文件的内容。使用任何方法的决策取决于我们是否希望在执行后自动关闭流,或者我们希望处理关闭资源,以及我们是否希望从流中读取或直接从文件中读取。

还有更多...

BufferedReader 一次从输入流中读取几个字符并将它们存储在缓冲区中。这就是为什么它被称为 BufferedReader。另一方面,InputReader 只读取输入流中的一个字符,其余字符仍然留在流中。在这种情况下没有缓冲区。这就是为什么 BufferedReader 快速,因为它维护一个缓冲区,从缓冲区中检索数据总是比从磁盘检索数据更快。

使用 InputReader 读取文件中的所有行

我们可以使用 InputReader 一次性读取文件中的所有行。在本教程中,我们将学习如何做到这一点。

准备工作

您需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行来完成此目的,这需要安装 Kotlin 编译器和 JDK。您还可以使用 IntelliJ IDEA 作为开发环境。

如何操作...

让我们按照以下步骤来了解如何使用 InputReader 类读取文件:

  1. 读取文件有两种方式,其中一种是将输入流附加到文件上。让我们看看如何操作,并使用 InputReader 来读取其内容:
import java.io.File
import java.io.InputStream

fun main(args: Array<String>) {
    val inputStream: InputStream = File("example2.txt").inputStream()
    val inputString = inputStream.reader().use { it.readText() }
    println(inputString)
}
  1. 另一种方式是不获取流,直接读取文件的内容,如下例所示:
import java.io.File
fun main(args: Array<String>) {
    val inputString = File("example2.txt").reader().use { it.readText() }
    println(inputString)
}

在这种情况下,输出仅仅是文件的内容,没有变化:

A panoramic view of Lower Manhattan as seen at dusk from Jersey City, New Jersey, in November 2014\. Manhattan is the most densely populated borough of New York City. It is the city's economic and administrative center, and a major global cultural, financial, media, and entertainment center.
The second paragraph of this file is small.

我们使用 use() 方法是因为它在执行后关闭了流。

它是如何工作的...

inputStream 附加到文件上会返回一个字节数据流。我们可以使用流返回的读取器,或者我们可以直接在文件上使用读取器。inputStreamread() 方法读取流中的下一个字节。readText() 方法使用 UTF-8 或指定的字符集返回文件的整个内容作为字符串。

这个 readText() 方法不推荐用于大文件。它有一个内部限制,即文件大小为 2 GB。在处理大文件的情况下,我们从流中逐字节读取。

使用 InputReader 逐行读取

有时候我们需要逐行读取文件内容并进行处理。这可以通过使用 InputReader 逐行读取文件来实现。让我们看看如何操作。

准备工作

您需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行来完成此目的,为此您需要安装 Kotlin 编译器和 JDK。您还可以使用 IntelliJ IDEA 作为开发环境。

如何做…

在以下步骤中,我们将学习如何使用InputReader类逐行读取文本:

  1. 让我们从将InputStream附加到文件并逐行读取内容开始,如下所示:
import java.io.File
import java.io.InputStream

fun main(args: Array<String>) {
    val listOfLines = mutableListOf<String>()
    val inputStream: InputStream =  File("example2.txt").inputStream()
    inputStream.reader().useLines { lines -> lines.forEach { listOfLines.add(it)} }
    listOfLines.forEach{println("* " + it)}
}
  1. 在这种情况下,每一行都附加了*。以下是输出结果:
* A panoramic view of Lower Manhattan as seen at dusk from Jersey City, New Jersey, in November 2014\. Manhattan is the most densely populated borough of New York City. It is the city's economic and administrative center, and a major global cultural, financial, media, and entertainment center.
* The second paragraph of this file is small.
  1. 我们可以直接将读取器附加到文件上,并逐行读取。以下代码正是这样做的:
import java.io.File

fun main(args: Array<String>) {
    val listOfLines = mutableListOf<String>()

    File("example2.txt").reader().useLines { lines -> lines.forEach { listOfLines.add(it)} }
    listOfLines.forEach{println("* " + it)}
}

它是如何工作的…

useLines()方法为我们提供了一个遍历文件或流中所有行的可迭代对象,并对每一行(字符串)进行一些操作。我们将所有修改后的字符串添加到一个列表中,并打印出来。

使用BufferedReader读取文件

BufferedReader在读取时将一些字符存储在缓冲区中。这使得读取更快,因此更高效。在本例中,我们将了解如何使用BufferedReader读取文件内容。

准备中

您需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行来完成此目的,为此您需要安装 Kotlin 编译器和 JDK。您还可以使用 IntelliJ IDEA 作为开发环境。

如何做…

按照以下步骤学习更多关于BufferedReader类的工作原理:

  1. 我们可以直接将BufferedReader附加到文件上,并读取整个文件的内容,如下面的代码所示:
import java.io.File
import java.io.InputStream

fun main(args: Array<String>) {
    val inputString = File("lorem.txt").bufferedReader().use { it.readText() }
    println(inputString)
}
  1. 我们也可以逐行处理所需的内容,以便能够单独处理每一行。在以下代码中,我们逐行读取并添加一个字符到字符串的开始处和字符后的字符串长度:
import java.io.File
import java.io.InputStream

fun main(args: Array<String>) {
    val listOfLines = mutableListOf<String>()
    File("lorem.txt").bufferedReader().useLines { 
        lines -> lines.forEach { 
            var x = "> (" + it.length + ") " + it;
            listOfLines.add(x)
        } 
    }
    listOfLines.forEach{println(it)}
}
  1. 在前面的代码块中,我们是直接将读取器附加到文件上的。然而,在某些情况下,我们需要获取一个数据流。在这种情况下,我们可以从要读取的文件中获取一个输入流,然后将其附加到BufferedReader上。

  2. 在以下代码中,我们尝试使用BufferedReader逐行从文件输入流中读取:

import java.io.File
import java.io.InputStream

fun main(args: Array<String>) {
    val listOfLines = mutableListOf<String>()
    val inputStream: InputStream = File("lorem.txt").inputStream()
    inputStream.bufferedReader().useLines { 
        lines -> lines.forEach { 
            var x = "> (" + it.length + ") " + it;
            listOfLines.add(x)
        } 
    }
    listOfLines.forEach{println(it)}
}
  1. 当我们尝试一次性读取文件的全部内容时,以下是输出结果:
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nunc consequat eleifend mauris, eget congue ipsum consectetur id.
Proin hendrerit felis metus, vitae suscipit mi tempus facilisis.
Proin ut leo tellus. Donec nec lacus vel ante venenatis porttitor et sit amet purus.
Sed tincidunt turpis ac metus pharetra dapibus.
Integer sed auctor tellus. Morbi a metus luctus, viverra enim vel, imperdiet est.
Curabitur purus massa, hendrerit id ligula et, finibus elementum purus.
In ut consectetur lacus.
Suspendisse non mauris eget dolor faucibus pharetra quis sed turpis.
Vivamus eget lectus vel mi faucibus dignissim.
Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
Ut vitae velit non nunc consectetur imperdiet.
Nunc feugiat diam tellus, in pellentesque nisl dapibus quis.
Proin luctus sapien ac ante tempor, eget mollis odio aliquet.
  1. 输出类似于文件,忽略charset。如果需要,我们也可以指定所需的charset,如下面的代码所示:
bufferedReader(charset).use { it.readText() }
  1. 当我们使用上述任一示例逐行读取时,我们得到以下输出:
> (56) Lorem ipsum dolor sit amet, consectetur adipiscing elit.
> (65) Nunc consequat eleifend mauris, eget congue ipsum consectetur id.
> (64) Proin hendrerit felis metus, vitae suscipit mi tempus facilisis.
> (84) Proin ut leo tellus. Donec nec lacus vel ante venenatis porttitor et sit amet purus.
> (47) Sed tincidunt turpis ac metus pharetra dapibus.
> (81) Integer sed auctor tellus. Morbi a metus luctus, viverra enim vel, imperdiet est.
> (71) Curabitur purus massa, hendrerit id ligula et, finibus elementum purus.
> (24) In ut consectetur lacus.
> (68) Suspendisse non mauris eget dolor faucibus pharetra quis sed turpis.
> (46) Vivamus eget lectus vel mi faucibus dignissim.
> (91) Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
> (46) Ut vitae velit non nunc consectetur imperdiet.
> (60) Nunc feugiat diam tellus, in pellentesque nisl dapibus quis.
> (61) Proin luctus sapien ac ante tempor, eget mollis odio aliquet.

它是如何工作的…

使用InputStream可以帮助我们获取要读取的文件的流。我们也可以直接从文件中读取。在两种情况下,BufferedReader都会预先保存一些数据到其缓冲区中,以便更快地操作,这比使用InputReader时提高了整体读取操作的效率。

我们使用 use() 和/或 useLines() 方法代替 Reader.readText() 等方法,这样它会在执行结束时自动关闭输入流,这是一种更干净、更负责任地处理文件 I/O 的方法。然而,如果需要,当想要自己处理流的打开和关闭时,可以使用 Reader.readText() 等方法。

使用 BufferedReader 逐行读取文件中的所有行

BufferedReader 可以用来读取文件或输入流的内容。它预先保存了一些读取的内容,因此读取操作更快。在本菜谱中,我们将学习如何使用 BufferedReader 一次性读取文件的所有内容。

准备工作

您需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行来完成此目的,这需要安装 Kotlin 编译器和 JDK。您还可以使用 IntelliJ IDEA 作为开发环境。

如何操作...

在以下步骤中,我们将学习如何使用 BufferedReader 读取文件的所有行:

  1. 让我们从获取文件的 InputStream 开始,并使用 BufferedReader 来一次性读取文件内容:
import java.io.File
import java.io.InputStream
fun main(args: Array<String>) {
    val inputStream: InputStream = File("lorem.txt").inputStream()
    val inputString = inputStream.bufferedReader().use {     it.readText() }
    println(inputString)
}
  1. 在这种情况下,输出将与文件完全相同,当然,这取决于字符集。这里有一个使用另一个字符集的例子:
import java.io.File
import java.io.InputStream
fun main(args: Array<String>) {
    val inputStream: InputStream = File("lorem.txt").inputStream()
    val inputString =   inputStream.bufferedReader(Charsets.ISO_8859_1).use {  it.readText() }
    println(inputString)
}
  1. 现在,让我们快速查看一个不获取此文件 inputStream 的代码示例:
import java.io.File
import java.io.InputStream
fun main(args: Array<String>) {
    val inputString = File("lorem.txt").bufferedReader().use { it.readText() }
    println(inputString)
}
  1. 尽管您可能已经猜到了输出,但这里还是提供了输出:
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nunc consequat eleifend mauris, eget congue ipsum consectetur id.
Proin hendrerit felis metus, vitae suscipit mi tempus facilisis.
Proin ut leo tellus. Donec nec lacus vel ante venenatis porttitor et sit amet purus.
Sed tincidunt turpis ac metus pharetra dapibus.
Integer sed auctor tellus. Morbi a metus luctus, viverra enim vel, imperdiet est.
Curabitur purus massa, hendrerit id ligula et, finibus elementum purus.
In ut consectetur lacus.
Suspendisse non mauris eget dolor faucibus pharetra quis sed turpis.
Vivamus eget lectus vel mi faucibus dignissim.
Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
Ut vitae velit non nunc consectetur imperdiet.
Nunc feugiat diam tellus, in pellentesque nisl dapibus quis.
Proin luctus sapien ac ante tempor, eget mollis odio aliquet.

它是如何工作的...

BufferedReader 在读取时会将一些字符存储在缓冲区中,因此读取操作更快。我们可以直接将 BufferedReader 连接到文件或流,并从中读取。

use() 方法确保在执行完成后关闭文件或流。

使用 bufferedReader 逐行读取

在这个菜谱中,我们将了解如何使用 bufferedReader 逐行读取文件内容。

准备工作

您需要安装一个首选的开发环境,该环境可以编译和运行 Kotlin。您也可以使用命令行来完成此目的,这需要安装 Kotlin 编译器和 JDK。您还可以使用 IntelliJ IDEA 作为开发环境。

如何操作...

在给定的步骤中,我们将学习如何使用 BufferedReader 逐行读取文件:

  1. 让我们从获取文件的 InputStream 开始,并使用 BufferedReader 来逐行读取文件内容:
import java.io.File
import java.io.InputStream
fun main(args: Array<String>) {
    val listOfLines = mutableListOf<String>()
    val inputStream: InputStream = File("lorem.txt").inputStream()
    inputStream.bufferedReader().useLines {
        lines -> lines.forEach {
            var x = "# (" + it.length + ") " + it.substring(0,8);
            listOfLines.add(x)
        }
    }
    listOfLines.forEach{println(it)}
}

在这种情况下,输出如下:

# (56) Lorem ip
# (65) Nunc con
# (64) Proin he
# (84) Proin ut
# (47) Sed tinc
# (81) Integer
# (71) Curabitu
# (24) In ut co
# (68) Suspendi
# (46) Vivamus
# (91) Class ap
# (46) Ut vitae
# (60) Nunc feu
# (61) Proin lu
  1. 如果我们使用字符集,输出将取决于我们使用的字符集。这是一个带有字符集的代码示例:
import java.io.File
import java.io.InputStream
fun main(args: Array<String>) {
    val listOfLines = mutableListOf<String>()
    val inputStream: InputStream = File("lorem.txt").inputStream()
    inputStream.bufferedReader(Charsets.US_ASCII).useLines {
        lines -> lines.forEach {
            var x = "# (" + it.length + ") " + it.substring(0,8);
            listOfLines.add(x)
        }
    }
    listOfLines.forEach{println(it)}
}
  1. 现在,让我们通过一个直接从文件读取的代码示例来了解:
import java.io.File
import java.io.InputStream
fun main(args: Array<String>) {
    val listOfLines = mutableListOf<String>()
    File("lorem.txt").bufferedReader().useLines {
        lines -> lines.forEach {
            var x = "# (" + it.length + ") " + it.substring(0,8);
            listOfLines.add(x)
        }
    }
    listOfLines.forEach{println(it)}
}

它是如何工作的...

BufferedReader 为我们提供了许多方法,我们可以使用这些方法逐行读取文件或输入流的内容。使用 useLines(),我们可以获取一个行序列,然后我们可以使用 forEach 来迭代它。用户可能会终止迭代循环,因此调用者需要关闭 BufferedReader,这正是 useLines() 所做的。我们只能迭代返回的序列一次。

useLines() 的语法如下:

inline fun <T> File.useLines(
    charset: Charset = Charsets.UTF_8, 
    block: (Sequence<String>) -> T
): T

还有更多…

我们还可以使用其他方法,如 readLine() 来实现这个目的。以下是一个示例代码:

import java.io.File
import java.io.InputStream
fun main(args: Array<String>) {
    val listOfLines = mutableListOf<String>()
    val reader = File("lorem.txt").bufferedReader()
    while(true) {
        var line = reader.readLine()
        if(line == null) break
        listOfLines.add("> "+line)
    }
    listOfLines.forEach{println(it)}
}

使用 useLines() 方法的优点是它在执行后关闭流。此外,前面示例中的代码是完成同样任务的一种更符合 Kotlin 风格和更简洁的方式。

Kotlin 提供的另一个返回行序列的方法是 lineSequence(),但它执行后不会关闭 BufferedReader,这就是为什么使用 useLines() 是一个好的选择。

最后,这取决于代码将要被使用的场景。

通过网络读取字符串和 JSON

网络是应用程序的一个基本组件。我们使用的许多应用程序都连接到互联网,并涉及在互联网上读取/写入数据。在这个菜谱中,我们将学习如何在 Kotlin 中执行网络请求。虽然你也可以使用像 Retrofit、Volley 这样的第三方库,但了解在 Kotlin 中是如何实现的仍然很有价值。所以,让我们开始吧!

准备工作

我们将使用 Android 代码,所以我将使用 Android Studio。还需要包含 anko-commons 库,因为我们将使用其方法来编写代码。

如何做…

让我们按照以下步骤来了解如何在 Kotlin 中进行网络请求:

  1. 在 Kotlin 中使用简单的语法进行网络请求非常直接。以下是你在 Kotlin 中如何读取网络数据的方法:
val response = URL("<api_request>").readText()
  1. 就这样!记住,当进行网络请求时,这相当于 Java 代码:
// 1\. Declare a URL Connection
URL url = new URL("http://www.google.com");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// 2\. Open InputStream to connection
conn.connect();
InputStream in = conn.getInputStream();
// 3\. Download and decode the string response using builder
StringBuilder stringBuilder = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String line;
while ((line = reader.readLine()) != null) {
    stringBuilder.append(line);
}

  1. 然而,当然,如果你在主线程上尝试它,你会得到 NetworkOnMainThreadException 异常。为了避免这种情况,我们需要在后台进行网络调用。一种方法是通过使用 Async 任务。在 Java 中实现 Async 任务是一个痛苦的过程,但我们可以使用 Anko(一个 Kotlin 的库)轻松地做到这一点。这是你在 Kotlin 中使用 Anko 创建后台任务的方法:
doAsync{
    val response=URL("<network_url>").readText()
    uiThread{
       // Here you would do UI operation
       toast(" ... ")
    }
}
  1. 在 Java 的 Async 任务实现中,即使活动正在被销毁,Async 任务也可能被触发。这导致了防御性编程,你必须检查 UI 是否仍然存在来进行 UI 操作。然而,Anko 的后台任务实现会处理这个问题,并且如果活动正在死亡,它不会触发任务。

它是如何工作的…

doAsync返回一个 Java Future。简单来说,Future是一个代理或包装器,它围绕着一个尚未存在的对象。当异步操作完成时,你可以从中提取它。如果你想要避免与Future一起工作,doAsync有一个不同的结构,它接受一个ExecutorService

val executor = Executors.newScheduledThreadPool(5)

doAsync(executorService = executor){
    val result = URL("https://httpbin.org/get").readText()
    uiThread {
        toast(result)
    }
}

正如我们讨论的那样,如果活动正在关闭,则不会执行uiThread块。原因是它不持有上下文实例,而只持有弱引用。因此,即使该块未完成,上下文也不会泄漏。

第八章:Anko Commons 和扩展函数

本章将涵盖以下内容:

  • 使用 Gradle 设置 Anko

  • 使用扩展函数扩展 Android 框架

  • 将扩展用作属性

  • 使用 Anko 的意图

  • 使用 Anko 制作电话意图

  • 使用 Anko 发送文本意图

  • 使用 Anko 浏览网页

  • 使用 Anko 的意图分享一些文本

  • 使用 Anko 发送电子邮件

  • 使用 Anko 创建 Android 对话框

  • 使用文本项列表显示一个警告对话框

  • 在视图中使用 Anko

  • 使用 Anko 进行日志记录

  • 使用 Anko 处理尺寸

  • Android 中的版本检查

简介

Anko 是一个 Kotlin 库,它被开发出来以改善 Android 开发体验。Kotlin 本身就使 Android 开发变得容易,而 Anko 则是它的点睛之笔。Anko 几乎为所有常见的 Android 功能提供了辅助工具,大大减少了你需要编写的代码量,并使 Android 开发变得有趣。

Anko 由几个部分组成:

  • Anko Commons:它包含了一系列辅助方法,用于处理意图、对话框、日志记录等,显著减少了代码量。

  • Anko Layouts:使用这个库,你不必坚持传统的 XML 来创建视觉界面。Anko 布局是一种快速且类型安全的编写动态 Android 布局的方法。

  • Anko SQLite:这是一个 Android SQLite 的查询 DSL 和解析器集合,使得与底层 SQLite 数据库的工作变得非常简单。

  • Anko Coroutines:协程是进行异步编程的绝佳方式。Anko 协程提供了基于 kotlinx.coroutinesgithub.com/Kotlin/kotlinx.coroutines)库的实用工具。

在本章中,我们将学习如何使用 Anko 进行 Android 开发。那么,让我们开始吧!

使用 Gradle 设置 Anko

我们将首先在我们的项目中设置 Anko 库。我们将使用 Gradle 来处理项目的依赖关系。

准备工作

我将使用 Android Studio 来编写代码。你还可以在gitlab.com/aanandshekharroy/Anko-examples存储库的 1-setting-up-anko-with-gradle 分支中找到源代码。

如何做到这一点…

按照以下步骤使用 Gradle 构建系统将 Anko 添加到你的项目中:

  1. 使用 Gradle 设置 Anko 的最简单方法是,在你的 build.gradle 文件中添加以下行:
    compile "org.jetbrains.anko:anko:$anko_version"
  1. 你可以将 $anko_version 替换为 Anko 的最新版本,当本书编写时,这个版本是 0.10.1。

  2. 前面的编译语句将一次性将所有可用的功能(包括 Commons、Layouts、SQLite)添加到你的项目中。如果你不想这样做,并且希望按需单独添加它们,以下是一些编译语句:

  • anko-commons:这个库包含了许多 Android SDK 的辅助工具,用于处理意图、对话框、Toast、日志记录以及资源和尺寸:
    compile "org.jetbrains.anko:anko-commons:$anko_version"
  • Anko Layouts:Anko Layouts 是一个用于编写动态 Android 布局的 DSL:
compile "org.jetbrains.anko:anko-sdk25:$anko_version" // sdk15,19,21,23 are also available
compile "org.jetbrains.anko:anko-appcompat-v7:$anko_version"
  • anko-sqlite:这为使用 SQLite 数据库提供了辅助工具:
compile "org.jetbrains.anko:anko-sqlite:$anko_version"

  • anko-coroutines:这个库使得使用 Kotlin 协程变得更加容易:
compile "org.jetbrains.anko:anko-coroutines:$anko_version"

使用扩展函数扩展 Android 框架

这个菜谱的标题可能对你来说非常令人困惑,因为你可能会想“我怎么能扩展如此复杂的 Android 框架?而且更重要的是,为什么我要?”我们将在这个菜谱中处理所有关于扩展函数的“是什么、为什么和怎么做”。扩展函数是 Kotlin 最伟大的功能之一。所以,让我们深入探讨。

准备中

我将使用 Android Studio 进行编码。我们将为 Android SDK 类创建扩展函数。

如何做到这一点…

首先,让我们看一个非常简单的例子:

  1. 我们将创建一个非常简单的类Student,并为它创建一个扩展函数:
class Student(val age:Int)
  1. 现在,我们想要创建一个isAgeGreaterThan20函数,如果年龄大于 20 则返回true,否则返回false。现在假设有一个限制,我们不能触摸Student类,你会怎么做?

  2. 在这些场景中,当你想要扩展类的功能时,扩展函数就派上用场了。如果你尝试调用该方法,你会看到一个错误,如下所示:

  1. 然后,你需要选择创建扩展函数 ...选项来为它创建一个扩展函数。当你选择该选项时,你将再次被给出两个选项,询问你想要创建扩展函数的对象。

  1. 由于我们要为Student类创建函数,我们将从下拉菜单中选择Student选项。选择它后,IDE 将自动生成方法体。我已经将返回类型修改为返回布尔值:
private fun Student.isAgeGreaterThan20(): Boolean {

}
  1. 然后,我们可以在方法块内执行操作。我们的方法看起来是这样的:
private fun Student.isAgeGreaterThan20(): Boolean {
    return this.age>20
}
  1. 注意,由于我们是在学生对象上调用该方法,我们可以使用this关键字来访问它,尽管在这种情况下你可以省略this关键字,因为我们在这个方法中不处理相同类型的其他对象。

  2. 现在,我们可以像调用普通方法一样调用它:

fun main(args: Array<String>) {
    val studentA=Student(25)
    println(studentA.isAgeGreaterThan20())
}
>
//Output: true
  1. 现在,让我们看看一个与 Android 相关的示例。如果你使用过任何第三方库,如PicassoGlide,你可能记得像这样在ImageView中设置图片:
Picasso.with(context).from(url).into(imageView);
  1. 你可以创建一个名为loadImageImageView扩展函数,然后在你的应用程序中调用该函数。当然,loadImage不是ImageView类提供的函数,所以你需要创建一个扩展函数来达到这个目的。我们将在这个imageView对象上调用该方法,并传递一个url
imageView.loadImage(url)
private fun ImageView.loadImage(url: String) {
    Picasso.with(this.context).load(url).into(this)
}
  1. 注意,在loadImage函数中,我们正在将this引用到调用该函数的ImageView对象上。

它是如何工作的…

扩展函数是静态解析的,这意味着它们是普通的静态方法,并且与它们扩展的类(这就是为什么我们能够扩展我们无法修改的类)没有关联,除了接受这个类的实例作为参数。

如果你反编译 Kotlin 的字节码,你会看到代码被转换为 Java:

private static final boolean isAgeGreaterThan20(@NotNull Student $receiver) {
   return $receiver.getAge() > 20;
}

如你所见,它只是一个静态方法,并接受类作为参数。

还有更多...

由于扩展函数非常有用,你可能想大量使用它们。然而,权力越大,责任越大。由于它们是静态解析的,你不应该在所有地方都使用它们,因为静态函数很难测试。不负责任地使用它们意味着你的代码将更难以测试,因此更难以维护。

将扩展用作属性

在上一个菜谱中,我们学习了扩展函数。在这个菜谱中,我们将学习扩展属性。如果你需要从类中获取一个或多个属性,你可以使用扩展属性来添加它们。在这个菜谱中,我们将学习如何使用扩展属性。

准备工作

我将使用 Android Studio 进行编码。确保你有配置了 Kotlin 的最新版本的 Android Studio。

如何做到这一点...

让我们看看扩展属性的例子:

  1. 我们将使用共享首选项的例子。你可能习惯于这样做来获取共享首选项:
PreferenceManager.getDefaultSharedPreferences(this)
  1. 你可以在Context类上创建一个名为偏好的扩展属性,并按以下方式访问它:
val Context.preferences: SharedPreferences
       get() = PreferenceManager
       .getDefaultSharedPreferences(this)
context.preferences.getInt("...")

它是如何工作的...

扩展函数没有修改类,扩展属性也是如此;它们不会向类本身添加属性,因此我们在这种情况下没有后端字段。由于我们没有后端字段,我们无法初始化它。处理它们的唯一方法是通过自定义获取器和设置器。

还有更多...

与扩展属性类似,我们可以有伴随对象扩展,这意味着我们可以向类的伴随对象添加方法,帮助我们以静态方式访问它。让我们看一个例子。假设我们有一个Student类:

class Student(val age:Int){
    companion object{

    }
}

现在让我们给伴随对象添加一个扩展方法:

fun Student.Companion.sayHi(){
    println("Hi")
}

现在,你可以在不创建类实例的情况下访问它:

Student.sayHi()

使用 Anko 与意图一起使用

意图是 Android 应用中最常用的组件之一。它们可以被看作是用于在不同 Android 组件之间传递消息的信使。例如,当你需要启动一个活动时,你会发送一个意图;当你需要启动一个服务时,你会发送一个意图。要在 Android 中启动一个活动,你首先需要创建一个意图,然后将其传递给startActivity方法。在下面的例子中,我们将尝试使用一些数据和标志启动一个活动:

val intent = Intent(this, SomeActivity::class.java)
intent.putExtra("data", 5)
intent.setFlag(Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)

此外,你可以假设所有你带有意图传递的数据都占用额外的行。

Anko 有一种更好的方法来实现类似的结果。在本食谱中,我们将学习如何使用 Anko 库实现这一点(启动意图)。

准备工作

我将使用 Android Studio 进行编码目的。您需要在您的 build.gradle 文件中在 app 级别包含 Anko 库。只需添加这些行,然后您就可以开始了:

compile "org.jetbrains.anko:anko-commons:$anko_version"

如何操作…

在 Anko 中创建意图非常简单。让我们检查以下步骤:

  1. 我们前面编写的代码所实现的功能,使用 Anko 只需几行代码就可以实现:
startActivity(intentFor<SomeActivity>("data" to 5).singleTop())
  1. 如果您不想添加标志,这要简单得多:
startActivity<SomeActivity>("data" to 5)
  1. 添加额外数据不需要额外的行:
startActivity<SomeActivity>("data" to 5, "another_data" to 10)

它是如何工作的…

让我们看看前面方法的源代码实现:

inline fun <reified T: Any> Context.intentFor(vararg params: Pair<String, Any?>)

intentFor 方法接受 vararg 作为参数,因此我们可以向其提供多个数据。此方法调用 createIntent,它实际上创建了一个包含提供数据的意图,其外观如下:

fun <T> createIntent(ctx: Context, clazz: Class<out T>, params: Array<out Pair<String, Any?>>): Intent {
    val intent = Intent(ctx, clazz)
    if (params.isNotEmpty()) fillIntentArguments(intent, params)
    return intent
}
private fun fillIntentArguments(intent: Intent, params: Array<out Pair<String, Any?>>) {
    params.forEach {
        val value = it.second
        when (value) {
            null -> intent.putExtra(it.first, null as Serializable?)
            is Int -> intent.putExtra(it.first, value)
            is Long -> intent.putExtra(it.first, value)
            is CharSequence -> intent.putExtra(it.first, value)
            is String -> intent.putExtra(it.first, value)
            is Float -> intent.putExtra(it.first, value)
            is Double -> intent.putExtra(it.first, value)
            is Char -> intent.putExtra(it.first, value)
            is Short -> intent.putExtra(it.first, value)
            is Boolean -> intent.putExtra(it.first, value)
            is Serializable -> intent.putExtra(it.first, value)
            is Bundle -> intent.putExtra(it.first, value)
            is Parcelable -> intent.putExtra(it.first, value)
            is Array<*> -> when {
                value.isArrayOf<CharSequence>() -> intent.putExtra(it.first, value)
                value.isArrayOf<String>() -> intent.putExtra(it.first, value)
                value.isArrayOf<Parcelable>() -> intent.putExtra(it.first, value)
                else -> throw AnkoException("Intent extra ${it.first} has wrong type ${value.javaClass.name}")
            }
            is IntArray -> intent.putExtra(it.first, value)
            is LongArray -> intent.putExtra(it.first, value)
            is FloatArray -> intent.putExtra(it.first, value)
            is DoubleArray -> intent.putExtra(it.first, value)
            is CharArray -> intent.putExtra(it.first, value)
            is ShortArray -> intent.putExtra(it.first, value)
            is BooleanArray -> intent.putExtra(it.first, value)
            else -> throw AnkoException("Intent extra ${it.first} has wrong type ${value.javaClass.name}")
        }
        return@forEach
    }
}

如您所见,它以传统的方式内部创建意图,并调用 fillIntentArguments,该函数将数据填充到意图中。

使用 Anko 创建拨打电话的意图

在上一个食谱中,我们学习了如何使用 Anko 库创建意图。在随后的食谱中,我们将看到如何使用 Anko 中的意图执行常见操作,如发送消息、拨打电话、发送邮件等。

准备工作

我将使用 Android Studio 进行编码。您需要在您的 build.gradle 文件中包含 Anko 库。只需将以下行添加到您的 build.gradle 文件中,然后您就可以开始了:

compile "org.jetbrains.anko:anko-commons:$anko_version"

您也可以克隆 gitlab.com/aanandshekharroy/Anko-examples 仓库并切换到 3-intent-actions 分支以获取源代码。

如何操作…

让我们按照给定的步骤使用意图拨打电话:

  1. Anko 提供了围绕使用意图可以执行的最常见操作的包装器;其中之一是拨打电话。为此,Anko 提供了 makeCall 函数,该函数接受您想要拨打的电话号码:
makeCall("+9195XXXXXXXX")
  1. makeCall 函数在操作成功时返回 true,如果操作未成功则返回 false。需要注意的是,您需要在您的清单文件中添加 CALL_PHONE 权限:
<uses-permission android:name="android.permission.CALL_PHONE"/>

它是如何工作的…

让我们看看 makeCall 函数 的源代码:

fun Context.makeCall(number: String): Boolean {
    try {
        val intent = Intent(Intent.ACTION_CALL, Uri.parse("tel:$number"))
        startActivity(intent)
        return true
    } catch (e: Exception) {
        e.printStackTrace()
        return false
    }
}

在包装器下面,它正在以 Android SDK 以前使用的老方法进行操作,即使用一个动作 Intent.ACTION_CALL 的隐式意图。

使用 Anko 发送文本意图

Anko 提供了围绕意图操作的包装器,这使得调用操作变得非常简单。其中之一就是发送短信。在本食谱中,我们将看到如何启动一个向电话号码发送消息的意图。

准备工作

我将使用 Android Studio 进行编码。您需要在您的 build.gradle 文件中包含 Anko 库。只需添加给定的行,然后您就可以开始了:

compile "org.jetbrains.anko:anko-commons:$anko_version"

您也可以克隆 gitlab.com/aanandshekharroy/Anko-examples 仓库并切换到 3-intent-actions 分支以获取源代码。

如何做…

让我们按照以下步骤使用意图发送短信:

  1. Anko 提供了 sendSMS 方法,它接受两个参数——其中一个参数是电话号码,另一个是消息:
sendSMS("+9195XXXXXX","Hi")
  1. 调用此方法将启动消息应用,或者如果您有多个此类应用,它将询问您要启动哪个消息应用,并将预先填充消息正文。调用此函数需要您添加以下权限,否则它将抛出安全异常:
<uses-permission android:name="android.permission.SEND_SMS"/>

它是如何工作的…

要了解其工作原理,让我们深入了解其实现:

fun Context.sendSMS(number: String, text: String = ""): Boolean {
    try {
        val intent = Intent(Intent.ACTION_VIEW, Uri.parse("sms:$number"))
        intent.putExtra("sms_body", text)
        startActivity(intent)
        return true
    } catch (e: Exception) {
        e.printStackTrace()
        return false
    }
}

如您所见,它使用隐式意图在您的设备上启动消息应用。由于此函数需要一个上下文,如果您从片段中调用它,您需要将其作为 activity.sendSMS(..) 调用。

使用 Anko 浏览网络浏览器

在这个菜谱中,我们将讨论 Anko 包装器,它将帮助我们使用网络浏览器浏览网站。那么,让我们开始吧。

准备工作

我将使用 Android Studio 进行编码。您需要在您的 build.gradle 文件中包含 Anko 库。只需添加以下代码行,您就可以开始了:

compile "org.jetbrains.anko:anko-commons:$anko_version"

您也可以克隆 gitlab.com/aanandshekharroy/Anko-examples 仓库并切换到 3-intent-actions 分支以获取源代码。

如何做…

现在,让我们看看如何使用意图启动浏览器。

Anko 提供了一个 browse 函数,它接受网页地址并在您的设备上启动浏览器。如果您有多个浏览器,它将显示一些选项供您选择。以下是一个示例:

browse("http://www.google.com")

您放入参数中的网页地址需要以 http://https:// 作为前缀,否则它将抛出 ActivityNotFound 异常。

它是如何工作的…

Anko 提供的 browse 函数只是一个语法糖,其下是我们之前使用的相同代码:

fun Context.browse(url: String, newTask: Boolean = false): Boolean {
    try {
        val intent = Intent(Intent.ACTION_VIEW)
        intent.data = Uri.parse(url)
        if (newTask) {
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        }
        startActivity(intent)
        return true
    } catch (e: ActivityNotFoundException) {
        e.printStackTrace()
        return false
    }
}

调用此方法返回 true 或 false,这取决于操作是否成功。

使用 Anko 的意图共享一些文本

在这个菜谱中,我们将探讨如何使用 Anko 包装器来共享文本。共享文本是一个非常常见的事情,Anko 为此提供了一个非常容易使用的包装器。那么,让我们开始吧!

准备工作

我将使用 Android Studio 进行编码目的。您需要在您的 build.gradle 文件中包含 Anko 库。只需添加给定的代码行,您就可以开始了:

compile "org.jetbrains.anko:anko-commons:$anko_version"

您也可以克隆 gitlab.com/aanandshekharroy/Anko-examples 仓库并切换到 3-intent-actions 分支以获取源代码。

如何做…

在以下步骤中,我们将看到如何使用意图共享文本:

  1. Anko 提供了一个 share 方法,该方法接受一个字符串参数,即要分享的文本和一个可选的参数 subject。主题参数在通过电子邮件应用分享文本时特别有用。毕竟,谁会给 WhatsApp 消息加上主题呢?让我们看看它的实现:
share("Hey","Some subject")
  1. 没有主题——这不会填写邮件的主题行:
share("Hey")

简单到这种程度!

它是如何工作的…

如果你查看实现,你会发现 Anko 只提供了语法糖,这大大减少了你的代码行数,以实现类似的功能:

fun Context.share(text: String, subject: String = ""): Boolean {
    try {
        val intent = Intent(android.content.Intent.ACTION_SEND)
        intent.type = "text/plain"
        intent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject)
        intent.putExtra(android.content.Intent.EXTRA_TEXT, text)
        startActivity(Intent.createChooser(intent, null))
        return true
    } catch (e: ActivityNotFoundException) {
        e.printStackTrace()
        return false
    }
}

正如你所见,库已经处理了可能出现的所有问题,并只提供了一个辅助函数来使事情更快、更有趣。

使用 Anko 发送电子邮件

在这个菜谱中,我们将看到如何使用 Anko 的包装器发送电子邮件。发送电子邮件非常有用,因为几乎所有的应用都提供了一个联系方式。所以,让我们开始吧!

准备工作

我将使用 Android Studio 进行编码。你需要在你的 build.gradle 文件中包含 Anko 库。只需添加以下行即可:

compile "org.jetbrains.anko:anko-commons:$anko_version"

你也可以在 gitlab.com/aanandshekharroy/Anko-examples 仓库中克隆,切换到 3-intent-actions 分支以获取源代码。

如何操作…

我们将使用 Anko 库提供的 email 函数,该函数接受三个参数,其中只有一个参数是必需的:

email("support@XXXXXX.com","Subject","Text")

如果你不想在电子邮件中预填充文本,你可以移除主题和文本。

它是如何工作的…

让我们看看它的实现:

fun Context.email(email: String, subject: String = "", text: String = ""): Boolean {
    val intent = Intent(Intent.ACTION_SENDTO)
    intent.data = Uri.parse("mailto:")
    intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(email))
    if (subject.isNotEmpty())
        intent.putExtra(Intent.EXTRA_SUBJECT, subject)
    if (text.isNotEmpty())
        intent.putExtra(Intent.EXTRA_TEXT, text)
    if (intent.resolveActivity(packageManager) != null) {
        startActivity(intent)
        return true
    }
    return false

}

正如你所见,它会检查额外的数据,例如主题和消息正文,然后启动电子邮件应用。Anko 提供的电子邮件函数只是一个方便的方法,它可以减少你的代码行数,并使你的代码看起来更美观。

使用 Anko 创建 Android 对话框

Anko 库的一个真正出色的功能是它可以帮助你轻松且代码量更少地创建警报对话框。

在这个菜谱中,我们将看到如何在 Anko 中创建警报对话框。

准备工作

我将使用 Android Studio 来编写代码。你还需要通过在你的 build.gradle 文件中添加以下行来包含 Anko 库:

 compile "org.jetbrains.anko:anko:$anko_version"

你可以在 gitlab.com/aanandshekharroy/Anko-examples/ 仓库的 2-creating-dialogs-using-anko 分支中找到源代码。

如何操作…

让我们按照提到的步骤在 Kotlin 中创建一个对话框:

  1. 在第一个示例中,我们将尝试创建一个简单的警报框。要创建它,你只需要遵循以下语法:
alert("A simple alert","Alert") {

        }.show()
  1. 如果你尝试运行它,你会看到类似这样的内容:

图片

  1. 有一些情况下,你希望用户执行某些操作,因此 Anko 为此提供了方法。查看以下示例:
alert("Would you like some action?","Alert") {
    yesButton {
         toast("Clicked on Yes")
    }
    noButton {
         toast("Clicked on No")
    }
    neutralPressed("Meh"){
         toast("Not interest")
    }
}.show()
  1. 如上图所示,它填充了警报对话框:

图片

  1. 你还可以通过将它们替换为 positiveButtonnegativeButton 来自定义 yesButtonnoButton 的文本。以下是一个示例:
alert("Would you like some action?","Alert") {
    positiveButton("Hell Yeah!") {
        toast("Clicked on Yes")
    }
    negativeButton("No way!") {
        toast("Clicked on No")
    }
    neutralPressed("Meh?"){
        toast("Not interest")
    }
}.show()

如果你运行前面的代码,你将在设备上看到一个对话框出现,如下所示:

图片

  1. 在 Android 开发中常用的一种对话框类型是进度对话框。你可以使用 Anko 创建一个像这样的进度对话框:

图片

  1. 这种进度对话框非常适合显示用户已完成的进度。它还提供了诸如 incrementProgressBy 这样的功能,通过它可以增加进度条。要创建这样的进度对话框,你需要像以下示例那样使用它:
val dialog = progressDialog(message = "Please wait a bit…", title = "Fetching data")
dialog.show()
  1. 也许你想要一个不确定的进度条,看起来像这样:

图片

要创建一个不确定的进度对话框,就像前面的截图所示,只需将以下行添加到之前的代码中:

indeterminateProgressDialog("This is an indeterminate progress dialog").show()

显示包含文本项的警告对话框

在之前的菜谱中,我们看到了如何创建不同类型的对话框。在本菜谱中,我们将看到如何创建一个包含文本项的警告对话框,其外观如图所示:

图片

准备工作

我将使用 Android Studio 编写代码。你还需要通过在 build.gradle 文件中添加以下行来包含 Anko 库:

 compile "org.jetbrains.anko:anko:$anko_version"

如何实现...

让我们按照给定的步骤创建一个包含项目列表的警告对话框。

Anko 提供了用于创建包含项目列表的对话框的选择器。选择器非常易于使用。你只需要提供警告对话框的标题、列表以及当选项被选中时将执行的 lambda 表达式。以下是其实施示例:

val companies = listOf("Google", "Microsoft", "HP", "Apple")
selector("Where do you work?", companies, { dialogInterface, i ->
    toast("So you work at ${companies[i]}, right?")
})

就这些了!这真的很简单和简洁。所以,在前面的示例中,如果你点击列表项,你将看到一个消息提示,上面写着“所以你在谷歌工作,对吧?”

工作原理...

Anko 隐藏了所有复杂性,并为你提供了一个易于使用的函数来实现复杂的事情。让我们来看看选择器函数的实现:

fun Context.selector(
        title: CharSequence? = null,
        items: List<CharSequence>,
        onClick: (DialogInterface, Int) -> Unit
) {
    with(AndroidAlertBuilder(this)) {
        if (title != null) {
            this.title = title
        }
        items(items, onClick)
        show()
    }
}

如你所见,在表面之下,它和以前的方法一样,但 Anko 提供了语法糖,这有助于我们用更少的代码实现相同的事情。

在视图中使用 Anko

Anko 使处理视图和创建布局变得极其快速和简单。使用 Anko,我们可以编写易于阅读和编写的干净代码。在本菜谱中,我们将学习 Anko 在处理 Android 中的视图时如何使用。

准备工作

我将使用 Android Studio 3 来编写代码。你可以通过在 Android Studio 3+中创建一个新的项目并添加一个空白活动来开始,因为我们不会使用其他菜谱中的任何代码。你还需要对 Android 开发有一个中级理解。确保你已经通过在你的 app 级别的build.gradle文件中添加以下行并将项目同步来添加 Anko 依赖项:

 compile "org.jetbrains.anko:anko:$anko_version"

这里,$anko_version是 Anko 的最新版本。

如何做到这一点……

Anko 使一些常见的 Android 开发任务变得极其简单,例如 toast、snackbar 和对话框。通常,显示这些视图需要大量的代码。让我们看看 Anko 是如何仅用几行简单的代码就能做到的:

警告对话框:一个出现在你视图顶部的弹出窗口,通常用于警告:

  • 要显示一个警告,我们使用以下语法(DSL 语法):
alert("Hi, I'm Moss", "This, Jen, is the internet") {
    yesButton { toast("Oh…") }
    noButton {toast("Well...") }
}.show()
  • 假设我们使用Appcompat对话框工厂中的对话框:
alert(Appcompat, "Hello, Jen.").show()
  • 我们还可以显示进度对话框和不定进度对话框:
val dialog = progressDialog(message = "Please stand by", title = "Fetching data")

indeterminateProgressDialog("You just have to wait indefinitely Jen.").show()

Toast:它们可以用来显示短时间的信息:

  • 根据具体情况,我们可以使用以下语法之一来显示 toast:
toast("Hi! I'm Roy")
toast(R.string.meet_roy)
longToast("We have been together for a long time.")

Snackbar:Snackbar 与 Toast 消息类似,但它们提供了与它们交互的操作:

  • 根据你是否使用字符串或字符串资源以及 snackbar 的超时时间长度以及是否需要操作按钮,有不同方式来显示 snackbar。为了显示 snackbar,你需要一个指向你希望显示 snackbar 的父视图的引用。在 XML 的情况下,你可以通过 Anko 找到一个方法来通过 ID 获取视图,在 DSL 的情况下,你可以直接使用存储父视图的变量。以下是我们可以使用的一些语法:
snackbar(rootView, "Hi! I'm Jen")
snackbar(rootView, R.string.go_away_jen)
longSnackbar(rootView, "I'm going to be here for a long time")
snackbar(rootView, "What do you want?", "Click me") { doSomething() }

Anko 使定义布局和处理已创建的布局(在 XML 中)变得更加容易。

在 DSL 中创建布局

  • 在 DSL 中创建布局非常简单,我们可以直接将其放在活动的onCreate()方法中,如下面的代码所示:
lateinit var rootView: View
lateinit var btn: Button
lateinit var editText1: EditText
lateinit var editText2: EditText

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    rootView = verticalLayout {
        padding = dip(20)

        editText1 = editText { 
            hint = "What's your name?"
        }

        editText2 = editText {
            hint = "What's your message?"
        }

        btn = button("Click me") {
            onClick {
                toast( "Hey! Here is a toast for you.")
            }
        }

    }
}
  • 或者,我们也可以将其放在一个实现AnkoComponent接口的外部类中:
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        MainActivityUI().setContentView(this)
    }

    class MainActivityUI : AnkoComponent<MainActivity> {
        override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
            verticalLayout {
                padding = dip(20)

                editText {
                    hint = "What's your name?"
                }

                editText {
                    hint = "What's your message?"
                }

                button("Click me") {
                    onClick {
                        toast( "Hey! Here is a toast for you.")
                    }
                }

            }
        }
    }
}
  • 此外,这就是我们的布局看起来:

处理已存在的 XML 布局的视图

  • 假设我们有一个来自我们旧项目的以下 XML 布局:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="android.my_company.com.helloworldapp.HelloWorldActivity">

    <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>

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:background="@color/white"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <EditText
            android:id="@+id/name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="What is your name?"/>

        <EditText
            android:id="@+id/message"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Your message"/>

        <Button
            android:id="@+id/btn_send"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Send"/>

    </LinearLayout>

</android.support.design.widget.CoordinatorLayout>
  • 我们可以使用 Anko 从这个 XML 布局中访问视图,也可以设置/获取这些视图的属性。查看以下代码:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_hello_world)

    var toolbar = find<Toolbar>(R.id.toolbar)
    setSupportActionBar(toolbar)

    var name = find<EditText>(R.id.name)
    var msg = find<EditText>(R.id.message)
    var buttonSend = find<Button>(R.id.btn_send)

    buttonSend.onClick {
        toast("Hello, ${name.text} we have recorded your message!")
    }
}

这就是我们的布局看起来:

使用 Anko 进行日志记录

日志记录是调试应用程序的绝佳方式。你可能已经使用过 android.util.Log,这不是一个很方便的记录消息的方式,因为它要求你为每条消息提供 Log 标签,并且还需要你定义标签,这通常每次都是类名。Anko 提供了 anko-logger,它包含在 anko-commons 中。这是一个非常方便的记录消息的方式,因为它不需要你一定覆盖日志标签。在本食谱中,我们将学习如何做到这一点。

准备工作

我将使用 Android Studio 来编写代码。你需要在你的 build.gradle 文件中添加 anko-commons。Anko 日志记录器包含在 anko-commons 库中:

dependencies {
    compile "org.jetbrains.anko:anko-commons:$anko_version"
}

如何做到这一点…

按照以下步骤学习如何使用 Anko 库进行日志记录:

  1. 在 Anko 中进行日志记录非常简单。你只需要实现 AnkoLogger,如下所示:
class MainActivity : AppCompatActivity(),AnkoLogger {
  1. 然后,你可以按照以下方式记录消息:
info(“info message”)
  1. 以下是对各种日志级别及其与 android.Log.util 的比较:
android.util.Log AnkoLogger
v() verbose()
d() debug()
i() info()
w() warn()
e() error()
wtf() wtf()
  1. 默认的标签名称是类名。如果你想覆盖 log 标签,你需要覆盖 loggertag 属性:
class MainActivity : AppCompatActivity(),AnkoLogger {
  override val loggerTag="CustomTag"
  1. 你还可以将记录器用作普通对象。以下是从文档中提供的示例,它使用记录器作为普通对象:
class SomeActivity : Activity() {
   private val log = AnkoLogger<SomeActivity>(this)
   private val logWithASpecificTag = AnkoLogger("my_tag")

   private fun someMethod() {
       log.warning("Big brother is watching you!")
   }
}
  1. 每个方法都有两个版本:普通和懒加载(内联):
info(“info message”)
info{“info message”}
  1. 如果 Log.isLoggable(tag, Log.INFO) 为真,则执行懒加载版本。

使用 Anko 处理维度

在 XML 中,我们使用 dpdip 作为布局和视图的密度无关像素,使用 sp 作为文本的缩放无关像素dp 是一个虚拟像素,用于以密度无关的方式定义布局大小;spdp 类似,但它还会根据用户的字体偏好进行缩放。在本食谱中,我们将了解如何在 DSL 布局中定义视图和文本的 dpsp 维度。

准备工作

我将使用 Android Studio 3 来编写代码。你可以在 Android Studio 3+ 中创建一个新的项目,并添加一个空白活动来开始,因为我们不会使用其他食谱中的任何代码。你还需要对 Android 开发有一个中级理解。确保你已经通过在你的 app-level build.gradle 文件中添加以下行并将项目同步来添加 Anko 依赖项:

 compile "org.jetbrains.anko:anko:$anko_version"

在这里,$anko_version 是 Anko 的最新版本。

如何做到这一点…

在给定的步骤中,我们将学习如何使用 Anko 库来处理维度:

  1. 让我们创建一个包含 120 dip 宽度和 wrapContent 高度的按钮以及一个 24 sp 文字大小的文本视图的布局。我建议你自己尝试,使用以下语法:
dip(dipValue)
sp(spValue)
  1. 以下是通过使用 dip()sp() 方法创建布局的一种方式。sp 通常用于文本,但为了演示,我在下一个示例中使用了它来设置视图的高度。Anko 默认将 textSize 属性的值转换为 sp,而您必须提供浮点数:
verticalLayout {
    padding = dip(20)

    textView {
        text = "A big text view"
        textSize = 24f
    }

    button("Click me") {
        onClick {
            toast( "Hey! Here is a toast for you.")
        }
    }.lparams(dip(280), sp(80))

}
  1. 以下是我们布局的外观:

图片

  1. Anko 还提供了方便我们的额外方法,即 px2dip(pixels)px2sp(pixels),分别用于将像素转换为 dipsp。我记得在 Anko 存在之前手动编写这些代码,所以它们在许多时候都很有用。

Android 版本检查

Android 版本发布非常频繁。随着每个最新版本的 Android,您都会获得新的功能和改进。尽管谷歌非常努力地提供向后兼容性,但他们并不总是能够做到。例如,Material 设计组件没有向后兼容性;您需要针对 API 级别大于 21 才能使用它们。这要求开发者事先检查该 API 级别是否支持该组件,以确保您的应用程序在所有级别上都能平稳运行。我们通常这样做:

if(Build.VERSION.SDK_INT>Build.VERSION_CODES.JELLY_BEAN){

}

Anko 提供了辅助函数,帮助我们以更简单的语法实现类似的功能。在本教程中,我们将看到如何使用它。

准备工作

我将使用 Android Studio 来编写代码。您还需要通过在 build.gradle 文件中添加以下行来包含 Anko 库:

 compile "org.jetbrains.anko:anko:$anko_version"

如何实现...

在 Anko 中添加版本检查非常简单,Anko 为此提供了两个主要函数:

  • doIfSdk:这个函数接受版本代码作为参数,以及一个函数。如果设备的 API 级别等于提供的版本代码,则执行该函数。以下是这个函数的一个示例:
doIfSdk(Build.VERSION_CODES.LOLLIPOP){
    // Do something specific to version 21
}
  • doFromSdk:这个函数也接受版本代码作为参数,以及一个函数,如果设备的 SDK 级别大于或等于提供的版本代码,则执行该函数。以下是这个函数的一个示例:
doFromSdk(Build.VERSION_CODES.LOLLIPOP){
    // Execute this method on API >=21
}

它是如何工作的...

让我们看看前面两个辅助方法的实现:

  • 对于 doIfSdk:
inline fun doIfSdk(version: Int, f: () -> Unit) {
    if (Build.VERSION.SDK_INT == version) f()
}
  • 对于 doFromSdk:
inline fun doFromSdk(version: Int, f: () -> Unit) {
    if (Build.VERSION.SDK_INT >= version) f()
}

如您所见,它只是隐藏在背后的旧 Android SDK 代码。doFromSdkdoIfSdk 只是其上层的语法糖。

第九章:Anko 布局

本章将涵盖以下菜谱:

  • 在 Gradle 中为 Anko 布局设置 Anko 库

  • 以编程方式创建用户界面

  • 使用旧版 XML 布局代码

  • 使用提供的 AnkoComponent 接口

  • 在 Anko 中为 Android 视图设置主题

  • 为 Anko 视图设置布局参数

  • 向 Anko 视图添加监听器

  • 将 XML 布局插入到 DSL 中

  • 将 XML 文件转换为 DSL

  • 显示 Snackbar

  • 显示 Toast

  • 使用合成属性访问视图

  • 使用扩展函数访问视图组中的视图

简介

Anko 是一个 Kotlin 库,它使 Android 开发变得更快、更简单。它还使代码更简洁。我们大多数人习惯于为 Android 的 UI 编写 XML 布局,这是冗余的,既不类型安全也不为空安全。它还会消耗设备上的 CPU 时间和电池来解析 XML。一些程序化编写布局的人知道代码会变得多么庞大,而且维护起来也非常困难。

使用 Anko,我们可以使用 DSL 来定义布局。使用 DSL 的优点是它们易于阅读和编写,并且没有运行时开销。如果您熟悉 Android 开发和 XML 布局,本章将帮助您快速开始使用 Anko 布局。

在 Gradle 中为 Anko 布局设置 Anko 库

要开始使用任何库,首先要做的事情是将它的依赖项添加到我们的项目中,以便能够在我们的项目中使用它的方法和功能。在本菜谱中,我们将探讨如何使用 gradle 将 Anko 布局的依赖项添加到我们的项目中。

准备工作

我将使用 Android Studio 3 来编写代码,因为它是目前最新的。您可以通过在 Android Studio 3+ 中创建一个新的 Kotlin 项目并添加一个空白活动来开始,因为我们不会使用其他菜谱中的任何代码。您还需要对 Android 开发有一个中级理解。

如何操作...

在以下步骤中,我们将向我们的项目中添加 Anko:

  1. 我们可以通过在 build.gradle 依赖项中添加以下行一次添加所有 Anko 功能和组件:
// Anko
compile "org.jetbrains.anko:anko:$anko_version"

在这里,$anko_version 是 Anko 的最新版本。您可以用 Anko 当前最新版本替换它。

  1. 之后,同步您的 build.gradle 文件。现在,Anko 依赖项已经添加到您的项目中。让我们通过简单地使用 Anko 公共库来创建并显示一个警报对话框来检查这一点。通过在您的 XML 布局中定义它并在上面添加 onClickListener 来在您的活动中创建一个按钮,点击它应该运行以下代码:
alert("This is my message from alert dialog", "An Alert!") {
    yesButton { toast("Thanks for clicking ok") }
    noButton {
        toast("Got it!") }
}.show()
  1. 如果点击按钮后出现一个警报,显示我们已经成功将 Anko 库添加到我们的项目中,这就是警报对话框的样式:

图片

  1. 然而,大多数时候我们只需要在我们的项目中添加 Anko 的单个功能。例如,本例中的 Anko 布局。所以让我们尝试只添加 Anko 布局库到我们的项目中。从 build.gradle 和您的 Activity 中移除之前的代码,然后重新开始。

  2. 现在将以下行添加到您的项目 app 级 build.gradle 依赖项中:

// Anko Layouts
compile "org.jetbrains.anko:anko-sdk25:$anko_version"
compile "org.jetbrains.anko:anko-appcompat-v7:$anko_version"
  1. 同步您的 build.gradle 文件,如果没有错误,您现在可以在项目中使用 Anko 布局。在此阶段,我们还应该添加 Anko 协程的依赖项,因为我们显然需要在布局上添加监听器。您可以通过将以下行添加到您的 build.gradle 文件中来添加这些依赖项:
// Coroutine listeners for Anko Layouts
compile "org.jetbrains.anko:anko-sdk25-coroutines:$anko_version"
compile "org.jetbrains.anko:anko-appcompat-v7-coroutines:$anko_version"

  1. 完成!现在,让我们检查一切是否运行得完美。为此,让我们向我们的主活动添加一个基本的 DSL 布局。查看以下目标活动 onCreate() 方法的代码:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    verticalLayout {
        button("Hello World button!") {
            onClick { toast("Hello, World!") }
        }
    }
}
  1. 现在在您的手机上运行应用程序;如果布局工作正确,即屏幕上有一个带有文本 HELLO WORLD BUTTON! 的按钮,那么我们已经成功地将 Anko 布局依赖项添加到我们的项目中。这就是我们的布局看起来像这样:

  1. 此外,当点击按钮时,我们会得到如下 toast 信息:

它是如何工作的...

通过在我们的 build.gradle 文件中添加项目依赖项,惊人的 Gradle 会负责处理我们的项目所需的库和依赖项。Gradle 会在我们机器上或远程仓库中定位依赖项,并且任何传递依赖项都会自动包含。Gradle 使得添加项目依赖项变得极其简单和快速,我们可以将大部分时间投入到创建我们惊人的软件中,而不是维护和解决在大型项目中变得极其困难的依赖项。

还有更多...

如果您不使用 Gradle 并且不想在项目中使用它,您可以直接从 jcenter 仓库jcenter.bintray.com/org/jetbrains/anko/)添加 Anko 库 JAR 作为库依赖项。

以编程方式创建用户界面

使用 XML 编写 UI 既不安全也不安全,它还会消耗 CPU 和电池。以编程方式编写 UI(尤其是在 Java 中)对于大型和复杂的 UI 来说变得庞大且难以管理。这就是 Anko 布局出现的时候。我们可以使用 Anko 布局轻松地使用 DSL 创建布局,并且它也没有运行时开销。在本菜谱中,我们将了解如何使用 DSL 创建布局。

准备工作

我将使用 Android Studio 3 来编写代码。您可以通过在 Android Studio 3+ 中创建一个新的 Kotlin 项目并添加一个空白活动来开始,因为我们不会使用其他菜谱中的任何代码。您还需要对 Android 开发有一个中级理解。请确保您已经将 Anko 布局依赖项添加到您的项目中(在本章中遵循菜谱 在 gradle 中设置 Anko 库用于 Anko 布局)。

如何做到这一点...

让我们从使用 Anko 为我们的目标活动(您想要创建布局的活动)创建布局的简单示例开始:

  1. 这是您需要在目标活动中放置的 onCreate() 方法的代码:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    verticalLayout {
        padding = dip(20)
        val name = editText {
            hint = "What is your name?"
        }
        val message = editText {
            hint = "Your message"
        }
        button("Send") {
            onClick { toast("Hello, ${name.text} we have recorded your message!") }
        }
    }
}
  1. 在前面的代码中,我们想要创建一个基本的“联系我们”表单。为此,我们创建了一个带有 20 dip 填充的垂直线性布局,并在垂直线性布局内部添加了两个用于姓名和消息的编辑文本。点击按钮时,我们获取数据并通过 toast 向用户显示消息已记录的确认。这是屏幕部分的外观:

图片

  1. Anko 布局 DSL 是一种在更少的代码行中构建 UI 的绝佳方式。它易于阅读和编写,且简洁。它没有 XML 布局中存在的运行时开销。Anko 布局也支持 XML,您可以使用自定义组件,也可以为监听器使用协程。您还可以在 Android Studio 中使用AnkoComponent接口预览 DSL 布局,我们将在本章后面学习。

  2. 让我们尝试另一个例子,在这个例子中,我们将前面的布局放入一个带有工具栏的协调器布局中。为了能够使用协调器布局,我们需要添加 Anko 设计支持库的依赖项。将以下行添加到您的build.gradle文件中,并同步您的项目:

// Anko layouts design support
compile "org.jetbrains.anko:anko-design:$anko_version"
  1. 目前市面上有很多 Anko 为各种 Android 支持库提供的工具。以下是一个列表:
// Appcompat-v7 (only Anko Commons)
 compile "org.jetbrains.anko:anko-appcompat-v7-commons:$anko_version"
// Appcompat-v7 (Anko Layouts)
 compile "org.jetbrains.anko:anko-appcompat-v7:$anko_version"
 compile "org.jetbrains.anko:anko-coroutines:$anko_version"
// CardView-v7
 compile "org.jetbrains.anko:anko-cardview-v7:$anko_version"
// Design
 compile "org.jetbrains.anko:anko-design:$anko_version"
 compile "org.jetbrains.anko:anko-design-coroutines:$anko_version"
// GridLayout-v7
 compile "org.jetbrains.anko:anko-gridlayout-v7:$anko_version"
// Percent
 compile "org.jetbrains.anko:anko-percent:$anko_version"
// RecyclerView-v7
 compile "org.jetbrains.anko:anko-recyclerview-v7:$anko_version"
 compile "org.jetbrains.anko:anko-recyclerview-v7-coroutines:$anko_version"
// Support-v4 (only Anko Commons)
 compile "org.jetbrains.anko:anko-support-v4-commons:$anko_version"
// Support-v4 (Anko Layouts)
 compile "org.jetbrains.anko:anko-support-v4:$anko_version"
  1. 现在我们需要的是一个能够适应父元素整个宽度和高度的coordinator layout,在其内部,我们需要一个带有工具栏的应用栏,在应用栏下方我们需要之前提到的垂直布局。我建议你在查看我的方法之前先自己尝试编写这段代码,方法如下:
coordinatorLayout {
    fitsSystemWindows = true
    lparams {
        width = matchParent
        height = matchParent
    }
    appBarLayout {
        toolbar {
            setTitleTextColor(Color.WHITE)
            id = R.id.toolbar
            title = resources.getString(R.string.main_activity)
         }.lparams {
             width = matchParent
             height = wrapContent
         }
    }.lparams { width = matchParent }
    verticalLayout {
        verticalLayout {
            background = context.getDrawable(R.color.colorLightGrey)
            gravity = Gravity.CENTER
            textView("logo"){
                textColor = context.getColor(R.color.colorAccent)
                textSize = 24f
            }.lparams(width = wrapContent, height = wrapContent) {
                horizontalMargin = dip(5)
                topMargin = dip(10)
            }
        }.lparams(width = matchParent, height = dip(200)) {
              horizontalMargin = dip(5)
              topMargin = dip(10)
          }
        padding = dip(20)
        val name = themedEditText(theme = R.style.newInput) {
            id = R.id.name
            hint = "What is your name?"
        }
        val message = editText {
            id = R.id.message
            hint = "Your message"
        }
        themedButton("Send", theme = R.style.newButton)                             {
            id = R.id.btn_send
        }
    }.lparams {
          width = matchParent
          height = matchParent
          behavior = AppBarLayout.ScrollingViewBehavior()
      }
 }
  1. 在前面的函数中使用的lparams是用于向视图添加布局参数的扩展函数。这就是在我们的应用中布局的外观:

图片

使用 DSL 创建布局与 XML 本身有些相似,这是故意的,因为开发者之前有 XML 的经验,这也让我们能够在动态添加视图的同时即时计算事物。

它是如何工作的...

XML 解析是在编译时完成的(除了少数几件事情)。它引入了 CPU 和电池开销。对于非常复杂的布局,它还引入了应用中的延迟,有时甚至严重影响了用户体验。

在 Anko 布局中,DSL 在运行时构建布局,因此我们可以包含任何内容。它还避免了运行时开销,我们可以避免空指针异常。此外,我们不需要进行类型转换,也可以避免findViewById调用。

使用 XML 布局的旧代码

Anko 布局的最好之处在于其灵活性,能够与我们的 XML 布局一起工作。Anko 还通过提供视图属性使事情变得简单。在本食谱中,我们将看到如何使用 XML 布局,同时仍然能够使用 Anko 布局来改进事物。

准备工作

我将使用 Android Studio 3 来编写代码。您可以通过在 Android Studio 3+中创建一个新的 Kotlin 项目并添加一个空白活动来开始,因为我们不会使用其他菜谱中的任何代码。您还需要对 Android 开发有中级理解。请确保您已经将 Anko 布局依赖项添加到项目中(遵循本章中的菜谱在 gradle 中设置 Anko 库以用于 Anko 布局)。

如何做到这一点...

在以下步骤中,我们将学习如何使用 XML 布局和 Anko 布局一起工作:

  1. 让我们从先有一个旧的 XML 文件开始工作。将以下代码添加到您将要添加为目标活动内容视图的 XML 布局中:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout        xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="android.my_company.com.helloworldapp.MainActivity"
    tools:showIn="@layout/activity_main"
    android:orientation="vertical"
    android:padding="20dp">

    <EditText
        android:id="@+id/name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="What is your name?"/>

    <EditText
        android:id="@+id/message"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Your message"/>

    <Button
        android:id="@+id/btn_send"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Send"/>

</LinearLayout>
  1. 传统上,我们在活动中使用findViewById()onClickListener()来操作布局元素的属性并处理事件。然而,使用 Anko 布局,这变得和以下一样简单:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    setSupportActionBar(toolbar)

    var name = find<EditText>(R.id.name)
    var msg = find<EditText>(R.id.message)
    var buttonSend = find<Button>(R.id.btn_send)

    buttonSend.onClick {
        toast("Hello, ${name.text} we have recorded your message!")
    }
}
  1. 上文是目标活动的onCreate()方法。请注意,find()方法比findViewById()简单得多。

  2. 我们可以获取和设置视图属性,还可以将监听器附加到视图事件。另一件事是,Kotlin 的 Android 扩展函数还允许我们处理视图而不使用find方法。查看以下代码,它使使用合成扩展属性获取和设置视图属性变得超级简单:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    setSupportActionBar(toolbar)
    var nameText = name.text
    var msg = message.text
    btn_send.onClick {
        toast("Hello, $nameText we have recorded your message!")
    }
}
  1. 在这里,namemessagebtn_send分别是 XML 布局中视图的 ID。

它是如何工作的...

Anko 为我们提供了这些扩展函数和属性,使得访问视图变得更加容易。其中一些函数和属性被预先安排成类型安全的构建器,这些构建器是通过 Android JAR 文件生成的。

还有更多...

值得理解 Kotlin 的合成属性是如何工作的。Kotlin 生成一些额外的代码,帮助我们像使用属性一样使用视图,变量命名类似于视图的 ID。基本上,它是在我们第一次尝试将视图作为属性访问时运行findViewById(),并将其存储在缓存中,以便所有对同一视图的连续调用都调用findCachedViewById(),从而使访问变得更快。

使用提供的 AnkoComponent 接口

我们可以直接在onCreate()方法中定义活动布局 DSL,但有时将 UI 分离到另一个类会更方便。在这个菜谱中,我们将看到如何使用AnkoComponent接口来实现这一点。

准备工作

我将使用 Android Studio 3 来编写代码。您可以通过在 Android Studio 3+中创建一个新的 Kotlin 项目并添加一个空白活动来开始,因为我们不会使用其他菜谱中的任何代码。您还需要对 Android 开发有中级理解。请确保您已经将 Anko 布局依赖项添加到项目中(遵循本章中的菜谱在 gradle 中设置 Anko 库以用于 Anko 布局)。

如何做到这一点...

在给定的步骤中,我们将学习如何使用 AnkoComponent 接口进行工作:

  1. 让我们先在实现AnkoComponent接口的不同类中添加我们的 UI。如下所示:
class MainActivityUI : AnkoComponent<MainActivity> {
    override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
    verticalLayout {
        padding = dip(20)
        val name = editText {
            id = R.id.name
            hint = "What is your name?"
        }

        val message = editText {
            id = R.id.message
            hint = "Your message"
        }

        button("Send") {
            id = R.id.btn_send
        }
    }
}
  1. 注意,前面的类实现了AnkoComponent接口。我们需要重写createView()方法并从中返回 DSL 布局。现在,让我们看看如何获取这个布局并将其设置到我们的活动中。检查我们活动中的修改后的onCreate()方法:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    MainActivityUI().setContentView(this)
}
  1. 现在,让我们尝试在我们的活动中访问这些视图,如果我们已经正确设置了布局,我们应该能够做到这一点。我们将像访问 XML 布局中的视图一样访问它们:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    MainActivityUI().setContentView(this)
    btn_send.onClick { toast("Hello, ${name.text} we have recorded your message!") }
}
  1. 下一张图片将展示屏幕的显示效果:

图片

  1. 此外,在输入详细信息并点击按钮后,我们会看到如下所示的 toast:

图片

它是如何工作的...

verticalLayout(这是一个垂直线性布局)块是 Anko 提供的扩展函数,它创建一个新的视图实例并将其添加到父级。对于 Android 框架中的每个视图都有这样的扩展函数。例如,我们在前面的例子中也使用了按钮和编辑文本。我们也可以将其用作button(),它接受一个字符串参数作为按钮上的文本,或者button{}如果我们想设置该视图的任何属性。

更多内容...

如果我们在另一个类中使用AnkoComponent接口来创建我们的 DSL,我们也可以使用 Anko 支持插件预览我们的布局 DSL。

为了这样做,首先从 Android Studio 设置中的插件中添加 Anko 支持插件。之后,将光标放在MainActivityUI声明内部,通过点击视图|工具窗口|Anko 布局预览打开 Anko 布局预览工具窗口,然后按刷新。

如果布局预览没有正确渲染,请重新构建项目。这是窗口的显示方式:

图片

在 Anko 中为 Android 视图设置主题

如果我们无法为视图设置样式,我们的 Android 应用将不会那么美观。Anko 布局让我们能够将自定义主题应用到我们的视图上。在本菜谱中,我们将学习如何在 Anko 中创建主题视图。

准备中

我将使用 Android Studio 3 来编写代码。您可以通过在 Android Studio 3+中创建一个新的 Kotlin 项目并包含一个空白活动来开始,因为我们不会使用其他菜谱中的任何代码。您还需要对 Android 开发有一个中级理解。请确保您已经将 Anko 布局依赖项添加到项目中(遵循本章中的菜谱在 gradle 中设置 Anko 库的 Anko 布局)。

如何操作...

在给定的步骤中,我们将学习如何使用 Anko 为 Android 视图设置主题:

  1. 让我们先为按钮创建一个样式。自定义样式是在res/values/目录中的styles.xml文件内创建的。让我们创建一个按钮样式,命名为newButton。在styles.xml中添加以下代码:
<style name="newButton" parent="android:Widget.Holo.Light.Button">
    <item name="android:colorButtonNormal">@color/colorAccent</item>
    <item name="android:textColor">@color/white</item>
</style>
  1. 现在,让我们使用这个样式在我们的目标活动中创建一个主题按钮。让我们使用AnkoComponent接口将我们的 UI 保持在另一个类中。以下是如何在 DSL 布局中创建一个具有自定义主题的按钮的示例(注意代码中的粗体部分):
class MainActivityUI : AnkoComponent<MainActivity> {
    override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
        verticalLayout {
            padding = dip(20)
            val name = editText {
                id = R.id.name
                hint = "What is your name?"
            }

            val message = editText {
                id = R.id.message
                hint = "Your message"
            }

            themedButton("Send", theme = R.style.newButton)                                       {
                id = R.id.btn_send }
        }
    }

}
  1. 此外,为了设置我们活动的布局,我们在onCreate()方法中添加了LayoutActivity().setContentView(this)这一行,如下所示(注意代码中的粗体部分):
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
 MainActivityUI().setContentView(this)
        btn_send.onClick { toast("Hello, ${name.text} we have recorded your message!") }
    }
  1. 以下是我们应用屏幕顶部的样子,主题按钮的背景颜色与我们在res/values/目录下的colors.xml文件中定义的强调颜色一致。文字颜色为白色,正如我们在自定义样式中设置的那样:

图片

  1. 这就是我们设置视图主题的方式,通过在视图名称之前附加主题关键字并将其转换为驼峰式来设置。我们将主题作为参数传递给函数。

主题视图也是 Anko 布局提供的 Kotlin 扩展函数。

为 Anko 视图设置布局参数

没有布局参数,我们几乎无法对布局做任何事情。在这个菜谱中,我们将看到如何在我们布局 DSL 中的视图中使用布局参数。

准备工作

我将使用 Android Studio 3 来编写代码。您可以通过在 Android Studio 3+中创建一个新的 Kotlin 项目并添加一个空白活动来开始,因为我们不会使用其他菜谱中的任何代码。您还需要对 Android 开发有中级理解。请确保您已经将 Anko 布局依赖项添加到项目中(遵循本章中的菜谱在 gradle 中设置 Anko 库以用于 Anko 布局)。

如何做到这一点...

在以下步骤中,我们将学习如何为 Anko 视图设置布局参数:

  1. 让我们从创建一个继承自AnkoComponent接口的外部类中的视图开始。为了向视图添加布局参数(我们使用 Anko 提供的扩展函数添加),我们使用lparams()扩展函数,它在 DSL 视图块的末尾添加,类似于这样:
val message = editText {
    id = R.id.message
    hint = "Your message"
}.lparams(){
 // We specify our layout parameters here
}
  1. 让我们尝试一个简单的垂直布局示例;查看以下代码块(注意代码中的粗体部分):
verticalLayout {
    verticalLayout {
        background = context.getDrawable(R.color.colorLightGrey)
        gravity = Gravity.CENTER
        textView("logo"){
            textColor = context.getColor(R.color.colorAccent)
            textSize = 24f
        }.lparams(width = wrapContent, height = wrapContent) {
            horizontalMargin = dip(5)
 topMargin = dip(10)
        }

    }.lparams(width = matchParent, height = dip(200)) {
        horizontalMargin = dip(5)
 topMargin = dip(10)
    }
    padding = dip(20)
    val name = themedEditText(theme = R.style.newInput) {
        id = R.id.name
        hint = "What is your name?"
    }

    val message = editText {
        id = R.id.message
        hint = "Your message"
    }

    themedButton("Send", theme = R.style.newButton) {
        id = R.id.btn_send
    }
}
  1. themedEditTextthemedButton扩展函数是由 Anko 提供的,用于创建具有主题的编辑文本和按钮。如果您不想使用主题视图,只需调用editText()button()而不传递主题参数即可。

图片

  1. 让我们通过另一个示例来了解,在这个示例中我们有一个包含页面标题的工具栏。查看下一个示例,它使用协调布局、应用栏布局和工具栏。给定的代码生成与代码后面的截图一致的布局:
coordinatorLayout {
    fitsSystemWindows = true
    lparams {
 width = matchParent
 height = matchParent
 }
    appBarLayout {
        toolbar {
            setTitleTextColor(Color.WHITE)
            id = R.id.toolbar
            title = resources.getString(R.string.main_activity)
         }.lparams {
 width = matchParent
 height = wrapContent
 }
    }.lparams { width = matchParent }
    verticalLayout {
        verticalLayout {
            background = context.getDrawable(R.color.colorLightGrey)
            gravity = Gravity.CENTER
            textView("logo"){
                textColor = context.getColor(R.color.colorAccent)
                textSize = 24f
            }.lparams(width = wrapContent, height = wrapContent) {
 horizontalMargin = dip(5)
 topMargin = dip(10)
 }
        }.lparams(width = matchParent, height = dip(200)) {
 horizontalMargin = dip(5)
 topMargin = dip(10)
 }
        padding = dip(20)
        val name = themedEditText(theme = R.style.newInput) {
            id = R.id.name
            hint = "What is your name?"
        }
        val message = editText {
            id = R.id.message
            hint = "Your message"
        }
        themedButton("Send", theme = R.style.newButton)                                                 {
            id = R.id.btn_send
        }
    }.lparams {
 width = matchParent
 height = matchParent
 behavior = AppBarLayout.ScrollingViewBehavior()
 }
 }    

以下是我们应用中的布局外观:

图片

它是如何工作的...

lparams 也是一个添加到视图的 Anko 扩展函数,我们可以将布局参数定义为属性。如果您在使用 lparams() 时省略宽度或高度,它们的值将自动默认为 wrapContent,就像在 XML 中一样。传递的参数是命名参数。一些属性包括 horizontalMarginverticalMarginmargin。对于不同的布局,我们有不同的布局参数,就像在 XML 中一样。例如,对于相对布局,我们有 alignParentBottom()alignParentTop()alignParentStart()leftOf(viewIdOfReferenceView)topOf(viewIdOfReferenceView) 等等。

查看以下示例,它具有作为根布局的相对布局:

class MainActivityUI : AnkoComponent<MainActivity> {
    override fun createView(ui: AnkoContext<MainActivity>) =     with(ui) {
        relativeLayout {
            button("Ok") {
                id = R.id.ok
            }.lparams { leftOf() } 
            button("Cancel").lparams { leftOf(R.id.ok) }
            lparams(matchParent, matchParent)
        }
    }
}

这就是前面布局的外观:

向 Anko 视图添加监听器

我们在 Android 中的视图中具有事件监听器。让我们了解 Anko 如何通过为我们提供监听器辅助工具来简化这一过程。

准备工作

我将使用 Android Studio 3 来编写代码。您可以通过在 Android Studio 3+ 中创建一个新的 Kotlin 项目并添加一个空白活动来开始,因为我们不会使用其他菜谱中的任何代码。您还需要对 Android 开发有一个中级理解。请确保您已将 Anko 布局依赖项添加到项目中(遵循本章中的菜谱 在 gradle 中设置 Anko 库以用于 Anko 布局)。

如何操作…

在以下步骤中,我们将学习如何向 Anko 视图添加事件监听器:

  1. 让我们从监听按钮上的点击事件的一个简单示例开始。以下是给具有 btn_send ID 的按钮附加 onClick 监听器的代码:
btn_send.onClick { toast("Hello there we have recorded your message!") }

  1. 前面的代码与以下代码相同:
var btn = find<EditText>(R.id.btn_send)
btn.setOnClickListener(object : OnClickListener {
    override fun onClick(v: View) {
      toast("Hello there we have recorded your message!")
    }
})
  1. 现在,让我们创建一个包含按钮和评分栏的布局。我们将在按钮上附加一个 onLongPress 监听器,并在评分栏上附加一个 onRatingBarChange 监听器。查看以下代码:
verticalLayout {
    padding = dip(20)
    val name = editText {
        id = R.id.name
        hint = "What is your name?"
    }

    val message = editText {
        id = R.id.message
        hint = "Your message"
    }

    button("Send") {
        id = R.id.btn_send
        onLongClick {
 toast("Hello there we have recorded your message!")
 }
    }

    var rating = ratingBar {
        id = R.id.rating_bar
        onRatingBarChange { ratingBar, rating, fromUser ->
 toast(rating.toString())
 }
    }.lparams(wrapContent, wrapContent)
}

重点关注前面代码中的粗体文本。我们可以通过直接将它们放在定义的视图内部来附加监听器。这就是我们的布局外观:

  1. 在前面屏幕上长按具有文本“发送”的按钮时,我们会看到预期的 toast。查看以下屏幕:

  1. 类似地,如果我们从评分栏中选择一个评分,我们会得到一个显示我们选择的评分的 toast。

  

  1. 我们也可以将监听器与布局分开,如下面的代码所示。但是,我们需要设置监听器的视图的 ID 才能使此操作生效:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    MainActivityUI().setContentView(this)
    btn_send.onLongClick {
        toast("Hello there we have recorded your message!")
    }
}

它是如何工作的…

Anko 提供事件监听器作为扩展函数,以帮助简化添加事件监听器的过程。我们还可以将这些监听器辅助工具传递给协程,并部分定义具有许多方法的监听器,即我们可以分别定义每个监听器方法,然后 Anko 如果它们在同一个视图中,则将它们合并。

还有更多...

协程用于编写异步非阻塞代码。你也可以说协程是由用户管理的线程。

将 XML 布局插入到 DSL 中

有时可能会出现这样的情况,我们可能需要在 DSL 布局中包含一个 XML 布局。Anko 提供了一个解决方案。在这个菜谱中,我们将了解如何将 XML 布局包含到 DSL 中。

准备工作

我将使用 Android Studio 3 来编写代码。你可以在 Android Studio 3+ 中创建一个新的 Kotlin 项目,并包含一个空白活动来开始,因为我们不会使用其他菜谱中的任何代码。你还需要对 Android 开发有一个中级理解。确保你已经将 Anko 布局依赖项添加到你的项目中(遵循本章中的菜谱 在 gradle 中设置 Anko 库以用于 Anko 布局)。

如何操作...

在以下步骤中,我们将学习如何将 XML 布局插入到 DSL 布局中:

  1. 要在 DSL 中包含 XML 布局,我们使用 include() 方法。我们可以通过简单地添加 {} 并在其中定义我们的视图属性来向使用 include() 方法创建的视图添加视图属性。我们也可以向视图添加布局参数,就像我们在 DSL 视图中做的那样。查看以下语法:
include<View>(R.layout.layoutName) {
    id = R.id.someId
    hint = "Some hint"
    text = "Some text"
}.lparams() {}
  1. 让我们在 XML 中创建一个布局,然后将其包含在我们的 DSL 布局中。让我们在线性布局中创建一个按钮,并将其保存到名为 test.xml 的文件中。查看以下我们将保存到 text.xml 中的布局代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:orientation="vertical"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="10dp">

    <Button
        android:id="@+id/btn_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Send"
        android:background="@color/colorAccent"
        android:textColor="@color/white"/>
</LinearLayout>
  1. 以下是我们 test.xml XML 布局的示例,它周围有 10dp 的空间:

  1. 现在,你需要尝试将你刚刚创建的布局包含在你的 DSL 布局中。你可以在活动的 onCreate() 方法中或在一个实现 AnkoComponent 接口的外部类中添加 DSL 布局。查看以下 DSL 布局的代码(注意给定代码中的粗体文本):
verticalLayout {
    padding = dip(20)
    val name = editText {
        id = R.id.name
        hint = "What is your name?"
    }

    val message = editText {
        id = R.id.message
        hint = "Your message"
    }

    button("Send") {
        id = R.id.btn_send
        onClick {
            toast("Hello there we have recorded your message!")
        }
    }

    include<View>(R.layout.test) {
 backgroundColor = Color.CYAN
 }.lparams(width = matchParent) { }
}

这是我们将 test.xml 包含到我们的 DSL 中后的布局外观:

  1. 我们可以通过使用 Kotlin 的合成属性、Anko 的 find() 方法或 findViewById() 来附加监听器并获取/设置包含视图的属性。上述每种方法都需要视图有一个 ID。查看以下代码,为具有 btn_test ID 的 test.xml 中的按钮附加点击监听器:
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        MainActivityUI().setContentView(this)
        btn_test.onClick {
 toast("test click")
 }
    }
  1. 我已经通过导入 test.xml 的合成属性导入了所有视图,如下所示:
import kotlinx.android.synthetic.main.test.*

将 XML 文件转换为 DSL

如果你已经是 Anko 的粉丝,并且想要将旧项目中的 XML 转换为 DSL 而不手动操作,那么这个菜谱将帮助你学习如何进行这项操作。

准备工作

我将使用 Android Studio 3 来编写代码。你可以通过在 Android Studio 3+ 中创建一个新的 Kotlin 项目并添加一个空白活动来开始,因为我们不会使用其他菜谱中的任何代码。你还需要对 Android 开发有一个中级理解。确保你已经将 Anko 布局依赖项添加到你的项目中(遵循本章中的菜谱 在 gradle 中设置 Anko 库以用于 Anko 布局)。

如何操作…

让我们从创建一个空白活动并开始工作于 XML 布局开始,以便有一个可以转换为 DSL 的东西。我将以下 XML 布局转换为 DSL:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 

    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="android.my_company.com.helloworldapp.Main2Activity">

    <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>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center">

        <TextView
            android:id="@+id/text1"
            android:text="@string/hello_calendar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/dp10"
            style="@style/TextAppearance.AppCompat.Title"/>

        <CalendarView
            android:id="@+id/calendarView"
            android:layout_width="match_parent"
            android:layout_height="180dp"
            android:layout_margin="@dimen/dp10"/>

        <Button
            android:id="@+id/btn_done"
            android:background="@color/colorAccent"
            android:text="@string/done"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/dp10"/>
    </LinearLayout>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        app:srcCompat="@android:drawable/ic_dialog_email" />

</android.support.design.widget.CoordinatorLayout>

显示 Snackbar

Snackbars 是向用户显示反馈和消息的绝佳方式。Snackbars 在移动设备的底部或在大设备上的左下角显示消息。它们还可以有一个操作按钮。它们在超时后、用户交互后或用户在 snackbar 上滑动后自动消失。

在这个菜谱中,我们将学习如何使用 Anko 布局轻松地显示 Snackbar。使用传统方式显示 Snackbar 有点长;Anko 使其变得简单,可以快速显示 snackbars。让我们看看如何操作。

准备工作

我将使用 Android Studio 3 来编写代码。你可以通过在 Android Studio 3+ 中创建一个新的 Kotlin 项目并添加一个空白活动来开始,因为我们不会使用其他菜谱中的任何代码。你还需要对 Android 开发有一个中级理解。确保你已经将 Anko 布局依赖项添加到你的项目中(遵循本章中的菜谱 在 gradle 中设置 Anko 库以用于 Anko 布局)。

如何操作…

在以下步骤中,我们将学习如何使用 Anko 库显示 snackbar:

  1. 让我们创建几个按钮,每个按钮对应不同的 snackbars。我们将在每个按钮的 onClick 监听器内部创建一个 snackbar。以下是某些 snackbars 的语法。我建议你在查看解决方案之前先自己尝试编写代码:
snackbar(parentView, "feedback message")
snackbar(parentView, R.string.message_string)
longSnackbar(parentView, "longer message")
snackbar(parentView, "message for action snackbbar", "Action name") { doSomething() }
  1. 查看一个可能的解决方案:
verticalLayout {
    id = R.id.rootView
    padding = dip(20)
    button("Simple Snackbar") {
        id = R.id.btn_snack1
        onClick {
            snackbar(rootView, "Hey! I'm a simple snackbar.")
        }
    }

    button("Simple Snackbar using resources") {
        id = R.id.btn_snack2
        onClick {
            snackbar(rootView, R.string.snack_message)
        }
    }

    button("Long Snackbar") {
        id = R.id.btn_snack3
        onClick {
            longSnackbar(rootView, R.string.snack_message)
        }
    }

    button("Action Snackbar") {
        id = R.id.btn_snack3
        onClick {
            longSnackbar(rootView, "Simple action snackbar rocks.",             "Action")
            {
                toast("Let us do some stuff!")
            }
        }
    }
}

这就是布局的外观:

以下截图显示了没有操作按钮的 snackbar:

这是带有操作按钮的一个:

显示 Toasts

Toasts 用于在 Android 中以弹出窗口的形式显示反馈或消息。Toast 在超时后自动消失。在 Anko 中显示 Toasts 非常简单。让我们看看如何操作。

开始

我将使用 Android Studio 3 来编写代码。你可以通过在 Android Studio 3+ 中创建一个新的 Kotlin 项目并添加一个空白活动来开始,因为我们不会使用其他菜谱中的任何代码。你还需要对 Android 开发有一个中级理解。确保你已经将 Anko 布局依赖项添加到你的项目中(遵循本章中的菜谱 在 gradle 中设置 Anko 库以用于 Anko 布局)。

如何操作…

让我们在布局中创建一些按钮,点击它们将显示一个托盘:

  • 这是使用 Anko 的托盘语法:
toast("a toast message")
toast(R.string.message_string)
longToast("a long duration toast message")

我建议你在继续到解决方案之前,先尝试自己在一个按钮点击时显示托盘。让我们使用前面的语法创建一个包含三个按钮的布局,点击这些按钮将显示托盘。

以下是在三个按钮中创建布局的一种方法,我们在按钮的 onClick 监听器中放置了显示托盘的代码。你也可以将布局放在一个实现 AnkoComponent 接口的外部类中:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        verticalLayout {
            id = R.id.rootView
            padding = dip(20)

            button("Show toast") {
                id = R.id.btn_snack1
                onClick {
                    toast( "Hey! Here is a toast for you.")
                }
            }

            button("Show toast using resource") {
                id = R.id.btn_snack2
                onClick {
                    toast(R.string.toast_message)
                }
            }

            button("Show long toast") {
                id = R.id.btn_snack3
                onClick {
                    longToast(R.string.toast_message)
                }
            }
        }
    }

以下是我们布局的外观,以及点击按钮时托盘的显示方式:

   

使用合成属性访问视图

因此,我们知道 Anko 如何使处理视图和布局变得简单,但 Kotlin 使得访问视图以及获取/设置视图属性变得非常有趣。如果你尝试过使用 findViewById(),你已经知道这是一段多么容易出错的笨拙代码。现在有很多库提供了解决方案,但 Kotlin 为此问题提供了一个内置插件。让我们了解如何使用它。

准备中

我将使用 Android Studio 3 来编写代码。你可以在 Android Studio 3+ 中创建一个新的 Kotlin 项目,并添加一个空白活动来开始,因为我们不会使用其他菜谱中的任何代码。你还需要对 Android 开发有一个中级理解。确保你已经将 Anko 布局依赖项添加到你的项目中(遵循本章中的菜谱 在 gradle 中设置 Anko 库用于 Anko 布局)。

如何实现...

在以下步骤中,我们将学习如何使用合成属性访问视图:

  1. 让我们从 XML 布局和一个使用此 XML 布局的 Activity 开始。从创建一个空白 Activity 开始,并创建你希望工作的 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="android.my_company.com.helloworldapp.HelloWorldActivity">

    <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>

    <LinearLayout

        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:background="@color/white"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <EditText
            android:id="@+id/name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="What is your name?"/>

        <EditText
            android:id="@+id/message"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Your message"/>

        <Button
            android:id="@+id/btn_send"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Send"/>

    </LinearLayout>

</android.support.design.widget.CoordinatorLayout>
  1. 要使用视图的合成属性,我们需要在活动中导入它们,如下所示:
import kotlinx.android.synthetic.main.xml_layout_name.*
  1. 以下是我们如何直接使用视图 ID 提供对视图的引用并获取/设置视图属性的方法:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main2)
    setSupportActionBar(toolbar)

    btn_send.onClick {
        toast("Hey there ${name.text}. We have recorded your message.")
    }
}

这就是我们的布局外观和工作方式:

使用扩展函数访问视图组的视图

我们可以使用扩展函数向一个我们可能甚至无法访问的类添加新行为。我们还可以向视图组添加扩展函数。其中一个这样的视图组是回收视图。让我们看看我们如何使用扩展函数访问回收视图的视图。

准备中

我将使用 Android Studio 3 来编写代码。你可以在 Android Studio 3+ 中创建一个新的 Kotlin 项目,并添加一个空白活动来开始,因为我们不会使用其他菜谱中的任何代码。你还需要对 Android 开发有一个中级理解。

如何实现...

Kotlin 有一些我们可以用于类的运算符。我们将重载这些运算符之一以获取视图组的视图:

  1. 我们可以通过重载get运算符来访问视图组的视图,如下所示:
operator fun ViewGroup.get(position: Int): View
{
    return getChildAt(position)
}
  1. 现在,为了从视图组获取视图,我们可以使用以下任一方法:
val view = viewContainer.get(2)
// where 2 is the position for the view we want to access
  1. 或者,使用以下方法,因为我们使用了运算符重载,el.get(index)与类似数组的el[index]操作匹配:
val view = viewContainer[2]
// where 2 is the position for the view we want to access

它是如何工作的...

扩展函数提供了在不修改类、继承它或使用任何设计模式的情况下向类添加新功能的能力。扩展函数是静态解析的,与它们扩展的类没有关联。

通过运算符重载,Kotlin 为我们提供了提供预定义运算集实现的能力。要重载运算符,我们可以使用成员函数或扩展函数,正如我们在前面的例子中所使用的。

第十章:数据库和依赖注入

本章将涵盖以下内容:

  • 在 Kotlin 中使用 SQLite 数据库

  • 创建数据库表

  • 在 Kotlin 中注入依赖

  • 从数据库读取数据

  • 将数据库游标转换为对象列表

  • 使用 parseOpt 处理可空对象

  • 向数据库中插入数据

  • 在 Kotlin 中创建单例

  • 在 Kotlin 中使用 Dagger2

  • 在 Kotlin 中使用 Butterknife

简介

当我们开发应用程序时,我们应该考虑到应用程序可能无法连接到互联网的情况。用户可能在电梯里,或者在他们尝试使用应用程序时可能没有网络覆盖。为了提供良好的用户体验,我们需要确保应用程序的一些部分在没有网络连接的情况下也能工作。为了能够做到这一点,我们需要在应用程序中有一个持久存储机制。这可以通过使用共享首选项或使用数据库来实现。当我们需要存储少量数据,如应用程序的设置值时,共享首选项会很有用。数据库在需要存储结构化数据的情况下功能更强大。在本章中,我们将学习如何使用 Android 内置的数据库 SQLite,还将学习使用 Dagger2 进行依赖注入,这被认为是开发优质应用程序的最佳实践之一。

在 Kotlin 中使用 SQLite 数据库

SQLite 是一个关系型数据库。Android 自带内置的 SQLite 数据库。它是一个开源的 SQL 数据库,在 Android 应用程序中得到广泛应用。然而,以原始方式操作非常耗时,消耗了大量的开发和测试时间。你必须与游标一起工作,逐行迭代它们,并将代码包裹在try-finally中,等等。当然,你可以使用提供 ORM 映射的库,这使得处理 SQLite 数据库更容易,但如果数据库很小,这很昂贵,并且通常是过度设计。Kotlin,结合 Anko,提供了一个处理 SQLite 数据库的简单方法。那么,让我们开始工作,看看我们如何在 Kotlin 中使用 SQLite 数据库。

准备工作

我们将使用 Android Studio 3.0 进行编码。首先,我们需要将anko-sqlite添加到我们的build.gradle文件中:

dependencies {
    compile "org.jetbrains.anko:anko-sqlite:$anko_version"
}

您可以将$anko_version替换为库的最新版本。

如何实现...

Anko 为我们内置的 SQLite API 提供了一个包装器,这有助于消除大量的样板代码,并增加了诸如在代码执行完成后关闭数据库等安全机制。

在实现 SQLite 数据库时,第一步是创建数据库辅助类。在这种情况下,我们需要这个类扩展ManagedSQLiteOpenHelper类,而不是我们之前使用的SQLiteOpenHelper类。ManagedSQLiteOpenHelper是并发感知的,并在查询执行结束时关闭数据库。

查看以下代码,这是一个简单的数据库辅助工具,我将在示例中使用它:

class DatabaseHelper(ctx: Context) : ManagedSQLiteOpenHelper(ctx, "SupportDatabase", null, 1) {
    companion object {
        private var instance: DatabaseHelper? = null

        @Synchronized
        fun getInstance(context: Context): DatabaseHelper {
            if (instance == null) {
                instance = DatabaseHelper(context.applicationContext)
            }
            return instance!!
        }
    }

    override fun onCreate(db: SQLiteDatabase) {
        db.createTable("Requests", true,
                "id" to INTEGER + PRIMARY_KEY + UNIQUE,
                "name" to TEXT,
                "message" to TEXT)
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        db.dropTable("Requests", true)
    }
}

所以基本上,在onCreate中,我们创建表,在onUpgrade中,我们升级表。

我在我的数据库中创建了一个单独的表,名为Requests。在Requests表中,我们拥有namemessageid字段作为主键。

我们可以通过将其添加为上下文的扩展属性来提供对数据库的访问。这允许任何需要上下文的类访问数据库。以下代码将数据库添加为上下文的扩展属性:

// Access property for Context
val Context.database: DatabaseHelper
    get() = DatabaseHelper.getInstance(getApplicationContext())

我将前面的代码添加到了与数据库助手相同的文件中,在类外部。

现在,这是我的活动代码,其中包含姓名和消息字段,在按下 Enter 按钮时,详细信息将存储在数据库中:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        MainActivityUI().setContentView(this)
        btn_send.onClick {
 database.use {
 insert("Requests",
 "id" to 1,
 "name" to name.text.toString(),
 "message" to message.text.toString())
 }
 }
    }

    class MainActivityUI : AnkoComponent<MainActivity> {
        override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
            verticalLayout {
                gravity = Gravity.CENTER
                padding = dip(20)

                textView {
                    gravity = Gravity.CENTER
                    text = "Enter your request"
                    textColor = Color.BLACK
                    textSize = 24f
                }.lparams(width = matchParent) {
                    margin = dip(20)
                }

                val name = editText {
                    id = R.id.name
                    hint = "What is your name?"
                }

                editText {
                    id = R.id.message
                    hint = "What is your message?"
                    lines = 3
                }

                button("Enter") {
                    id = R.id.btn_send
                }
            }
        }
    }
}

注意粗体代码。基本上,我们可以在 use 块内部对数据库执行操作。数据库将在 use 块开始时打开,并在执行后关闭。

以下是我们布局的截图:

现在尝试将一些内容放入数据库。下面是我的数据库截图,插入操作已成功执行:

我正在使用 Stetho (github.com/facebook/stetho)在 Chrome 开发者工具中查看数据库。

对于活动的布局,我使用了 Anko DSL 布局。您可以参考本书的第九章Anko Layouts,以了解更多信息。

创建数据库表

现在您已经学会了如何将 anko-sqlite 依赖项添加到您的项目中,以及如何在第一个菜谱中使用 SQLite 数据库,下一步是学习如何创建数据库表。

准备工作

我们将使用 Android Studio 3 进行编码。确保您已将 anko-sqlite 添加到您的build.gradle文件中,并完成了关于如何使用 SQLite 数据库的第一个菜谱。

如何操作…

我们将创建两个表:Requestscustomers

  1. 对于Requests表,我们有namemessage字段,我们可以在数据库助手的onCreate方法中直接创建它们,如下所示:
db.createTable("Requests", true,
    "id" to INTEGER + PRIMARY_KEY + UNIQUE,
    "name" to TEXT,
    "message" to TEXT)
  1. 对于customers表,我们将通过创建数据类并使用它来定义customers表的列来采用更好的编码实践。

    这里提供了我们的Customer数据类的代码:

data class Customer(val id: Int, val name: String, val phone_num: String) {
    companion object {
        val COLUMN_ID = "id"
        val TABLE_NAME = "customers"
        val COLUMN_NAME = "name"
        val COLUMN_PHONE_NUM = "phone_num"
    }
}
  1. 现在,我们将使用这个数据类来创建我们的表,如下所示:
db.createTable(Customer.TABLE_NAME,
        true,
        Customer.COLUMN_ID to INTEGER + PRIMARY_KEY,
        Customer.COLUMN_NAME to TEXT,
        Customer.COLUMN_PHONE_NUM to TEXT)
  1. 以下是在为 drop tables 填充代码后,我们的数据库助手最终的样子:
class DatabaseHelper(ctx: Context) : ManagedSQLiteOpenHelper(ctx, "SupportDatabase", null, 1) {
    companion object {
        private var instance: DatabaseHelper? = null

        @Synchronized
        fun getInstance(context: Context): DatabaseHelper {
            if (instance == null) {
                instance = DatabaseHelper(context.applicationContext)
            }
            return instance!!
        }
    }

    override fun onCreate(db: SQLiteDatabase) {
        db.createTable("Requests", true,
 "id" to INTEGER + PRIMARY_KEY + UNIQUE,
 "name" to TEXT,
 "message" to TEXT)

 db.createTable(Customer.TABLE_NAME,
 true,
 Customer.COLUMN_ID to INTEGER + PRIMARY_KEY,
 Customer.COLUMN_NAME to TEXT,
 Customer.COLUMN_PHONE_NUM to TEXT)
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        db.dropTable("Requests", true)
        db.dropTable(Customer.TABLE_NAME, true)
    }
}

// Access property for Context
val Context.database: DatabaseHelper
    get() = DatabaseHelper.getInstance(getApplicationContext())
  1. 现在,让我们重新安装我们的应用程序,看看是否在我们的数据库中形成了两个表。以下是我们数据库截图的样式(使用 Stetho),并且我们的表已成功创建:

Kotlin 中的依赖注入

在 Android 开发中,Dagger 2 是最受欢迎的依赖注入框架。你定义依赖对象,然后借助 Dagger 组件,将其注入到你想要的位置。在本食谱中,我们将了解如何注入依赖。我们不会深入探讨如何详细使用 Dagger 2;关于这一点,你可以参考本章中的“使用 Kotlin 与 Dagger2”食谱。

准备工作

我们将使用 Android Studio 3.0 进行本食谱。请确保你有其最新版本。

如何做…

当你在模块类中定义了所有需要的依赖对象后,你可以获取组件。让我们看看以下步骤:

  1. 要注入对象,你只需在变量前添加@Inject注解,然后对象就会被注入到那里。让我们看看以下示例:
@Inject
lateinit var mPresenter:AddActivityMvpPresenter

我们还使用了lateinit修饰符来避免在使用变量之前进行空检查。

  1. 另一种方法是构造函数注入。为了理解它,让我们看看以下代码:
@Module
class AddActivityModule {
  @Provides @ControllerScope
  fun providesAddActivityPresenter(addActivityPresenter: AddActivityPresenter):AddActivityMvpPresenter =addActivityPresenter
}
  1. 如你所见,我们在providesAddActivityPresenter中发送了AddActivityPresenter,但模块没有提供它。除非你按照以下方式提供AddActivityPresnter,否则这通常不会起作用:
class AddActivityPresenter @Inject constructor(var mDataManager:DataManager):AddActivityMvpPresenter

它是如何工作的…

当你在构造函数中使用@Inject注解时,这意味着在创建类之前,它需要DataManager对象。Dagger2 将检查依赖树,并在可能的情况下提供依赖。

从数据库读取数据

现在我们已经看到了如何创建数据库和如何创建表,让我们学习如何从数据库中读取。

准备工作

我将使用 Android Studio 3 来编写代码。你可以通过将 anko-sqlite 依赖项添加到你的项目中,并通过实现本章中的“使用 Kotlin 与 SQLite 数据库”食谱来创建一个包含Requests表的 SQLite 数据库来开始。通过使用本食谱中创建的表单,向你的Requests表添加一些数据。

如何做…

让我们看看以下步骤,以了解如何从数据库中读取数据:

  1. 现在,让我们将一个按钮添加到第一个食谱中现有的布局中;点击时,它应该从我们的Requests表中检索所有数据。查看以下更新后的代码,其中我添加了一个带有点击监听器的按钮:
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        MainActivityUI().setContentView(this)
        val btn_send = find<Button>(R.id.btn_send)
        btn_send.onClick {
            database.use {
                insert("Requests",
                        "name" to name.text.toString(),
                        "message" to message.text.toString())
            }
            toast("success")
            name.text.clear()
            message.text.clear()
        }
        val btn_read = find<Button>(R.id.btn_read)
        btn_read.onClick {
            var reqs = database.use {
                select("Requests").parseList(classParser<Request>())
 }
            for(x in reqs) {
 logd(x.name + ": " + x.message)
 }
        }
    }

    private fun logd(s: String) {
        Log.d("request", s)
    }

    class MainActivityUI : AnkoComponent<MainActivity> {
        override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
            verticalLayout {
                padding = dip(20)

                textView {
                    gravity = Gravity.CENTER
                    text = "Enter your request"
                    textColor = Color.BLACK
                    textSize = 24f
                }.lparams(width = matchParent) {
                    margin = dip(20)
                }

                val name = editText {
                    id = R.id.name
                    hint = "What is your name?"
                }

                editText {
                    id = R.id.message
                    hint = "What is your message?"
                    lines = 3
                }

                button("Enter") {
                    id = R.id.btn_send
                }

                button("Show me requests") {
                    id = R.id.btn_read
                }
            }
        }
    }

    class Request(val id: Int, val name: String, val message: String)

}
  1. 我正在使用 Anko DSL 来创建我的活动的布局。正如我们在之前的食谱中讨论的那样,我们在database.use{...}块内执行所有数据库操作。要从数据库中读取数据,我们使用select函数。语法如下:
db.select(tableName, vararg columns) // where db is an instance of the SQLiteDatabase
  1. database.use {...}内部,this是数据库实例,因此我们可以直接使用selectinsert等方法。以下是在数据库表中的数据和输出:

这是数据:

这是输出:

11-18 18:21:34.709 12523-12523/android.my_company.com.helloworldapp D/request: name 1: request 1
11-18 18:21:34.709 12523-12523/android.my_company.com.helloworldapp D/request: name 2: request 2
11-18 18:21:34.709 12523-12523/android.my_company.com.helloworldapp D/request: name 3 : request 3
  1. 查询构建器还有很多我们可以做的事情;以下是 Anko 提供的方法列表:

    • column(String): 用于将列添加到我们的 select 查询中

    • distinct(Boolean): 用于在查询中添加 distinct

    • whereArgs(String): 用于指定原始 where 字符串

    • whereArgs(String, args): 用于指定 where 查询及其对应的参数

    • whereSimple(String, args): 用于指定带有 ? 标记的 where 查询及其对应的参数

    • orderBy(String, [ASC/DESC]):用于指定用于排序的列

    • groupBy(String): 用于指定用于分组的列

    • limit(count: Int): 用于限制查询返回的行数

    • limit(offset: Int, count: Int): 用于在 offset 之后限制查询返回的行数

    • having(String): 用于指定原始 having 表达式

    • having(String, args): 用于指定带有参数的原始 having 表达式

  2. 让我们尝试另一个例子。在这个例子中,我们将使用 where 子句从数据库中选择数据:

select("Requests")
    .whereArgs("(id > {userId})",
        "userId" to 1)

这是上一个查询的输出:

11-18 21:11:04.328 18149-18149/android.my_company.com.helloworldapp D/request: name 2: request 2
11-18 21:11:04.329 18149-18149/android.my_company.com.helloworldapp D/request: name 3 : request 3
  1. 在获取查询结果后,我们还需要解析结果。我们从查询中获取一个游标,并使用 Anko 提供的方法,我们可以轻松地将它们解析到常规类中。在上一个例子中,我们创建了一个名为 Request 的类:
class Request(val id: Int, val name: String, val message: String)
  1. 该类包含我们可能从查询结果游标中获取的所有字段。以下是我们用于解析结果的方法:

    • parseSingle(rowParser): T:解析正好且仅一行;如果游标中有多于一行,则抛出异常

    • parseOpt(rowParser): T?:解析零行或一行,但如果游标中有多于一行,则抛出异常

    • parseList(rowParser): List<T>:解析零行或多行

我们在上一个例子中使用了 parseList。你可以传递行解析器或映射解析器,你也可以使用你自定义类的 classParser,它传递一个行解析器,如下所示:

val rowParser = classParser<Person>()

将数据库游标转换为对象列表

在上一个菜谱中,我们学习了如何从数据库表中查询数据。查询的结果是一个游标。在这个菜谱中,我们将学习如何使用 parseList 将游标转换为对象的列表。

准备工作

我将使用 Android Studio 3 来编写代码。你可以通过向项目中添加 anko-sqlite 依赖项并创建一个类似于我们在 在 Kotlin 中使用 SQLite 数据库 菜谱中做的数据库辅助类来开始。

如何做到这一点...

按照以下步骤将游标转换为对象列表:

  1. 让我们从创建一个 Customer 类开始,作为我们的 customers 表的模型:
data class Customer(val id: Int, val name: String, val phone_num: String) {
    companion object {
        val COLUMN_ID = "id"
        val TABLE_NAME = "customers"
        val COLUMN_NAME = "name"
        val COLUMN_PHONE_NUM = "phone_num"
    }
}
  1. 现在,我们将编写代码在数据库辅助类中创建 customers 表。查看以下代码:
class DatabaseHelper(ctx: Context) : ManagedSQLiteOpenHelper(ctx, "SupportDatabase", null, 1) {
    companion object {
        private var instance: DatabaseHelper? = null

        @Synchronized
        fun getInstance(context: Context): DatabaseHelper {
            if (instance == null) {
                instance = DatabaseHelper(context.applicationContext)
            }
            return instance!!
        }
    }

    override fun onCreate(db: SQLiteDatabase) {
        db.createTable(Customer.TABLE_NAME,
                true,
                Customer.COLUMN_ID to INTEGER + PRIMARY_KEY,
                Customer.COLUMN_NAME to TEXT,
                Customer.COLUMN_PHONE_NUM to TEXT)
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        db.dropTable(Customer.TABLE_NAME, true)
    }
}

// Access property for Context
val Context.database: DatabaseHelper
    get() = DatabaseHelper.getInstance(getApplicationContext())
  1. 现在,我们将创建一个表单来输入客户,并使用select函数显示数据库表中的所有客户。我们将使用parseList方法获取结果游标中的行作为List。我们需要在parseList方法中传递一个行解析器或映射解析器。这样做最简单的方法是使用 Anko 提供的classParser,并使用我们的Customer类构造函数来获取行解析器,如下所示:
var customers = database.use {
    select(Customer.TABLE_NAME)
    .parseList(classParser<Customer>())
}

我建议你在查看解决方案之前先自己尝试这个练习。

下面是我的包含 DSL 布局的活动版本:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        MainActivityUI().setContentView(this)
        val name = find<EditText>(R.id.name)
        val phone = find<EditText>(R.id.phone)
        btn_send.onClick {
            database.use {
                insert(Customer.TABLE_NAME,
                        Customer.COLUMN_NAME to name.text.toString(),
                        Customer.COLUMN_PHONE_NUM to phone.text.toString())
            }
            toast("success")
            name.text.clear()
            phone.text.clear()
        }
        val btn_read = find<Button>(R.id.btn_read)
        btn_read.onClick {
            var customers = database.use {
                select(Customer.TABLE_NAME)
                        .parseList(classParser<Customer>())
 }
            // customers is the list of objects which we can now iterate on to get individual values as objects of Customer class
            for(c in customers) {
 debug(c.name + " (" + c.phone_num + ")")
 }
        }
    }

    private fun debug(s: String) {
        Log.d("customer", s)
    }

    class MainActivityUI : AnkoComponent<MainActivity> {
        override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
            verticalLayout {
                padding = dip(20)

                textView {
                    gravity = Gravity.CENTER
                    text = "Enter the customer"
                    textColor = Color.BLACK
                    textSize = 24f
                }.lparams(width = matchParent) {
                    margin = dip(20)
                }

                val name = editText {
                    id = R.id.name
                    hint = "Name"
                }

                editText {
                    id = R.id.phone
                    hint = "Phone no."
                }

                button("Enter") {
                    id = R.id.btn_send
                }

                button("Show me customers") {
                    id = R.id.btn_read
                }

                button("Delete all customers") {
                    id = R.id.btn_delete
                }
            }
        }
    }
}

查询结果,即customers,是我们现在可以迭代的对象列表,以获取Customer类的单个行对象。

使用parseOpt处理可空对象

当我们在游标中获取多行时,我们使用parseList,但当只获取一行时,我们使用parseSingleparseOpt。然而,parseSingleparseOpt之间有什么区别呢?在本教程中,我们将了解两者的区别以及何时使用哪一个。

准备工作

我将使用 Android Studio 3 来编写代码。你可以通过将anko-sqlite依赖项添加到你的项目中并创建一个数据库助手来开始,就像我们在*在 Kotlin 中使用 SQLite 数据库*教程中所做的那样。你需要阅读并实现上一个教程,以便能够跟随本教程。

如何操作…

如果你已经阅读并实现了上一个教程,那么你数据库中必须已经有一个customers表。按照提到的步骤来了解parseSingleparseOpt之间的区别:

  1. 在上一个教程中,我们使用parseList获取行列表作为对象。如果我们只需要获取一行作为对象,那么我们需要使用parseSingle。以下为parseSingle的语法:
parseSingle(rowParser): T
  1. 现在我们以以下方式在我们的前一个代码中使用它:
btn_read.onClick {
    var c = database.use {
        select(Customer.TABLE_NAME)
            .whereArgs("(id = {userId})",
            "userId" to 1)
 .parseSingle(classParser<Customer>())
    }
    debug(c.name + " (" + c.phone_num + ")")
}
  1. 我们使用parseSingle是因为我们将在游标中只得到一行,但如果从游标中获取零行,即我们得到一个空游标,那么我们会得到一个异常:
android.database.sqlite.SQLiteException: parseSingle accepts only cursors with a single entry

然而,如果我们期望得到一个单行游标,但有可能得到一个空游标,会发生什么?它总是会抛出异常,即当我们使用parseOpt时;parseOpt接受零行或一行游标。此外,如果parseOpt得到一个 null 对象,它会相应地处理场景,为每个列提供null值。基本上,parseOpt用于可能为空的游标和可能为null的对象。

parseOpt的语法如下:

parseOpt(rowParser): T? // ?表示返回的对象可能是可空的。

下面是如何在我们的代码中使用它的示例:

btn_read.onClick {
    var c = database.use {
        select(Customer.TABLE_NAME)
            .whereArgs("(id = {userId})",
            "userId" to 1)
 .parseOpt(classParser<Customer>())
    }
    debug(c?.name + " (" + c?.phone_num + ")")
}

现在即使返回的游标为空,我们也不会得到异常,并且输出为null值。

如果表为空,则输出如下:

11-18 21:11:04.329 18149-18149/android.my_company.com.helloworldapp D/customer: null (null)

将数据插入数据库

使用 Anko SQLite 将数据插入数据库就像做蛋糕一样简单。在本教程中,我们将学习如何做到这一点。

准备工作

我将使用 Android Studio 3 来编写代码。你可以通过在你的build.gradle文件中添加以下行来将anko-sqlite依赖项添加到你的项目中开始:

dependencies {
    compile "org.jetbrains.anko:anko-sqlite:$anko_version"
}

你可以将$anko_version替换为库的最新版本。

如何做到这一点...

按照以下步骤将数据插入我们的数据库:

  1. 让我们从我们的数据库助手开始,我们将创建一个包含namemessageid字段的Requests表,如下所示:
class DatabaseHelper(ctx: Context) : ManagedSQLiteOpenHelper(ctx, "SupportDatabase", null, 1) {
    companion object {
        private var instance: DatabaseHelper? = null

        @Synchronized
        fun getInstance(context: Context): DatabaseHelper {
            if (instance == null) {
                instance = DatabaseHelper(context.applicationContext)
            }
            return instance!!
        }
    }

    override fun onCreate(db: SQLiteDatabase) {
        db.createTable("Requests", true,
                "id" to INTEGER + PRIMARY_KEY + UNIQUE + AUTOINCREMENT,
                "name" to TEXT,
                "message" to TEXT)
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        db.dropTable("Requests", true)
    }
}

// Access property for Context
val Context.database: DatabaseHelper
    get() = DatabaseHelper.getInstance(getApplicationContext())
  1. 现在,让我们创建一个活动,其中包含一个表单,用于接收姓名和消息并将其存储在数据库中。我正在使用 Anko DSL 布局来布局活动:
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        MainActivityUI().setContentView(this)
        btn_send.onClick {
            database.use {
 insert("Requests",
 "name" to name.text.toString(),
 "message" to message.text.toString())
 }
            toast("success")
            name.text.clear()
            message.text.clear()
        }
    }

    class MainActivityUI : AnkoComponent<MainActivity> {
        override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
            verticalLayout {
                padding = dip(20)

                textView {
                    gravity = Gravity.CENTER
                    text = "Enter your request"
                    textColor = Color.BLACK
                    textSize = 24f
                }.lparams(width = matchParent) {
                    margin = dip(20)
                }

                val name = editText {
                    id = R.id.name
                    hint = "What is your name?"
                }

                editText {
                    id = R.id.message
                    hint = "What is your message?"
                    lines = 3
                }

                button("Enter") {
                    id = R.id.btn_send
                }
            }
        }
    }
}
  1. 注意前面代码片段中加粗的代码。我们将在database.use {...}块内执行所有操作,因为它具有并发安全性,并且在块执行后关闭数据库。如果你已经完成了创建数据库表的食谱,你会注意到表创建和插入相当相似。语法如下:
db.insert(TABLE_NAME, 
    COLUMN_NAME_1 to VALUE_1,
    COLUMN_NAME_2 to VALUE_2,
    COLUMN_NAME_3 to VALUE_3
)

这是我们的布局:

在输入数据时,我们可以检查我们的姓名和消息是否被存储在我们的数据库中。我正在使用 Stetho 在我的设备上查看数据库。

在 Kotlin 中创建单例

单例类是一个类,在任意时刻只能有一个该类的实例/对象。这个概念是为了限制对象的实例化数量。在本食谱中,我们将探索 Kotlin 中的单例。

准备工作

我将使用 Android Studio 3 来编写代码。

如何做到这一点...

按照以下步骤在 Kotlin 中创建单例:

  1. Kotlin 没有静态成员或变量,因此为了声明类的静态成员,我们使用companion object。查看以下示例:
class SomeClass {

    companion object {
        var intro = "I am some class. Pleased to meet you!"
        fun infoIntro(): String {
            return "I am some class. Pleased to meet you!"
        }
    }
}
  1. 访问前一个类的companion对象的成员和方法与访问任何静态成员或方法相同:
var x = SomeClass.intro
toast(SomeClass.infoIntro())
  1. 现在假设我们想要一个单例类,即每次只有一个对象/实例的类?做好准备,这个很有趣。以下是在几行代码内创建单例类的方法:
object SomeClass {

    var intro = "I am some class. Pleased to meet you!"
    fun infoIntro(): String {
        return "I am some class. Pleased to meet you!"
    }
}

此外,我们就像在前面示例中使用静态成员一样使用它:

var x = SomeClass.intro
toast(SomeClass.infoIntro())

它是如何工作的...

在 Kotlin 中,反编译字节码是了解幕后发生的事情的绝佳方法。如果我们反编译我们创建的对象的字节码,我们会得到以下代码,这表明在幕后,对象只是一个每次只有一个实例的类:

public final class SomeClass {
   @NotNull
   private static String intro;
   public static final SomeClass INSTANCE;

   @NotNull
   public final String getIntro() {
      return intro;
   }

   public final void setIntro(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      intro = var1;
   }

   @NotNull
   public final String infoIntro() {
      return "I am some class. Pleased to meet you!";
   }

   private SomeClass() {
      INSTANCE = (SomeClass)this;
      intro = "I am some class. Pleased to meet you!";
   }

   static {
      new SomeClass();
   }
}

使用 Kotlin 与 Dagger 2

Dagger 2 是 Android 社区中最好的依赖注入框架,也是开源的。它由 Google 支持,并且被广泛使用。依赖注入被认为是最佳实践,可以使你的代码库可扩展。在本食谱中,我们将学习如何使用 Dagger 2 在 Kotlin 中进行依赖注入。

准备工作

我们将使用 Android Studio 3.0 进行编码。首先,我们需要将 Dagger 2 包含到项目中,通过在build.gradle文件中添加以下行来实现:

compile "com.google.dagger:dagger:$daggerVersion"
kapt "com.google.dagger:dagger-compiler:$daggerVersion"

你需要将$daggerVersion替换为 Dagger2 的最新版本。

如何做到这一点...

在我们继续之前,我们需要了解 Dagger2 是如何工作的。Dagger2 使用注解生成代码,并使用它来访问字段;因此,它不能使用私有字段。

以下注解在 Dagger2 中使用:

  • @Module@Provides:定义提供依赖关系的类和方法

  • @Inject:请求依赖关系,可以在构造函数、字段或方法中使用

  • @Component:启用选定的模块,并用于执行依赖注入

注有@Module的类负责提供可注入的对象。提供这些对象的那些方法需要注有@Provides。如果方法需要另一个对象来创建依赖对象,它们将在方法参数中提供。Dagger2 创建一个依赖关系树并检查参数是否可以提供。让我们看看模块的实现:

  1. 我们将查看一个网络模块的示例,该模块将提供诸如HttpCacheHttpLoggingInterceptor、GSON 对象等对象:
@Module
class NetworkModule {
    @Provides @Singleton
    fun getHttpLoggingInterceptor():HttpLoggingInterceptor=
            HttpLoggingInterceptor().
                    setLevel(HttpLoggingInterceptor.Level.BODY)

    @Provides
    @Singleton
    fun provideHttpCache( @AppContext application: App): Cache {
        val cacheSize = 10 * 1024 * 1024
        val cache = Cache(application.cacheDir, cacheSize.toLong())
        return cache
    }

    @Provides
    @Singleton
    fun provideGson(): Gson {
        val gsonBuilder = GsonBuilder()
        gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
        return gsonBuilder.create()
    }

    @Provides
    @Singleton
    fun provideOkhttpClient(cache: Cache, httpLoggingInterceptor: HttpLoggingInterceptor): OkHttpClient =
            OkHttpClient.Builder().addInterceptor(httpLoggingInterceptor).cache(cache).build()

    @Provides @Singleton 
    fun getRetrofit(okHttpClient: OkHttpClient): Retrofit =                                             Retrofit.Builder().addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .addConverterFactory(GsonConverterFactory.create())
            .client(okHttpClient)
            .baseUrl(AppConstants.INSTAGRAM_BASE_URL)
            .build()

}

如你所见,我们注解了每个通过依赖注入提供对象的每个方法,我们使用了@Singleton注解,这意味着方法提供了一个单例对象。

你可能会注意到,我们使用了其他对象来创建可注入对象,并将它们作为参数提供。这些参数应从外部或其他注入对象中可用。

  1. 现在,让我们看看Dagger组件的示例:
@Component(dependencies = arrayOf(ApplicationComponent::class)
        , modules = arrayOf(AddActivityModule::class))
interface AddActivityComponent {
    fun inject(addActivity: AddActivity)
}

组件充当一个接口,告诉我们从哪些模块(或其他组件)满足依赖关系。在上面的示例中,我们创建了一个组件,它将为我们提供来自AddActivityModule和另一个ApplicationComponent组件的依赖对象。

我们还定义了一个注入方法,它接受一个参数(这里为AddActivity),告诉我们对象将被注入的位置。

  1. 一旦定义,我们就可以将其注入到我们的AddActivity中,如下所示:
class AddActivity : BaseActivity<AddActivityMvpView,AddActivityMvpPresenter>(),AddActivityMvpView {

    @Inject
    lateinit var mPresenter:AddActivityMvpPresenter
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_add)
        DaggerAddActivityComponent.builder()
                .applicationComponent(applicationComponent)
                .build()
                .inject(this)
    }
}

如你所见,我们使用我们的AddActivityComponent(现在以Dagger为前缀)来注入我们的AddActivity类。

此外,我们使用@Inject注解标记了我们的依赖对象,这意味着对象将被注入到这里。我们还添加了lateinit修饰符,以防止我们每次访问它时都进行空检查。添加@Inject注解意味着你想要一个对象,然后 Dagger 将检查其组件和依赖关系以提供该对象。

除了那个模块类之外,你还可以在构造函数级别实例化对象。让我们看看以下示例:

class AddActivityPresenter @Inject constructor(var mDataManager:DataManager)

在前面的示例中,将@Inject注解添加到构造函数中意味着在创建类之前,该类需要DataManager对象。Dagger 将检查其依赖关系树(在其组件中)并创建AddActivityPresenter,如果存在的话。

使用 Kotlin 的 Butterknife

Android 世界有许多需要注解处理的库。您只需注解代码,它就会在幕后为您生成所有代码,使您的生活更轻松。许多库,如 Butterknife 和 Dagger2,以类似的方式工作。在本教程中,我们将学习如何使用 Kotlin 的 Butterknife。对于那些不熟悉 Butterknife 的人来说,这是一个将视图绑定到字段而不需要findViewById调用的库。它是 Android 开发界的家喻户晓的名字。在 Kotlin 中,Kotlin Android Extension 几乎做同样的工作,并且与 Kotlin 捆绑在一起。然而,如果您正在迁移使用 Butterknife 的 Java 代码,这个教程将帮助您。

准备工作

我们将使用 Android Studio 3.0 进行编码。

如何做到这一点...

要将 Butterknife 包含到您的项目中,请遵循以下步骤:

  1. 首先,将以下行添加到您的build.gradle文件中;同时,您需要添加kotlin-kapt插件,并将annotationProcessor替换为kaptkaptannotationProcessor的 Java 等价物,因此无论您在哪里使用了annotationProcessor,都需要将其替换为kapt
apply plugin: 'kotlin-kapt'  
dependencies {  ...  
    compile "com.jakewharton:butterknife:$butterknife-version"  
    kapt "com.jakewharton:butterknife-compiler:$butterknife-version" }
  1. 在 Java 中,我们使用了 Butterknife 库,如下所示:
@BindView(R.id.headline) TextView headline;

在 Kotlin 中,我们可以这样做:

@BindView(R.id.headline) lateinit var headline: TextView

注意,我们已经使用了lateinit修饰符,这将使我们免于声明它为可空。我们还可以实现点击监听器,如下所示:

@OnClick(R.id.button) 
internal fun sayHello() {  
    Toast.makeText(this, "Hello, World!", LENGTH_SHORT).show() 
}

还有更多...

理解注解处理器的运作方式很重要。它们基本上充当编译器的钩子,用于分析源代码中定义的注解,并通过产生编译错误、警告或在其位置生成额外代码来处理它们。这使得编写应用程序更快,因为您只需注解,编译器就会在幕后为您生成所有必要的代码。Dagger 2 是一个流行的库,它以这种方式工作。

第十一章:网络和并发

本章将涵盖以下食谱:

  • 如何通过网络获取数据

  • 如何创建数据类

  • 如何修改后复制数据类

  • 如何从网络解析 JSON 数据到数据类

  • 如何在 Kotlin 中下载文件

  • 如何在 Kotlin 中使用 RxJava 和 Retrofit

  • 如何使用 RecyclerView 制作无限列表

  • 如何在 Android 中使用 Kotlin 运行后台任务

  • 如何使用协程实现多线程

简介

你可能会发现很难找到一个不通过网络进行通信的应用程序。在几乎所有的应用程序中,无论是文件共享应用程序、流媒体应用程序、社交网络应用程序还是其他什么,列表可以一直继续。当你想要向你的 Android 应用程序添加网络通信功能时,你需要考虑许多变量。例如,你不能在主线程上运行它,网络请求总是在后台线程上执行。除此之外,你还需要检测网络请求失败的情况,以便向用户反馈出了什么问题。在本章中,我们将讨论如何在 Kotlin 中高效地进行网络请求。

如何通过网络获取数据

在 Android 中进行网络请求非常繁琐,除非你使用任何第三方库。例如,让我们看看 Android 中的网络请求过去是如何的:

try {
    URL url = new URL("<api call>");

    urlConnection = (HttpURLConnection) url.openConnection();
    urlConnection.setRequestMethod("GET");
    urlConnection.connect();

    InputStream inputStream = urlConnection.getInputStream();
    StringBuffer buffer = new StringBuffer();
    if (inputStream == null) {
        // Nothing to do.
        return null;
    }
    reader = new BufferedReader(new InputStreamReader(inputStream));

    String line;
    while ((line = reader.readLine()) != null) {
        buffer.append(line + "\n");
    }

    if (buffer.length() == 0) {
        return null;
    }
    result = buffer.toString();
} catch (IOException e) {
    Log.e("Request", "Error ", e);
    return null;
} finally{
    if (urlConnection != null) {
        urlConnection.disconnect();
    }
    if (reader != null) {
        try {
            reader.close();
        } catch (final IOException e) {
            Log.e("Request", "Error closing stream", e);
        }
    }
}

当然,前面的代码很丑陋。Kotlin 简化了我们的痛苦,使我们能够进行网络请求。在本食谱中,我们将学习如何在 Kotlin 中进行网络请求。

准备工作

我们将使用 Android Studio 3.0。请确保你有其最新版本。

如何做到这一点…

让我们看看在 Kotlin 中进行网络请求所需的以下步骤:

  1. 记得我们在本食谱开始时看到的那些用于执行网络请求的大量代码?所有这些都可以用一行 Kotlin 代码来替换。让我们看看以下代码:
var response= URL("<url>").readText()

这将仅返回从网络请求中获取的 response。你只需提供你的 URL 作为参数。

当你在 Android 上使用此方法时,请确保你已经将此任务推送到后台,否则你将得到一个 NetworkOnMainThread 异常。

  1. 我们想要异步执行的代码被包装在 doAsync 块下。将代码包装在异步任务中也非常简单。让我们看看以下代码:
doAsync {
    val result= URL("https://api.instagram.com/319bad89407ffd7082").readText()
    uiThread {
        toast(result)
    }
}

我们已经将网络请求包装在一个异步块中,然后我们有一个 uiThread 方法,我们可以从中触摸应用程序的 UI 元素。

  1. uiThread 方法由 Anko 库提供,你可以通过在你的 build.gradle 文件中添加以下行将它们包含到你的项目中:
implementation "org.jetbrains.anko:anko:1.0"

还有更多…

查看本章的 如何使用 Anko 在 Android 中使用 Kotlin 运行后台任务 食谱,了解更多关于如何在 Kotlin 中创建后台任务的信息。

如何在 Kotlin 中创建数据类

你是否厌倦了仅仅为了存储数据而编写冗长的样板代码?你是否觉得以下代码只是为了定义一个Student模型而显得过于繁琐?:

public class Student {

    private String name;
    private String roll_number;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getRoll_number() {
        return roll_number;
    }

    public void setRoll_number(String roll_number) {
        this.roll_number = roll_number;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public int hashCode() {
        return super.hashCode();
    }

    @Override
    public String toString() {
        return super.toString();
    }
}

如果你同意,那么 Kotlin 的数据类正是你所需要的。所以让我们在这个菜谱中深入探讨,了解更多。

准备工作

我们将使用 IntelliJ IDEA 来编写我们的代码。你可以使用任何能够执行 Kotlin 代码的 IDE。

如何操作...

在每一个现实世界的项目中,你都会创建一些没有其他用途,仅仅用于存储数据的类,就像我们之前描述的Student类。在一个具有许多角色和模型的复杂项目中,这些类型的类的数量可能会非常高,这会导致大量的样板代码。Kotlin 为这个问题提供了一个很好的解决方案:

  1. 在菜谱开头提到的代码可以简化为仅仅一行:
data class Student(var name:String,var roll_number:String,var age:Int)

就这样!

  1. 现在,让我们尝试使用我们刚刚创建的数据类:
fun main(args: Array<String>) {
    val student=Student("Aanand","2013001",21)
    println("Student: name- ${student.name}, roll_number:${student.roll_number}, age:${student.age}")
}

//Output: Student: name- Aanand, roll_number:2013001, age:21

如你所见,我们不需要任何 getter setter,这节省了我们大量的样板代码。getter setters 已经包含在 Kotlin 属性中。

  1. 让我们检查toString()方法(我们甚至还没有定义):
println("${student.toString()}")

//Output: Student(name=Aanand, roll_number=2013001, age=21)

这比 Java 的toString()方法返回的结果要好。

  1. 数据类还提供了很多灵活性。例如,如果你不想要属性的 setter,你可以将属性设置为val。这将使属性为只读:
data class Student(val name:String,val roll_number:String,var age:Int)
  1. 你可以用数据类做的酷事情之一是你可以解构对象以获取属性。查看以下代码以了解更多信息:
fun main(args: Array<String>) {
    val student= Student("Aanand", "2013001", 21)
    val (name, roll_number,age)=student
    println("Student: name- $name, roll_number:$roll_number, age:$age")
}

//Output: Student: name- Aanand, roll_number:2013001, age:21
  1. 你也可以在类中为属性设置默认值。让我们看看下一个例子:
data class Student(val name:String="Aanand",val roll_number:String,var age:Int)
var studentA= Student(roll_number =  "2013001", age = 21)
println(studentA.toString())

//Output: Student(name=Aanand, roll_number=2013001, age=21)

还有更多...

数据类有一些限制。根据 Kotlin 文档,这些限制如下:

  • 主要构造函数需要至少有一个参数

  • 所有主要构造函数参数都需要标记为valvar

  • 数据类不能是抽象的、开放的、密封的或内部的

  • 数据类可能不能扩展其他类(但可以实现接口)

如何修改后复制数据类

在上一个菜谱中,我们学习了如何使用数据类以及它如何减少大量的样板代码。在这个菜谱中,我们将看到数据类如何使复制另一个数据类变得容易,即使你需要修改属性。

复制数据类的暴力机制可以通过复制所有属性来创建一个数据类,但使用copy方法会更容易。

准备工作

我们将使用 IntelliJ IDEA 来编写我们的代码。你可以使用任何能够执行 Kotlin 代码的 IDE。

如何操作...

我们将使用copy方法,该方法接受命名参数并创建一个具有更改的命名参数值的对象副本。让我们看看一个例子:

data class Student(val name:String,val roll_number:String,var age:Int)
fun main(args: Array<String>) {
    var studentA= Student("Aanand Roy", "2013001", 21)
    var olderStudentA=studentA.copy(age = 25)
    println(olderStudentA.toString())
}

//Output: Student(name=Aanand Roy, roll_number=2013001, age=25)

还有更多...

人们通常会在copy()apply()函数之间感到困惑:

  • apply(): 它接受一个函数并将其作用域设置为被调用的对象。它是一个转换函数,也可以用来在返回之前评估复杂逻辑。最后,它只是返回一个经过更改(如果进行了更改)的相同对象。

  • copy(): apply 函数不是线程安全的,并且会修改对象。另一方面,copy() 函数返回一个新的对象(不会修改原始对象)。

如何从网络解析 JSON 数据到数据类

JSON 是最广泛使用的响应格式之一。通常,APIs 以 JSON 响应的形式提供输出,在 Android 开发中,它们也被广泛使用,因为我们与网络进行通信。将 JSON 响应解析到数据类中可以帮助我们像 Java 对象一样处理它们。你也可以使用 JSONObject 来解析它,但结果会是代码很脏。在这个菜谱中,我们将学习如何将 JSON 数据解析到数据类中。我们使用数据类,因为当类的唯一目的是保存数据时,它们是首选的。那么,让我们开始吧!

准备工作

我们将使用 Android Studio 3.0;请确保你有其最新版本。我们将使用 GSON 库,这是谷歌开源的一个用于解析 JSON 响应的库。GSON 非常易于使用,并且是市面上最受欢迎的 JSON 解析库之一。要将 GSON 包含到你的项目中,只需将以下行添加到你的 build.gradle 文件中:

compile 'com.google.code.gson:gson:2.8.2'

如何做…

按照以下步骤了解如何从网络解析 JSON 数据:

  1. 通常,我们在发起网络请求后都会得到一个 JSON 响应,所以为了简化,我们将假设在发起一些网络请求后得到了给定的 JSON 响应:
{
 "data": [{
             "id": "17867282641151111",
             "from": {
                 "id": "1391934316",
                 "username": "aanandshekharroy",
                 "full_name": "Aanand Shekhar Roy",
                 "profile_picture": "https://scontent.cdninstagram.com/t51.2885-19/10475071_605790259527941_865730435_a.jpg"
                 },
             "text": "Testing api",
             "created_time": "1501571384"
         }, {
             "id": "17892289033060177",
             "from": {
                 "id": "1391934316",
                 "username": "aanandshekharroy",
                 "full_name": "Aanand Shekhar Roy",
                 "profile_picture": "https://scontent.cdninstagram.com/t51.2885-19/10475071_605790259527941_865730435_a.jpg"
 },
                 "text": "My second test",
                 "created_time": "1501571390"
             }],
         "meta": {
         "code": 200
     }
}

IntelliJ IDEA 提供了一个插件,可以帮助将 JSON 响应转换为 Kotlin 对象。我们将使用 RoboPojoGenerator 插件。执行以下步骤来安装它:

  1. 前往设置 | 插件:

  1. 点击 Install JetBrains plugin;它将打开一个对话框。在其中搜索 Robopojo,你将看到一个 RoboPOJOGenerator 插件。点击 Install 并重新启动 Android Studio。

  1. 完成这些后,为了根据 JSON 响应生成类,首先创建一个空的包,你将在这里保存这些类。我已经创建了一个名为 InstagramCommentsResponse 的包(因为我们使用了 Instagram API 来获取最新的评论)。

  2. 现在,右键单击包,选择 New | Generate POJO from JSON。然后,你将看到一个由 RoboPOJO 生成器提供的对话框,你需要将你的 JSON 响应粘贴进去。完成之后,勾选 Kotlin 和 Gson 复选框,然后点击 Generate。

  3. 现在,你将看到在该包内部创建了许多类,如图所示:

  1. 让我们看看这些类。第一个类是 JSON 响应的外部容器:
@Generated("com.robohorse.robopojogenerator")
data class Response(

   @field:SerializedName("data")
   val data: List<DataItem?>? = null,

   @field:SerializedName("meta")
   val meta: Meta? = null
)
// DataItem -  Class that will hold comments
@Generated("com.robohorse.robopojogenerator")
data class DataItem(

   @field:SerializedName("created_time")
   val createdTime: String? = null,

   @field:SerializedName("from")
   val from: From? = null,

   @field:SerializedName("id")
   val id: String? = null,

   @field:SerializedName("text")
   val text: String? = null
)
  1. 现在,让我们尝试解析从网络调用中接收到的 JSON。我们将尝试获取收到的第一个评论并像在 Kotlin 中那样访问它:
fun main(args:Array<String>){
    var response= URL("https://api.instagram.com/v1/media/1571595528561539504_5812999640/comments?access_token=5812999640.42ee6f0.9441d5bd909f40319bad89407ffd7082").readText()
    var gson= Gson()
    val comments=gson.fromJson(response,Response::class.java)
    println(comments.data?.get(0))
}

//Output: DataItem(createdTime=1501571384, from=From(fullName=Aanand Shekhar Roy, profilePicture=https://scontent.cdninstagram.com/t51.2885-19/10475071_605790259527941_865730435_a.jpg, id=1391934316, username=aanandshekharroy), id=17867282641151111, text=Testing api)

如您所见,我们可以将其用作普通的 Kotlin 对象,而无需使用JSONObject通过键来解析它,这使得 JSON 解析变得非常简单。

如何在 Kotlin 中下载文件

在我们的 Android 应用程序中,我们经常需要下载文件。最基本的方法将是打开 URL 连接并使用InputStream读取文件的内容,然后使用FileOutputStream将其存储在本地文件中;所有这些都在后台线程中使用AsyncTask完成。然而,我们不想重新发明轮子。有许多库可以为我们处理所有这些事情,使我们的工作变得非常简单,帮助我们创建干净的代码。

我们可以使用由谷歌开发者开发的网络库Volley(developer.android.com/training/volley/index.html),它使得网络通信变得非常简单快捷。另一个我们可以使用的是OkHttp(由Square提供),它非常高效,并且我们可以与Retrofit(用于 HTTP API)一起使用。

对于这个菜谱,我们将使用一个名为Fuel的网络库,它是用 Kotlin 编写的。

准备工作

创建一个新的 Android 项目并添加一个活动。现在,通过在您的build.gradle中添加以下行并将项目同步,将 fuel 依赖项添加到您的项目依赖项中:

//Fuel - Networking in Kotlin
compile 'com.github.kittinunf.fuel:fuel:$fuel_version' //for JVM

在这里,$fuel_version是 fuel 库的最新版本。

如何做到这一点…

按照以下步骤在 Kotlin 中下载文件:

  1. 让我们从视图中添加一个带有onClickListener的按钮开始。我还添加了一个progressBar到视图中,以便能够看到下载的进度。这是我的视图:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 

    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="android.my_company.com.helloworldapp.DownloadFileActivity"
    tools:showIn="@layout/activity_download_file">

    <Button
        android:id="@+id/btn_download_file"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="32dp"
        android:layout_marginTop="32dp"
        android:text="@string/download_file"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:progress="0"/>
</android.support.constraint.ConstraintLayout>
  1. 让我们从下载一个临时文件开始。我们将使用httpbin.org/来模拟下载文件 API。以下是为下载临时文件编写的代码:
class DownloadFileActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_download_file)
        setSupportActionBar(toolbar)
        Log.d("ya", filesDir.absolutePath + " " + filesDir.canonicalPath)

        btn_download_file.onClick {
            progressBar.progress = 0
            Fuel.download("http://httpbin.org/bytes/32768").destination { response, url ->
 File.createTempFile("abcd", ".tmp")
 }.progress { readBytes, totalBytes ->
 val progress = readBytes.toFloat() / totalBytes.toFloat()
 Log.d("progress", progress.toString())
 progressBar.progress = progress.toInt()*100
 }.response { req, res, result ->
 Log.d("status result", result.component1().toString())
 Log.d("status res", res.responseMessage)
 Log.d("status req", req.url.toString())
 }

        }
    }

}

这就是我们的 UI 看起来的样子:

图片

现在,正如我们之前提到的,我们将使用Fuel库来下载文件;以下是代码的示例:

btn_download_file.onClick {
            progressBar.progress = 0
            Fuel.download("http://httpbin.org/bytes/32768").destination { response, url ->
 File(filesDir , "abcd.txt")
 }.progress { readBytes, totalBytes ->
 val progress = readBytes.toFloat() / totalBytes.toFloat()
 Log.d("ya", progress.toString())
 progressBar.progress = progress.toInt()*100
 }.response { req, res, result ->
 Log.d("status result", result.component1().toString())
 Log.d("status res", res.responseMessage)
 Log.d("status req", req.url.toString())
 }
        }

我建议你尝试在 Fuel 中下载文件,包括有进度和无进度的情况,以更好地掌握这一点。你可以在github.com/kittinunf/Fuel上阅读 Fuel 提供的所有其他功能。

还可以尝试在 Android 中使用 Volley 和其他网络库,以了解每个库的不同点和用例。

如何在 Kotlin 中使用 RxJava 和 Retrofit

Retrofit是 Android 中最广泛使用的网络库之一。它是由Jake Wharton创建的开源库。RxJava 是 Java 中 ReactiveX 的开源实现。RxJava 是进行响应式编程或事件驱动编程的绝佳方式。本菜谱不会教你关于响应式编程(en.wikipedia.org/wiki/Reactive_programming),所以如果你不熟悉它,你可以通过文档(github.com/ReactiveX/RxJava)来学习。相反,在本菜谱中,你将学习如何一起使用 Retrofit 和 RxJava。

准备工作

我们将使用 Android Studio 3.0。请确保您拥有其最新版本。我们还需要添加以下依赖项:

compile "com.squareup.retrofit2:retrofit:$retrofit_version"
compile "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"
compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
// RxKotlin - Kotlin version of RxJava
compile "io.reactivex.rxjava2:rxkotlin:$rxKotlinVersion"

adapter-rxjava2库帮助我们返回作为响应的Observable,可以被观察者订阅。

如何做到这一点...

在 Retrofit 中使用 RxJava 非常简单;让我们看看以下步骤:

  1. Retrofit(用于与网络通信)的实例将按照以下方式创建:
Retrofit.Builder()
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
        .addConverterFactory(GsonConverterFactory.create())
        .client(okHttpClient)
        .baseUrl(AppConstants.INSTAGRAM_BASE_URL)
        .build()
  1. 下面的示例是一个接口,我们在这里定义了所有的 Retrofit 调用。你可能注意到,我们返回的是Observable,这是唯一的变化:
interface InstagramApiService {

    @FormUrlEncoded
    @POST("oauth/authorize")
    fun getRedirectCode(@Field("client_id") client_id: String,
                       @Field("redirect_uri") redirect_uri: String,
                       @Field("response_type") response_type: String): Call<String>

    @FormUrlEncoded
    @POST("oauth/access_token")
    fun getAccessToken(@Field("client_id") client_id: String,
                       @Field("client_secret") client_secret: String,
                       @Field("redirect_uri") redirect_uri: String,
                       @Field("grant_type") grant_type: String
                       , @Field("code") code: String)
                        :Observable<InstagramLoginResponse>

    @GET("v1/users/{user_id}/media/recent/")
    fun getInstagramPosts(@Path("user_id") user_id: String?, @Query("access_token") access_token: String?)
                                                        :Observable<InstagramPostsResponse>

    @GET("v1/media/{media_id}/comments")
    fun getCommentsForInstagramPost(@Path("media_id") media_id: String?
                                    , @Query("access_token") access_token: String?)
            :Observable<InstagramCommentsResponse>
}
  1. 然后,您需要创建您之前提到的服务实例,如下所示:
retrofit.create<InstagramApiService>(InstagramApiService::class.java)
  1. 现在你只需要调用该方法,并且你需要一个订阅者对象来订阅它:
instagramApiService.getCommentsForInstagramPost(instagramId)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeBy(onNext={
                    response: InstagramCommentsResponse ->
                   // Do something with response

                },onError = {
                    // Do something with error
                }))
  1. 我们已经在后台的另一个线程上推送了网络调用。当调用/任务完成时,结果将在主线程上被观察。

如何使用 RecyclerView 制作无限列表

Facebook、Instagram 和 Twitter 的动态有什么共同点?它们都几乎有无穷无尽的内容在你不断向下滚动时展示给你。毫无疑问,这是在您的平台上吸引用户的绝佳方式。

在这个菜谱中,我们将了解如何使用RecyclerView制作无限列表。它有许多用例,例如社交媒体、电子商务应用程序或任何基于内容的应用程序。

我们将创建一个简单的应用程序,它最初将加载一小部分数据,但一旦用户滚动到内容的底部,我们将获取另一组数据并将其附加到它,给用户一种无限内容的感觉。所以让我们开始吧!

准备工作

我们将使用 Android Studio 3.0;请确保您拥有其最新版本。

你还需要在build.gradle文件中包含RecyclerView,你可以按照以下方式添加:

compile 'com.android.support:recyclerview-v7:26.1.0'

你也可以在https://gitlab.com/aanandshekharroy/Anko-examples/存储库中找到源代码,通过检出6-endless-list-using-recycler-view分支。

如何做到这一点...

我们将创建一个简单的应用程序,该程序将在列表中显示数字。当你向下滚动时,列表将无限增长:

列表的创建方式如下:

  1. 首先,我们将创建一个将放置在列表中的项。以下是该行项 recycler_row.xml 的代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:orientation="vertical"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/recycler_row_text_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp" />

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_alignParentBottom="true"
        android:alpha="0.1"
        android:background="@android:color/black" />
</LinearLayout>
  1. 接下来,我们将在主活动布局文件中创建一个 RecyclerView
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:scrollbars="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</android.support.constraint.ConstraintLayout>
  1. 现在,我们将创建一个简单的 RecyclerView 适配器:
class RecyclerAdapter(val recyclerList: List<Int>) : RecyclerView.Adapter<RecyclerAdapter.ViewHolder>() {
    override fun onBindViewHolder(viewHolder: RecyclerAdapter.ViewHolder, position: Int) {
        viewHolder.bind(recyclerList[position])
    }

    override fun onCreateViewHolder(viewGroup: ViewGroup, position: Int): RecyclerAdapter.ViewHolder {
        val view = LayoutInflater.from(viewGroup.context).inflate(R.layout.recycler_row, viewGroup, false)
        return ViewHolder(view)
    }

    override fun getItemCount(): Int {
        return recyclerList.count()
    }

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val itemTextView :TextView= itemView.findViewById(R.id.recycler_row_text_view)

        fun bind(recyclerItemText: Int) {
            itemTextView.text = recyclerItemText.toString()
        }
    }

}
  1. 现在,让我们创建一个简单的函数,当调用它时,将 30 个数据项追加到列表中。这正是应用程序中所做的。一旦用户到达列表底部,就会发起一个网络调用,将数据追加到之前的列表中:
fun updateDataList(dataList: MutableList<Int>) : List<Int> {
    kotlin.repeat(30) {
        dataList.add(dataList.size + 1)
    }
    return dataList
}
  1. 现在,让我们在活动中设置回收视图:
class MainActivity : AppCompatActivity() {
    val dataList = mutableListOf<Int>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val layoutManager = LinearLayoutManager(this)
        val adapter = RecyclerAdapter(recyclerList = updateDataList(dataList))

        recyclerView.layoutManager = layoutManager
        recyclerView.adapter = adapter
        recyclerView.addOnScrollListener(object :                              RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)
                if (!recyclerView.canScrollVertically(1)) {
                    onScrolledToBottom();
                }
        }
        fun onScrolledToBottom() {
            val initialSize = dataList.size
            updateDataList(dataList)
            val updatedSize = dataList.size
            adapter.notifyItemRangeInserted(initialSize,updatedSize)
        }
})
}

它是如何工作的...

由于我们必须拦截用户到达列表底部的情况,我们添加了一个 ScrollListener。我们重写了 onScroll 方法,并使用了 canScrollVertically 方法。canScrollVertically 方法是在 API 级别 14 中添加的,它用于检查此视图是否可以在某个方向上垂直滚动。它接受一个整数参数(负数用于检查向上滚动,正数用于检查向下滚动)并返回一个布尔值(如果可能则为 true,否则为 false)。在我们的例子中,我们提供了一个正整数,如果视图可以向下滚动,则返回 true,如果不能则返回 false。如果不能进一步向下滚动(意味着数据已耗尽),我们将向列表中添加数据,并通过调用适配器的 notifyItemRangeInserted 方法来更新列表。

如何使用 Anko 在 Android 中用 Kotlin 运行后台任务

Anko 是由 JetBrains 团队创建的一个库,它通过许多辅助函数抽象了很多复杂性,使得 Android 开发变得相当容易。其中之一就是处理后台任务。使用 Anko,我们可以非常容易地处理后台任务。在这个菜谱中,我们将学习如何使用 Anko 来处理后台任务。

准备工作

我们将使用 Android Studio 3.0 进行编码;确保你有其最新版本。你需要将 Anko 添加到你的 build.gradle 文件中,如下所示:

implementation "org.jetbrains.anko:anko:$anko_version"

如何做到这一点...

在 Kotlin 中在后台执行任务非常简单。让我们看看下一个例子。在这个例子中,我们将发起一个网络请求(这需要在后台执行,否则你会得到一个 NetworkOnMainThread 异常);一旦网络请求完成,我们将使用 toast 显示成功消息。由于我们不能从后台线程触摸 UI 元素,我们需要回到 UI 线程来完成它。我们将使用 Anko 提供的 uiThread 方法,该方法将在后台任务完成后被调用:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        doAsync {
            val result= URL("https://api.instagram.com/v1/media/1571595528561539504_5812999640/comments?access_token=5812999640.42ee6f0.9441d5bd909f40319bad89407ffd7082").readText()
 uiThread {
                toast(result)
 } }
    }
}

正如你所见,URL().readText() 是一个长时间处理任务,因此我们将其放在后台任务中。

如何工作…

你必须已经使用过异步任务来在后台执行任务,但这并不是一个很有效的方法来做这件事。它处理屏幕旋转时的它们时存在问题,因为它没有注意到活动生命周期。

doAsync 方法是由活动调用的,如果活动正在销毁,则 uiThread 不会执行。这确保了你只能触摸 UI 直到它存在。

如何使用协程实现多线程

协程 是 Kotlin 中的一项优秀语言特性。以下是文档中对协程的一个恰当的定义:

"协程是编写异步、非阻塞代码(以及更多)的新方法。"

不仅使用起来方便,而且比线程更强大,尤其是在移动环境中,即使是一毫秒的性能提升也值得赞赏。启动多个线程可能会引起性能问题,但协程不会,因为即使有成千上万的协程运行,性能水平也不会有太大下降。

以下内容是 Kotlin 官方文档中所述:

"可以将协程想象为一个轻量级的线程。像线程一样,协程可以并行运行,互相等待,并进行通信。最大的区别是协程非常便宜,几乎是免费的;我们可以创建成千上万的协程,而在性能方面付出的代价非常小。另一方面,真正的线程启动和保持的成本很高。一千个线程对于一个现代机器来说可能是一个严重的挑战。”

这个事实使得它非常强大,Kotlin 团队也提供了简单的语法来使其易于使用。在这个菜谱中,我们将学习如何使用协程。那么,让我们开始吧!

准备工作

我们将使用 Android Studio 3.0 进行编码。协程作为一个库提供,它抽象了所有复杂性,并让库来处理。你需要在 build.gradle 文件中添加这个库,如下所示:

dependencies {  
    ...  
    compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.19.2" 
}

这个库发布到了 Bintray JCenter 仓库,所以你需要在你的仓库中添加 jcenter(),如下所示:

repositories {
 jcenter()
}

有一个需要注意的事项是,在 Kotlin 1.1 中协程是实验性的,所以你需要明确告诉编译器你知道这一点,并且你愿意使用它。为此,你需要在你的 build.gradle 文件中添加以下行:

apply plugin: 'kotlin'
 kotlin {
    experimental {
    coroutines 'enable'
 }
}

现在一切准备就绪,你可以在你的项目中开始使用协程。

如何操作…

让我们按照给定的步骤来了解 Kotlin 中协程的工作原理:

  1. 有两个函数可以启动协程:

    • launch{}

    • async{}

  2. 让我们尝试编写我们的第一个简单的协程函数:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        launch {
            delay(10000)
 println("Hello")
        }
    }
}

上述函数将在 10 秒后打印 "Hello" 到 Android Studio 控制台。请注意,我们使用了 launch 函数来启动一个协程,它返回一个 Job,但它不携带任何结果值。它在一个给定的线程池上启动一个新的协程(默认情况下,协程是在一个共享的线程池上运行的)。尽管基于协程的程序中仍然存在线程,但一个线程可以运行多个协程,因此我们不需要太多线程。

  1. 协程的关键在于挂起函数。我们只需在函数上添加suspend修饰符就可以创建一个挂起函数。考虑以下示例:
suspend fun timeConsumingMethod(arg: String): Boolean {
     //...
}
  1. 挂起函数只能从协程或另一个挂起函数中调用。如果你尝试从其他地方调用它们,你的代码甚至无法编译。

安东尼奥·利瓦(Antonio Leiva)将挂起函数解释如下:

“...当它们被调用时可以停止执行,一旦它们完成自己的任务就会继续执行。”

协程至少需要有一个挂起函数(在上一个示例中,delay是一个挂起函数)。

  1. 接下来,我们将看到async函数。从概念上讲,它与启动函数非常相似,除了async返回一个延迟的——一个轻量级的非阻塞未来,它代表了一个承诺,稍后提供结果(类似于 Java 的Future)。要从该延迟中获取结果,你使用.await(),由于延迟也是一个Job,因此如果需要,你可以取消它。让我们看看async的一个示例。

  2. 首先,我们将创建两个挂起函数并并发执行它们,然后将从两个函数中添加结果:

suspend fun longOperationOne(): Int {
    delay(1000L) 
    return 10
}

suspend fun longOperationTwo(): Int {
    delay(1000L) 
    return 20
}
val one = async { longOperationOne() }
val two = async { longOperationTwo() }
async {
    println("The answer is ${one.await() + two.await()}")
}
  1. 在前面的示例中,我们得到了延迟对象,这是一个Future对象。要从其中获取结果,我们使用了await函数。await函数本身就是一个挂起函数;这就是为什么我们将其包裹在一个async块中的原因。

  2. 一个需要注意的关键点是,这两个作业都是异步和并发运行的,因此是非阻塞的。

  3. 如果你想要以阻塞的方式运行它,你需要使用runBlocking方法。以下是相同的示例,但它将在获取结果时阻塞主线程:

val one = async { longOperationOne() }
val two = async { longOperationTwo() }
runBlocking {
    println("The answer is ${one.await() + two.await()}")
}

还有更多...

无论何时在考虑使用线程还是协程时,请记住罗马(来自 JetBrains 团队的工程师)的这些话:

“协程适用于大多数时候都在等待某些事情异步任务。线程适用于 CPU 密集型任务。”

在 Android 的上下文中,你总是想更新 UI,但你不能从后台线程中这样做。协程为此提供了一个解决方案。让我们看看下一个示例:

launch(UI) {
     val sum = lengthyJobOne.await() +lengthyJobTwo.await()
     myTextView.text = "Sum of results is $sum."
}

在前面的代码中,我们不仅可以在不阻塞主线程的情况下在后台计算两个作业,我们还可以触摸 UI 线程来更新视图。

第十二章:Lambda 和委托

本章将涵盖以下内容:

  • 使用 lambda 的点击监听器

  • 在 Kotlin 中使用懒委托

  • 使用可观察的委托

  • 使用可撤销的委托

  • 编写自己的委托

  • 使用 lateinit 修饰符

  • 与 SharedPreferences 一起工作

  • 在 Kotlin 中创建多个 let 的链

  • 创建全局变量

简介

在本章中,我们将探讨 Kotlin 语言的函数式特性。Kotlin 通过 lambda 表达式内置了函数式编程。Java 直到现在都缺少这种现代语言特性,但 Java 8 已经包含了 lambda 表达式。然而,由于大多数 Android 设备不支持 Java 8,Android 开发者无法使用这个特性。在本章中,我们将介绍这些内容,并学习关于委托的知识。委托是 Kotlin 的一个强大语言特性。那么,让我们开始吧!

使用 lambda 的点击监听器

在 Android 中,onclick 监听器是那些曾经占用大量行数的东西之一,即使代码的重要部分只有一行。Kotlin 极大地简化了 Android 框架,其中最好的改进之一就是onClickListener。在本菜谱中,我们将看到如何通过 lambda 的帮助简化传统的长点击监听器。

准备工作

我将使用 Android Studio 3 来编写代码。您可以在 Android Studio 3+中创建一个新的 Kotlin 项目,并包含一个空白活动,因为我们不会使用其他菜谱中的任何代码。您还需要对 Android 开发有一个中级理解。

如何做…

让我们按照给定的步骤来了解如何使用 lambda 表达式来使用点击监听器:

  1. 让我们从创建一个包含一些视图的活动开始,例如一个可以附加onClickListener的按钮。查看以下 XML 布局,这是一个可能的活动布局:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout

    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <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>

    <LinearLayout

        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/white"
        android:orientation="vertical"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <Button
            android:id="@+id/btn1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="@string/button1"/>

        <Button
            android:id="@+id/btn2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="@string/button2"/>

        <Button
            android:id="@+id/btn3"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="@string/button3"/>

    </LinearLayout>

</android.support.design.widget.CoordinatorLayout>
  1. 以下是我们布局的外观,其中包含三个需要附加onClickListener的按钮:

图片

  1. 现在,让我们看看当我们在 Java 中为三个按钮附加onClickListener时的代码:
public class HelloWorldActivity extends AppCompatActivity {
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_hello_world);
        final Button btn1 = (Button) findViewById(R.id.btn1);
        final Button btn2 = (Button) findViewById(R.id.btn2);
        final Button btn3 = (Button) findViewById(R.id.btn3);
        final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        getSupportActionBar().setTitle("Let's click");
        btn1.setOnClickListener(new View.OnClickListener() {
 public void onClick(View v) {
 Toast.makeText(HelloWorldActivity.this, "Button 1 has been clicked by you! One", Toast.LENGTH_SHORT).show();
 }
 });
        btn2.setOnClickListener(new View.OnClickListener() {
 public void onClick(View v) {
 Toast.makeText(HelloWorldActivity.this, "Button 2 has been clicked by you! Two.", Toast.LENGTH_SHORT).show();
 }
 });
 btn3.setOnClickListener(new View.OnClickListener() {
 public void onClick(View v) {
 Toast.makeText(HelloWorldActivity.this, "Button 3 has been clicked by you! Three", Toast.LENGTH_SHORT).show();
 }
 });
    }
}
  1. 你注意到我们为了仅仅附加显示 toast 的点击监听器而必须编写的代码量吗?所有这些代码只是为了三个 onclick 监听器;现在,让我们看看在 Kotlin 中编写相同代码的差异:
class HelloWorldActivity2 : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)
        supportActionBar?.title = "Let's click"
        btn1.setOnClickListener(object : View.OnClickListener {
 override fun onClick(v: View?) {
 toast("Button 1 has been clicked by you! One")
 }
 })
 btn2.setOnClickListener(object : View.OnClickListener {
 override fun onClick(v: View?) {
 toast("Button 2 has been clicked by you! Two")
 }
 })
 btn3.setOnClickListener(object : View.OnClickListener {
 override fun onClick(v: View?) {
 toast("Button 3 has been clicked by you! Three")
 }
 })
    }
}
  1. 代码确实更少、更整洁,这要归功于 Kotlin 的合成属性Anko的 toast 辅助函数。现在,让我们尝试使用lambda并看看它带来的差异:
class HelloWorldActivity2 : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)
        supportActionBar?.title = "Let's click"
        btn1.setOnClickListener({ toast("Button 1 has been clicked by you! One") })
 btn2.setOnClickListener({ toast("Button 2 has been clicked by you! Two") })
 btn3.setOnClickListener({ toast("Button 3 has been clicked by you! Three") })
    }
}

哇!向 lambda 的强大力量致敬。注意代码量减少了多少,看起来更整洁、更易读。这是一次大量的样板代码减少,这为我们节省了时间和精力。

它是如何工作的…

Lambda 函数是未声明但作为表达式传递的函数。在 Kotlin 中,如果一个函数接收一个接口,我们可以用 lambda 来替换它。例如,setOnClickListener函数接收View.OnClickListener,因此我们可以使用 lambda:

fun setOnClickListener(listener: (View) -> Unit)
someView.setOnClickListener({ view -> doSomething() })

此外,如果没有参数要传递给 lambda 函数,我们可以省略箭头,如果最后一个传递的参数是函数,我们可以将其移出括号:

someView.setOnClickListener() { doSomething() }

然后,如果传递给 lambda 函数的实际上是唯一参数,您可以完全省略括号:

button.setOnClickListener { doSomething() }

还有更多...

我们可以使用 Kotlin 的库 Anko 进一步减少代码。Anko 提供了一个接受 lambda 表达式的方法,该表达式在 onClick 事件发生时执行:

class HelloWorldActivity2 : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)
        supportActionBar?.title = "Let's click"
        btn1.onClick { toast("Button 1 has been clicked by you! One") }
 btn2.onClick { toast("Button 2 has been clicked by you! Two") }
 btn3.onClick { toast("Button 3 has been clicked by you! Three") }
    }
}

在 Kotlin 中使用懒代理

懒加载结构主要用于属性的懒初始化,这在初始化的对象是重对象(需要时间初始化)时尤其有用。在启动时实例化重对象可能会导致移动用户体验中的性能下降。懒初始化可以解决我们的问题。在这个菜谱中,我们将学习如何使用 Kotlin 的懒代理,让我们开始吧!

准备工作

我们将使用 Android Studio 3.0 进行编码,请确保您已下载最新版本。

如何做...

在以下步骤中,我们将学习如何在 Kotlin 中使用懒代理:

  1. 首先,让我们看看如何通过懒初始化创建属性。语法如下:
val / var <property name>: <Type> by <delegate>
  1. 在创建懒代理时,我们使用 by lazy,如下所示:
class MainActivity : AppCompatActivity() {
    private val textView : TextView by lazy {
        findViewById<TextView>(R.id.textView) as TextView
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        textView.text="ABC"
    }
}

懒代理在第一次访问时初始化对象并存储值,然后对于后续访问返回该值。

它是如何工作的...

属性的委托看起来像这样:

class Delegate {
    operator fun getValue(
            thisRef: Any?,
            property: KProperty<*>
    ): String {
        // return value
    }
 operator fun setValue(
            thisRef: Any?,
            property: KProperty<*>, value: String
    ) {
        // assign
    }
}

读取操作调用 getValue 方法,而写入操作调用 setValue

懒属性有三个评估模式:

  • LazyThreadSafetyMode.SYNCHRONIZED:初始化仅在单个线程上发生。其余线程看到缓存的值。这也是初始化的默认模式。

  • LazyThreadSafetyMode.PUBLICATION:当不需要初始化代理的同步时使用。它可以从多个线程同时调用,并且可以在每个线程上执行初始化。然而,如果初始化由一个线程执行,它将返回而不执行初始化。

  • LazyThreadSafetyMode.NONE:不使用锁来同步初始化,因此开销较小。

使用可观察的代理

之前,我们看到了如何处理委托属性。在这个菜谱中,我们将学习如何处理可观察的代理。这个代理帮助我们观察属性中的任何变化。那么,让我们开始吧。

准备工作

我们将使用 IntelliJ IDEA 编写代码。您可以使用任何能够执行 Kotlin 代码的 IDE。

如何做...

可观察的代理接受一个默认值和一个包含旧值和新值的结构。让我们看看下一个示例:

fun main(args: Array<String>) {
    var a:String by Delegates.observable("",{_,oldValue,newValue ->
        println("old value: $oldValue, new value: $newValue ")
    })
    a="a"
    a="b"
>}
//Output:old value: , new value: a 
 old value: a, new value: b

在前面的例子中,我们将初始值提供为空字符串。每次我们尝试更新a属性的值时,都会执行该结构。我们已经更改了a的值两次,因此我们看到了两条打印语句。

还有更多…

可观察的委托在RecyclerView的情况下特别有用,因为我们可以使用DiffUtils来更新仅更改的项目,而不是用新的列表替换整个列表。有关更多信息,请参阅第四章中的配方,在 Kotlin 中创建 RecyclerView 适配器

使用可撤销委托

可撤销委托与可观察委托非常相似,唯一的区别是它拒绝更改。在可观察委托中,每当可观察属性更改时,我们都可以获取新值和旧值。让我们看看 Kotlin 文档中提供的定义:

"返回一个读写属性委托,当属性更改时调用指定的回调函数,允许回调拒绝修改。"

准备工作

我将使用 IntelliJ IDEA 进行编码。您可以使用任何能够执行 Kotlin 代码的 IDE。

如何做…

让我们现在看看给定的步骤来理解vetoable修饰符:

  1. 让我们快速看一下vetoable委托属性的实现:
fun main(args: Array<String>) {
    var student:Student by Delegates.vetoable(Student(10),{property, oldValue, newValue ->
        if(newValue.age>25){
            println("Age can't be greater than 25")
            return@vetoable false
        }
        true
    })
    student=Student(26)
}
class Student(var age:Int)
//Output: Age can't be greater than 25
  1. 如您所见,由于年龄不能大于25,修改被vetoable委托“拒绝”。只有当年龄小于25时,才会分配新对象。

它是如何工作的…

让我们看看可撤销委托属性的声明:

inline fun <T> vetoable(
initialValue: T, 
crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean
): ReadWriteProperty<Any?, T> (source)

vetoable()接受一个初始值,这可以是一个空列表,还有一个onChange回调,它在更改属性之前被调用。如果更改成功,回调返回 true;如果被拒绝,则返回 false。

还有更多…

如果你在RecyclerView适配器中使用可撤销委托,它特别有用。通常,你会直接将数据分配给列表,并可能调用notifyDatasetChanged,但这非常低效,因为它会导致重新加载所有数据。我们可以使用可撤销委托通过匹配旧值和新值来检查内容是否相同,并在内容相同的情况下拒绝修改。此外,我们可以使用DiffUtils仅更新更改的数据。DiffUtils是在 Android 支持库 26.01 及以后的版本中引入的,使RecyclerView更加高效。

编写你自己的委托

委托属性是 Kotlin 语言中最好的特性之一。我们已经看到了可观察的和可撤销的委托。在这个菜谱中,我们将学习如何创建我们自己的自定义委托。作为一个演示示例,我们将创建一个只能初始化一次的委托属性;如果再次初始化,它应该抛出异常。那么让我们深入探讨一下,看看我们如何实现它。

准备工作

我们将使用 IntelliJ IDEA 进行编码。你可以使用任何能够执行 Kotlin 代码的 IDE。

如何做到这一点…

现在,让我们深入探讨如何创建我们自己的委托:

  1. 让我们创建一个名为 SingleInitializationProperty 的自定义委托。这个自定义委托属性如果变量没有被初始化,将抛出异常,并且它只能初始化一次。如果再次初始化,它将抛出异常。让我们看看我们的自定义委托类:
class SingleInitializableProperty<T>() : ReadWriteProperty<Any?, T>{
    private var value: T? = null
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        if(value==null){
            throw IllegalStateException("Variable not initialized")
        }else {
            return value!!
        }
    }
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        if(this.value==null){
            this.value=value
        }else{
            throw IllegalStateException("Cannot be initialized twice")
        }
    }
}
  1. 现在,我们已经创建了一个自定义委托,让我们尝试以下方式使用它而不进行初始化:
fun main(args: Array<String>) {
    var a:String by SingleInitializableProperty()
    println(a)
}

这就是输出结果:

Output: Exception in thread "main" java.lang.IllegalStateException: Variable not initialized
  1. 让我们看看另一个例子;这次,我们将首先初始化它,然后访问它,然后再次尝试初始化它:
fun main(args: Array<String>) {
    var a:String by SingleInitializableProperty()
    a="first"
    println(a)
    a="second"
}

这里是输出结果:

Output:first
Exception in thread "main" java.lang.IllegalStateException: Cannot be initialized twice

它是如何工作的…

如你所见,我们在委托属性中实现了 ReadWriteProperty 接口,这意味着我们的变量将是 var 类型。如果你想使其不可变,你可以实现 ReadOnlyProperty 接口。

getValue 函数接收一个类的引用和属性元数据。setValue 函数反过来接收一个设置的值。在不可变属性(val)的情况下,将只有一个 getValue 函数。

使用 lateinit 修饰符

延迟初始化(Lateinit)是一个重要的初始化属性,因为如果你不想在构造函数中初始化你的变量,可以使用 lazylateinit 来实现。在这个菜谱中,我们将看到如何使用 lateinit 修饰符以及它与 lazy 修饰符的不同之处。

准备工作

我将使用 IntelliJ IDEA 进行编码;你可以使用任何能够执行 Kotlin 代码的 IDE。

如何做到这一点…

让我们按照给定的步骤来理解 lateinit 修饰符是如何工作的:

  1. 在 Java 中,我们可以在声明变量后稍后初始化它,但 Kotlin 要求你在声明时立即初始化它(除非你使用特殊修饰符)。所以你可以这样做:
var student:Student?=null

或者,你可以这样做:

val student=Student()

这两种方法都有它们的缺点。第一种方法要求你在使用时检查可空性,而第二种初始化方法将使其不可变。

  1. 为了克服限制,我们可以使用 lateinit 修饰符,通过它可以先声明后在我们想要的地方(但在第一次访问之前)初始化。这在使用依赖注入时尤其需要。让我们看看 Kotlin 文档中使用的 lateinit 修饰符来声明变量的例子:
public class MyTest {  
    lateinit var subject: TestSubject
    @SetUp fun setup() {  
        subject = TestSubject()  
    }   
    @Test fun test() {  
        subject.method() // dereference directly  
    } 
}
  1. 如果你尝试在初始化之前访问变量,你会得到 UninitializedPropertyAccessException。如果你使用依赖注入,以下是使用 lateinit 的方法:
@Inject
 lateinit var mPresenter:EducationMvpPresenter

还有更多…

初始化属性的另一种方式是使用 lazy 修饰符;lazy() 基本上是一个接受 lambda 表达式并返回一个 lazy 实例的函数,该实例作为实现懒加载属性的代理。让我们看看下一个示例:

public class Student{ 
    val name: String by lazy { 
        “Aanand Shekhar Roy” 
    }
}

通过 lazy 初始化,我们将初始化推迟到我们第一次使用它的时候。属性仅在第一次访问时初始化,并且对于后续的访问返回相同的值。这就是为什么必须标记变量为不可变的原因。这真的可以帮助我们初始化耗时较多的对象,这些对象需要花费很多时间。懒加载初始化可以提高我们的启动时间。唯一的缺点是,由于它是一个 val 属性,你将无法稍后修改它。

使用 SharedPreferences

SharedPreferences 是 Android 设备上持久化数据存储的一种方式,通常用于以键值对的形式保存数据,例如应用程序的设置。Kotlin 通过其独特的语言结构使与共享偏好设置一起工作变得更加容易。在这个菜谱中,我们将看到 Kotlin 如何帮助我们轻松地处理 SharedPreferences。所以让我们开始吧。

准备工作

我们将在这个菜谱中使用 Android Studio 3.0。如果你有更早版本的 Android Studio,要么将其更新到 3.0,要么在其中配置 Kotlin。

如何操作...

为了能够定义和使用 SharedPreferences,我们需要遵循特定的步骤。我们将逐一介绍每个步骤,并一起实现:

  1. 首先,我们将创建一个 Prefs 类,它将作为读取/写入我们应用程序 SharedPreferences 的单一入口。这将使处理所有 SharedPreferences 变得更容易,因为它们都将在一个地方。正如我们所知,共享偏好设置需要上下文存在,所以我们将上下文传递给主构造函数。我们还将创建一个单一的 SharedPreferences 对象,我们将在整个类中使用它:
class Prefs (mContext:Context){
    val sharedPrefences=mContext.getSharedPreferences("com.ankoexamples.app",Context.MODE_PRIVATE)
    val PREF_USERNAME="pref_username"
}
  1. 例如,我们定义了一个 PREF_USERNAME SharedPreferences;在这里,我们将存储用户的用户名。现在有趣的部分开始了;记住 Kotlin 有一个属性,我们可以显式地定义如何获取和设置该属性。我们在这里也会使用同样的方法。让我们看看给出的代码:
var username:String
    get() = sharedPrefences.getString(PREF_USERNAME,null)
   set(value)=sharedPrefences.edit().putString(PREF_USERNAME,value).apply()

如你所见,在设置器中,我们正在编辑共享偏好设置,在获取器中,我们正在提取共享偏好设置的值。

  1. 现在我们已经准备好了 Prefs 类,我们可以在我们的活动、片段等中使用它。最好的方法是在 Application 类中定义它,并从许多活动或片段中访问它,因为这样我们就不需要创建多个 Prefs 类的对象。所以让我们创建一个 Application 类和一个 Prefs 类的单例实例:
class App:Application() {
    companion object {
        var prefs: Prefs? = null
    }

    override fun onCreate() {
        prefs = Prefs(this)
        super.onCreate()
    }
}

我们将 prefs 变量添加到伴生对象中,以便能够静态地使用它。现在,由于我们将它放置在 Application 类中,我们只处理 prefs 对象的单个实例。

  1. 我们还可以使用 lazy 构造来确保我们只在第一次访问时创建对象。这样做也有助于我们避免空检查。下面是我们的 App 类将如何看起来:
val prefs: Prefs by lazy {
    App.prefs!!
}
class App:Application() {
    companion object {
        var prefs: Prefs? = null
    }
    override fun onCreate() {
        prefs = Prefs(this)
        super.onCreate()
    }
}
  1. 现在我们来看一个例子,向我们的 SharedPreferences 添加一个值:
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        prefs.username="Aanand"
    }
}
  1. 现在使用共享偏好设置看起来非常简单,就像我们正在给变量赋值一样。访问它们也非常容易:
Log.d(prefs.username) // Aanand

还有更多...

如你所见,我们使用了 apply() 方法来保存偏好设置,这立即提交了内存中 SharedPreferences 的更改,同时也启动了对磁盘的异步提交;另一方面,commit() 方法则是同步写入持久存储。

在 Kotlin 中创建多个 let 的链

let 是 Kotlin 的 Standard.kt 库提供的一个非常有用的函数。它基本上是一个作用域函数,允许你在其作用域内声明变量。让我们看看下面的代码:

someVariable.let{
    // someVariable is present as "*it"* }

然而,最好的是它可以用来避免空检查。之前,你可能使用以下方法:

if(someVariable!=null){
    // do something 
}

虽然前面的代码是好的,但它并不非常适合用于修改属性。另一种选择是使用 ?.let (someVariable.?let{}),这确保了当变量不为 null 时代码块会运行。然而,如果我们有多个非空链,我们应该如何处理这些情况呢?让我们看看在这个菜谱中如何处理这些情况。

准备工作

我们将使用 IntelliJ IDEA 来编写代码。你可以使用任何能够执行 Kotlin 代码的 IDE。

如何做...

按照提到的步骤了解如何创建多个 let 的链:

  1. 当你需要进行多次空检查时,你可以显然使用嵌套的 if-else,检查空条件,如下面的代码所示:
if(variableA!=null){
    if(variableB!=null){
        if(variableC!=null){
            // do something.
        }
    }
}
  1. 由于我们知道 let 函数保证只有在对象不为 null 时才会运行代码块,我们需要创建一个函数来执行 let 的功能,但适用于三个变量场景。让我们看看我们的函数:
fun <T1: Any, T2: Any,T3:Any, R: Any> multiLet(p1: T1?, p2: T2?,p3:T3?, block: (T1, T2,T3)->R?): R? {
    return if (p1 != null && p2 != null &&p3!=null) block(p1, p2,p3) else null
}
  1. 现在,我们可以像下面这样使用它:
fun main(args: Array<String>) {
    var variableA="a"
    var variableB="c"
    var variableC="b"
    multiLet(variableA,variableB,variableC){
        _,_,_->
        println("Everything not null")
    }
}

//Output: Everything not null
  1. 以类似的方式,它也可以用于两个变量场景。你可能想知道如何在多对象场景中实现,比如在列表的情况下。让我们创建一个 whenAllNotNull 函数,它只会在列表的所有元素都不为 null 时运行代码块:
var nonNullList=listOf("a","b","c")
nonNullList.whenAllNotNull {
    println("all not null")
}
fun <T: Any, R: Any> Collection<T?>.whenAllNotNull(block: (List<T>)->R) {
    if (this.all { it != null }) {
        block(this.filterNotNull())
    }
}

Output: all not null

创建全局变量

在 Java 中,我们只需在类声明的开头定义变量并在之后初始化它,就可以创建一个全局变量。通过仅仅声明它,我们就可以将其用作全局变量。

在这个菜谱中,我们将学习如何在 Kotlin 中创建和使用全局变量。

准备工作

我将使用 IntelliJ 进行编码。你可以使用任何可以编写和执行 Kotlin 代码的 IDE。

如何做...

现在,让我们看看如何在 Kotlin 中创建全局变量。有两种方法可以实现,让我们逐一来看:

  1. 一种实现方式是在类声明下声明它。我们可以使用 var 声明,如下所示:
fun main(args: Array<String>) {
    var student:Student?=null
}

然而,这种方法会在每次使用时都进行空值检查:

println(student?.age)
  1. 为了防止这种情况,你可以使用 val 声明并初始化它,但这将导致变量不可变,这可能不是期望的行为。

  2. 另一种声明全局变量的方式是使用 lateinit 修饰符。以下是前面代码的修改方式:

fun main(args: Array<String>) {
    lateinit var student:Student
    student=Student()
    println(student.age)
}
  1. lateinit 修饰符用于首先声明变量,无需将其定义为 null 或不可变。然而,在使用它之前我们需要对其进行初始化;否则,它将抛出 UninitializedPropertyAccessException 异常。

lateinit 修饰符不适用于原始类型。

  1. 当你尝试使用依赖注入初始化变量时,lateinit 也可以很有用。这样,你可以在类体内部引用属性时避免进行空值检查。

第十三章:测试

本章将涵盖以下食谱:

  • Kotlin 代码的单元测试

  • 使用 Mockito 进行单元测试

  • 运行仪器测试

  • 在 Kotlin 中编写 JUnit 规则(@Rule)

  • 使用 Espresso Kotlin 进行验收测试

  • 在 Kotlin 中编写 assertEquals

简介

如果您希望代码库可扩展且可维护,测试是软件工程的基本部分。在 Android 中,基本上有两种类型的测试:一种是单元测试,另一种是集成测试。单元测试是一种独立测试各个单元的测试类型,而集成测试(有时也称为仪器测试),则需要 Android 设备或模拟器来运行测试。由于集成测试需要真实设备或模拟器,这些测试通常执行速度较慢。单元测试速度快,因为它们不需要真实设备或模拟器来运行。由于单元测试速度快而仪器测试慢,人们通常认为一个健壮的测试套件应该有这些测试的比例为 80%到 20%。因此,您的代码库应该由 80%的单元测试和 20%的仪器测试组成。

Kotlin 代码的单元测试

单元测试基本上涉及单元测试。这些测试通常执行速度更快,因为它们在 JVM 中执行,因此不需要进行 dexing、打包和在模拟器上安装的步骤,将测试周期从分钟缩短到秒,以便您可以快速迭代和重构代码。另一方面,集成测试则需要上述所有步骤。除了测试代码外,单元测试还充当代码库的绝佳文档。这就是为什么如果您看到方法名称以奇特的方式表达,例如,testIfConfirmationEmailIsSent,您可能不会感到惊讶。

在本食谱中,我们将学习如何为您的 Android 代码编写单元测试。

准备工作

您将需要 Android Studio,因为我们将学习如何为 Android 代码编写单元测试,并且因为 Android Studio 为单元测试提供了极大的支持。您还可以在gitlab.com/aanandshekharroy/Anko-examples的 4-unit-tests 分支中找到源代码。

如何操作...

按照以下步骤了解如何在 Kotlin 语言中编写 Android 代码的单元测试:

  1. 当您在 Android Studio 中创建一个新的 Android 项目时,Android Studio 将提供对单元测试和 Android 测试的支持。它为您提供了单独的目录,您可以在其中放置您的测试。请看以下截图:

如您所见,test 是放置单元测试的地方,androidTest 是放置Android 测试仪器测试的地方。

  1. 已经为你提供了两个演示测试:ExampleUnitTest 和 ExampleInstrumentedTest。要运行它们,只需右键单击 ExampleUnitTest 并点击运行 ExampleUnitTest。运行测试后,你可以在控制台中看到测试结果,如下面的截图所示:

图片

现在,让我们尝试创建我们自己的单元测试:

  1. 我们通常在我们的代码中有一个 Utility 类,它包含可以被任何类使用的方法,所以我们不是在每一个类中定义这些方法,而是在 Utility 类中定义它们。所以,让我们创建一个方法,addTwoNumbers,它将接受两个参数,ab,并返回一个结果——a+b
class Utility {
  companion object {
      fun addTwoNumbers(a:Int, b:Int):Int=a+b
  }
}
  1. 在 Android Studio 中,你可以直接从类本身创建测试。只需右键单击类名,然后点击创建测试:

图片

  1. 之后,你将看到一个对话框,你可以选择你想要创建测试的所有方法。建议为每个方法都编写测试:

图片

  1. 当你点击“确定”时,将显示另一个对话框,询问你将测试放在哪里。在这种情况下,这是一个单元测试,所以我们将它放在 app/src/test…

图片

  1. 当你点击“确定”时,Android Studio 会自动在 UtilityTest.kt 中生成样板代码,如下所示:

图片

  1. 现在,我们将添加几个 assertEquals 语句,这些语句将检查预期值与结果:
class UtilityTest {
  @Test
  fun addTwoNumbers() {
      assertEquals(5,Utility.addTwoNumbers(2,3))
      assertEquals(5,Utility.addTwoNumbers(4,1))
      assertNotEquals(5,Utility.addTwoNumbers(2,5))
  }
}

第一个参数是预期值,第二个参数是函数的输出。如果你运行测试,它将通过。

它是如何工作的…

当你运行单元测试时,它们会测试所有带有 @Test 注释的方法。还有一个 @Before 注释,它位于一个方法上方。带有 @Before 注释的方法将在类的任何其他方法之前运行。这在你设置可能被以后使用到的对象和变量时非常有用。

在这里需要注意的是,单元测试不能使用 Android SDK 组件。要使用这些组件,你需要使用仪器测试,或者使用像 Mockito 这样的模拟框架,它模拟 Android 组件以便在单元测试中使用。我们将在下一个菜谱中介绍这一点。

使用 Mockito 进行单元测试

正如我们在前面的菜谱中讨论的,我们不能在单元测试中使用 Android 组件。这就是为什么我们能够更快地运行它们,而且不需要任何设备。如果你想在测试中使用 Android 组件,有两种选择:

  • 编写集成测试,这些测试将在你的设备或模拟器上运行。

  • 使用模拟框架,例如 Mockito,它基本上模拟了 Android SDK 组件,这样你就可以在不使用任何设备或模拟器的情况下使用它们,就像其他单元测试一样。模拟框架的好处是测试运行时间大大减少,因为测试基本上是单元测试。以下是 Vogella 对模拟对象的准确定义:

"一个模拟对象是一个接口或类的占位实现,在其中你定义了某些方法调用的输出。模拟对象被配置为在测试期间执行特定的行为。它们通常会记录与系统的交互,测试可以验证这些交互。"

有了这个想法,让我们尝试使用 Mockito 编写单元测试。

准备工作

你需要 Android Studio,因为它为单元测试提供了很好的支持,我们还将学习如何编写 Android 代码的单元测试。

你也可以在gitlab.com/aanandshekharroy/Anko-examples的 4-unit-tests 分支中找到源代码。

首先,你需要将 Mockito 依赖项添加到你的项目中。你可以在你的build.gradle文件中添加以下行来实现这一点,在 app 级别:

testImplementation 'org.mockito:mockito-core:2.8.47'

一旦添加了依赖项,你就可以继续前进。

如何做…

我们通常使用 Mockito 来模拟 Android 类,但让我们用 Mockito 测试一个简单的类:

  1. 这里有一个小的测试,用于测试Utility类的functionUnderTest函数:
@Test
fun test_functionUnderTest(){
    val classUnderTest= mock(Utility::class.java)
    classUnderTest.functionUnderTest()
    verify(classUnderTest).functionUnderTest()
}
  1. 在前面的类中,我们调用functionUnderTest方法,然后验证该方法是否被调用过。(是的,这不是一个很好的测试用例,但让我们尝试运行这个测试)当你运行它时,你会看到一个错误,如下所示:
org.mockito.exceptions.base.MockitoException: 
Cannot mock/spy class com.ankoexamples.app.Utility
Mockito cannot mock/spy because :
 - final class
  1. 前面错误的原因是 Kotlin 中每个类默认都是 final 的。如果你想扩展或模拟它们,你需要将它们打开。然而,这意味着你需要为想要测试的每个类添加open修饰符吗?这听起来是个坏主意,确实如此。有一个解决这个问题的方法。这个方法是在test/resources/mockito-extensions文件夹中手动添加模拟 final 类的选项。你需要创建一个名为org.mockito.plugins.MockMaker的文件,并将以下代码放入该文件中:
mock-maker-inline

现在如果你运行代码,它将顺利通过。

  1. verify方法有许多变体,如下所示:

    • verify(classUnderTest, never()).functionUnderTest(),这测试了该方法是否从未被调用过

    • atLeastOnce()atLeast(2)times(5)atMost(3),这些也可以用来验证与方法的交互次数

  2. 让我们看看另一个 Mockito 测试,它模拟了SharedPreferences(一个 Android 组件):

@Test
fun testSharedPreference(){
    val sharedPreferences=mock(SharedPreferences::class.java)
    `when`(sharedPreferences.getInt("random_int",-1)).thenReturn(1)
    assertEquals(sharedPreferences.getInt("random_int",-1),1)
}

when(...).thenReturn(...)结构会监视对象,当when结构内的方法被调用时,它返回thenReturn结构下的值。注意when周围的java `` ;这是因为when是 Kotlin 中的保留关键字,所以我们用反引号来调用它。

  1. 你还可以返回多个值,这模拟了多次调用方法:
@Test
fun testSharedPreference(){
    val sharedPreferences=mock(SharedPreferences::class.java)
    `when`(sharedPreferences.getInt("random_int",-1)).thenReturn(1).thenReturn(2)
    assertEquals(sharedPreferences.getInt("random_int",-1),1)
    assertEquals(sharedPreferences.getInt("random_int",-1),2)
}

在前面的示例中,对 getInt 方法的第一次调用将返回 1,第二次调用将返回 2

还有更多…

让我们了解单元测试中的 spy 对象。

间谍对象

模拟框架还提供了一个 spy 方法,可以用来包装真实对象。对间谍对象的调用被委派给真实对象。你可能会想,这有什么用呢?它可以检查真实对象上的交互,如果对象没有被模拟,这是不可能的。让我们看看以下示例:

@Test
fun testSpyObject(){
    val list = List(2,init = {-1})
    val spy= spy(list)
    assertEquals(spy.get(0),-1)
    verify(spy).get(0)
}

前面的测试将会通过。

注意,调用 spy.get(0) 返回 -1,这与你与真实对象交互时得到的结果相同。此外,你还能验证交互。

Mockito 限制

Mockito 有某些限制——例如,你不能模拟 staticprivate 方法。这超出了本书的范围,所以想了解更多关于 Mockito 限制的信息,请访问 github.com/mockito/mockito/wiki/FAQ#what-are-the-limitations-of-mockito

运行仪器测试

在前面的菜谱中,我们学习了如何运行和编写单元测试。在这个菜谱中,我们将学习如何运行仪器测试。集成测试位于你的 Android 项目中的 androidTest 目录下。

准备工作

由于仪器测试需要在真实设备或模拟器上运行,请确保你拥有其中之一。我们将使用 Android Studio 3.0 进行编码。你可以从 gitlab.com/aanandshekharroy/Anko-examples 下载源代码,并切换到 5-instrumentation-tests 分支。我们还将使用 Espresso 编写仪器测试,因为它是最容易使用的软件。当你创建新项目时,Espresso 会自动包含在你的项目中。

Espresso 面向那些认为自动化测试是开发生命周期中不可或缺部分的开发者。虽然它可以用于黑盒测试,但只有熟悉待测代码库的人才能解锁 Espresso 的全部功能。

如何做…

在以下步骤中,你将学习如何运行仪器测试:

  1. 让我们创建一个简单的应用程序,它将只包含 Hello World! 文本和一个按钮:

图片

  1. 点击按钮时,文本将更改为 Goodbye World!:

图片

  1. 现在,让我们编写一个测试来验证这个行为。

在这个菜谱中,我们将学习如何运行咖啡测试;在下一个菜谱中,我们将学习如何编写咖啡测试,所以请耐心等待,直到我解释如何在后续菜谱中编写咖啡测试,因为它很复杂,需要整个菜谱来公正地处理它。

这是一个咖啡测试:

class MainActivityTest {
    @Rule
    @JvmField var activityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java)

    @Test
    fun testButtonBehaviour() {
        onView(withText("Hello World!"))
                .check(matches(isDisplayed()))
        onView(withId(button)).perform(click())
        onView(withText("Goodbye World!"))
                .check(matches(isDisplayed()))

    }

}

testButtonBehaviour 方法的第一行,我们正在检查“Hello World!”是否出现在屏幕上。然后,我们对按钮执行点击操作,最后检查“Goodbye World!”是否出现在屏幕上。

  1. 要运行前面的测试,只需右键单击测试类并选择运行 MainActivityTest:

  1. 一旦选择该选项,就会弹出一个对话框,询问您要在哪个设备上运行测试。您可以选择一个真实设备或模拟器。

  2. 之后,您可以在设备上看到测试的运行情况(您将看到代码中写出的步骤在设备上执行)。

还有更多……

如果运行任何仪器测试,您会注意到即使是一个小测试,它也需要很长时间才能通过。在当前场景中,测试驱动开发(TDD)越来越受欢迎,但它的测试执行时间很长;这不是使用 TDD 的好方法。因此,应该将仪器测试的数量保持在最低限度,并且最好使用模拟框架,如 Mockito 或 Robolectric。

维基百科表示,TDD 是一种依赖非常短的开发周期重复的软件开发过程:需求被转化为非常具体的测试用例,然后软件被改进以通过新的测试。

在 Kotlin 中编写 JUnit 规则(@Rule)

规则是一种为类中所有测试添加功能的方式。例如,ExternalResource 在测试方法前后执行代码。这可以在测试方法之前设置数据库、网络和文件系统连接,并在测试完成后断开它们。当然,您也可以使用 @Before@After 注解来完成,但使用 ExternalResource(作为 JUnit 规则)有助于代码重用。

准备工作

我将使用 Android Studio 3.0 进行编码。

如何做到这一点……

在这个菜谱中,我们将使用 ExpectedException 作为 JUnit 规则,因为它有助于测试声明期望出现异常,并提供了一种清晰表达期望行为的方式。它比使用 @Test(expected= ...) 注解更加灵活,因为我们可以测试特定的错误消息和自定义字段。

在以下步骤中,我们将学习如何编写 JUnit 测试:

  1. 让我们先创建一个会抛出异常的简单方法。然后我们将编写一个测试来测试这个方法:
fun methodThrowsException() {
    throw IllegalArgumentException("Age must be integer")
}
  1. 现在,让我们为 ExpectedException 类创建一个新的规则并编写一个测试:
@Rule
var thrown = ExpectedException.none()

@Test
fun testExceptionFlow() {
    thrown.expect(IllegalArgumentException::class.java)
    thrown.expectMessage("Age must be integer")
    Utility.methodThrowsException()
}
  1. 如果运行前面的代码,您将得到一个错误:
org.junit.internal.runners.rules.ValidationError: The @Rule 'thrown' must be public.
  1. 错误是因为 JUnit 允许通过测试类字段或 getter 方法提供规则。然而,在 Kotlin 中我们没有字段——我们有属性;所以您在这里真正注解的是属性,而不是字段。

  2. 解决这个问题的最简单方法是通过添加 @JvmField 注解和 @Rule

@Rule @JvmField
var thrown = ExpectedException.none()
  1. 如果现在运行测试,它将通过。

它是如何工作的……

我们知道 Kotlin 与 Java 不同,它操作的是属性而不是字段。然而,为了与 Java 语言兼容,可以使用 @JvmField 指示 Kotlin 编译器不要为这个属性生成 getters-setters,并将其作为字段暴露。

然而,在使用注解时有一些限制。我们无法与以下内容一起使用:

  • 私有属性

  • 带有 openoverrideconst 修饰符的属性

  • 委托属性

使用 Espresso Kotlin 进行验收测试

Espresso 是 Android 最受欢迎的 UI 测试框架。它于 2013 年由 Google 发布,是同类中最容易使用的。它支持复杂的功能,例如确保在运行测试之前运行活动,或者等待观察到的后台任务完成。在 Espresso 之前,这些事情很难同步,UI 测试被认为是一项困难的任务。

在这个菜谱中,我们将学习如何使用 Espresso 进行验收测试。

验收测试是一种软件测试级别,其中测试系统是否可接受。此测试的目的是评估系统是否符合业务需求,并判断其是否适合交付。

来源:softwaretestingfundamentals.com/

准备工作

我们将使用 Android Studio 3.0 进行编码。你可以从gitlab.com/aanandshekharroy/Anko-examples下载源代码,并切换到 5-instrumentation-tests 分支。

如何做到这一点...

在 Espresso 中,我们主要有三个组件:

  • ViewMatchers: 允许你在当前视图层次结构中查找视图。这可以通过多种方式完成,例如通过 idnamechild 等进行搜索。你还可以使用 Hamcrest 匹配器,例如 containsString

  • ViewActions: 允许你在视图中执行操作,例如点击、输入、清除文本等。

  • ViewAssertions: 允许你断言视图的状态,检查视图断言下的条件是否通过。

让我们看看以下步骤,以了解如何使用 Espresso 进行验收测试:

  1. 这里是一个文本匹配器(文本匹配器匹配文本;它是 ViewMatchers 的一部分)的示例:
onView(withId(R.id.textView)).check(matches(withText(not(containsString("Hello")))));
  1. 现在我们将创建一个简单的测试,以测试点击按钮是否将文本从 Hello World! 更改为 Goodbye World!:

图片

  1. 以下是一个用于测试前面功能的 Espresso 测试示例:
class MainActivityTest {
    @Rule
    @JvmField var activityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java)

    @Test
    fun testButtonBehaviour() {
        // Testing if the text is initially Hello World!        
        onView(withText("Hello World!"))
                .check(matches(isDisplayed()))
        onView(withId(button)).perform(click())
        // Testing if the text is initially Goodbye World!
        onView(withText("Goodbye World!"))
                .check(matches(isDisplayed()))

    }

}
  1. 在第一行,我们创建了一个规则,它提供了单个活动的功能测试。这将打开测试运行之前的活动,并在测试完成后关闭它。testButtonBehaviour 方法中的语句检查 UI 条件,例如文本是否最初为 Hello World!(第一个条件),然后对按钮执行点击操作,最后检查文本是否现在是 Goodbye World!。

  2. 您也可以使用 instrumentation API 获取“上帝”对象——即 Context

var targetContext:Context = InstrumentationRegistry.getTargetContext()
  1. 如果您想从意图中启动活动,您只需在 ActivityTestRule 构造函数中将第三个参数提供为 false 即可;它可以像下面这样使用:
@Rule
@JvmField var intentActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java,true,false)

@Test
fun testIntentLaunch(){
    val intent = Intent()
    intentActivityRule.launchActivity(intent)
    onView(withText("Hello World!"))
            .check(matches(isDisplayed()))
}

Espresso 的另一个酷特性是使用 Record Espresso Test 记录交互。这将记录您与应用程序的所有交互,并可以从中生成测试。要使用该功能,请按照以下步骤操作:

  1. 在工具栏上转到“运行”并选择“Record Espresso Test”:

图片

  1. 然后将打开一个对话框,您可以在其中看到记录的步骤:

图片

  1. 点击 OK 将自动根据您的交互生成测试。

还有更多...

注意,我们已经在规则中使用了 @JvmField 注解。关于这一点,在 如何在 Kotlin 中编写 JUnit 规则 (@Rule) 菜谱中有详细的讨论。

在 Kotlin 中编写 assertEquals

assertEquals 语句在测试代码中得到了广泛的应用。它基本上接受两个参数——一个预期值和一个实际值,还有一个可选的第三个参数消息。如果预期值与实际值匹配,assertEquals 通过——否则,它失败。

使用原始类型与 assertEquals 一起使用很简单,但如果你想用它与自定义对象一起使用,你将不得不做更多的工作。例如,以下 assertEquals 将不会通过:

assertEquals(MyObj("abc"),MyObj("abc"))

在这个菜谱中,我们将学习如何编写 assertEquals 语句。

准备工作

我们将使用 Android Studio 3.0 进行我们的编码。您可以从 gitlab.com/aanandshekharroy/Anko-examples 下载源代码,并切换到 5-instrumentation-tests 分支。

如何做到这一点...

让我们通过以下步骤来了解assertEquals

  1. 在以下代码中,如果您运行给定的 assertEquals,它将不会通过:
assertEquals(MyObj("abc"),MyObj("abc"))
  1. 如果您检查差异,它将告诉您它们不相等,因为它们是两个不同的对象:

图片

  1. 因此,我们需要重写 MyObj 类的 equals 方法,我们将检查以下内容:

    • 是否其他对象引用相同的对象——这可以通过使用 === 运算符来完成,它检查引用相等性

    • 是否问题中的对象等于其他对象的 Java 类

    • 两个对象的内容是否相同:

override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other?.javaClass != javaClass) return false
    other as MyObj

    if (name != other.name) return false

    return true
}

现在,当你用内容相同的两个对象运行 assertEquals 时,它将顺利通过。

第十四章:使用 Kotlin 的 Web 服务

本章将涵盖以下食谱:

  • 如何在 Tomcat 上运行应用程序

  • 设置构建 RESTful 服务的依赖项

  • 如何创建 REST 控制器

  • 为 Spring boot 创建应用程序类

简介

Kotlin 已经占据了 Java 世界。它已经成为 Android 生态系统中的热门,该生态系统曾由 Java 主导,并且到处都受到热烈欢迎。Kotlin 不仅限于 Android 开发,还可以用于开发服务器端、客户端 Web 应用程序。在本章中,我们将解决的一个use案例是使用 Kotlin 创建 Web 服务。Kotlin 与 JVM 100%兼容,因此你可以使用任何现有的框架,如 Spring Boot、Vert.x 或 JSF 来编写 Java 应用程序。

如何在 Tomcat 上运行应用程序

在本食谱中,我们将学习如何在 IntelliJ IDEA 中安装、配置和运行 Tomcat 上的应用程序。

Apache Tomcat,通常被称为 Tomcat 服务器,是由 Apache 软件基金会(ASF)开发的开源 Java Servlet 容器。Tomcat 实现了多个 Java EE 规范,包括 Java Servlet、JavaServer Pages(JSP)、Java EL 和 WebSocket,并提供了一个 Java 代码可以运行的“纯 Java”HTTP Web 服务器环境。

来源:维基百科

如何做到这一点…

现在,让我们按照给定的步骤在 Tomcat 上运行应用程序:

  1. 首先,你需要从tomcat.apache.org/download-80.cgi下载 Tomcat。

  2. 下载的文件将是一个压缩文件,你可以使用以下方法提取它:

tar xvzf apache-tomcat-8.0.9.tar.gz
  1. 接下来,你需要将其从下载文件夹移动到正确的位置,在:
mv apache-tomcat-8.0.9 /opt/tomcat
  1. 你还需要检查你的系统上是否已设置 JDK。你可以通过输入以下命令来完成:
java -version
  1. 如果你看到“The program 'java' can be found in the following packages:”,这意味着你需要安装 JDK。你可以使用以下方法完成:
sudo apt-get install openjdk-7-jdk
  1. 然后,将以下行添加到.bashrc文件的末尾:
export JAVA_HOME=/usr/lib/jvm/java-7-openjdk-amd64
export CATALINA_HOME=/opt/tomcat
  1. 简单保存并退出.bashrc,然后通过运行以下命令使更改生效:
. ~/.bashrc
  1. Tomcat 和 Java 现在应该已安装并配置在您的服务器上。要激活 Tomcat,请运行以下脚本:
$CATALINA_HOME/bin/startup.sh

你应该得到以下类似的结果:

Using CATALINA_BASE: /opt/tomcat
Using CATALINA_HOME: /opt/tomcat
Using CATALINA_TMPDIR: /opt/tomcat/temp
Using JRE_HOME: /usr/lib/jvm/java-7-openjdk-amd64/
Using CLASSPATH: /opt/tomcat/bin/bootstrap.jar:/opt/tomcat/bin/tomcat-juli.jar
Tomcat started.
  1. 打开http://127.0.0.1:8080以检查它是否正常工作。

  2. 现在,你需要 IntelliJ IDEA 的终极版才能在 IntelliJ 中使用 Tomcat;社区版不提供对 Java EE 应用程序的支持。

  3. 为了运行应用程序,我们需要相应的 WAR 文件进行部署,你只需在终端中添加以下行即可完成:

gradle war
  1. 你需要转到“运行”|“编辑配置”并添加 Tomcat:

  1. 现在,如果你转到你的 localhost 服务器,你可以在那里看到托管的应用程序。

关于 Windows 上 Tomcat 安装的说明,请参阅www.ntu.edu.sg/home/ehchua/programming/howto/Tomcat_HowTo.html

为构建 RESTful 服务设置依赖项

在这个菜谱中,我们将为开发 RESTful 服务打下基础。我们将了解如何设置依赖关系并运行我们的第一个 SpringBoot Web 应用程序。SpringBoot 为 Kotlin 提供了极大的支持,这使得使用 Kotlin 变得容易。所以,让我们开始吧。

准备工作

我们将使用 IntelliJ IDEA 和 Gradle 构建系统。如果你没有这些,你可以从www.jetbrains.com/idea/获取。

如何操作...

让我们按照给定的步骤来设置构建 RESTful 服务的依赖项:

  1. 首先,我们将在 IntelliJ IDE 中创建一个新的项目。我们将使用 Gradle 构建系统来维护依赖项,因此创建一个Gradle项目:

图片

  1. 当你创建了项目后,只需将以下行添加到你的build.gradle文件中。这些代码行包含我们将需要来开发 Web 应用程序的 Spring-boot 依赖项:
buildscript {
    ext.kotlin_version = '1.1.60' // Required for Kotlin integration
    ext.spring_boot_version = '1.5.4.RELEASE'
    repositories {
        jcenter()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // Required for Kotlin integration
        classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version" // See https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin
        classpath "org.springframework.boot:spring-boot-gradle-plugin:$spring_boot_version"
    }
}

apply plugin: 'kotlin' // Required for Kotlin integration
apply plugin: "kotlin-spring" // See https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin
apply plugin: 'org.springframework.boot'

jar {
    baseName = 'gs-rest-service'
    version = '0.1.0'
}
sourceSets {
    main.java.srcDirs += 'src/main/kotlin'
}

repositories {
    jcenter()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" // Required for Kotlin integration
    compile 'org.springframework.boot:spring-boot-starter-web'
    testCompile('org.springframework.boot:spring-boot-starter-test')
}
  1. 现在让我们在以下目录结构中创建一个App.kt文件:

图片

重要的是要保持App.kt文件在一个包中(我们使用了college包)。否则,你将得到一个错误,如下所示:

** WARNING ** : Your ApplicationContext is unlikely to start due to a `@ComponentScan` of the default package.

出现这个错误的原因是,如果你没有包含包声明,它将认为它是一个“默认包”,这是不推荐的,应该避免。

  1. 现在,让我们尝试运行App.kt类。我们将放置以下代码来测试它是否正在运行:
@SpringBootApplication
open class App {
}

fun main(args: Array<String>) {
    SpringApplication.run(App::class.*java*, *args)
}
  1. 现在运行项目;如果一切顺利,你将在最后看到以下行:
Started AppKt in 5.875 seconds (JVM running for 6.445)
  1. 我们现在已经在我们的嵌入式 Tomcat 服务器上运行了我们的应用程序。如果你访问http://localhost:8080,你将看到一个如下所示的错误:

图片

  1. 前面的错误是404 错误,原因是我们没有告诉应用程序当用户在/路径上时应该做什么。

如何创建 REST 控制器

在之前的菜谱中,我们学习了如何为创建 RESTful 服务设置依赖项。最后,我们在http://localhost:8080端点上启动了我们的后端,但得到了404 错误,因为我们的应用程序没有配置为处理该路径(/)的请求。我们将从这个点开始,学习如何创建 REST 控制器。让我们开始吧!

准备工作

我们将使用 IntelliJ IDE 进行编码。关于环境的设置,请参考之前的菜谱。你还可以在gitlab.com/aanandshekharroy/kotlin-webservices的仓库中找到源代码。

如何操作...

在这个菜谱中,我们将创建一个 REST 控制器,它会为我们提供关于大学学生信息。我们将使用一个内存数据库,用列表来简化事物:

  1. 让我们首先创建一个具有姓名和学号属性的Student类:
package college

class Student() {
    lateinit var roll_number: String
    lateinit var name: String
    constructor(
            roll_number: String,
            name: String): this() {
        this.roll_number = roll_number
        this.name = name
    }
}
  1. 接下来,我们将创建StudentDatabase端点,它将作为应用程序的数据库:
@Component
class StudentDatabase {
    private val students = mutableListOf<Student>()
}

注意,我们已经用@Component注解了StudentDatabase类,这意味着它的生命周期将由 Spring 控制(因为我们希望它充当我们应用程序的数据库)。

  1. 我们还需要一个@PostConstruct注解,因为这是一个内存数据库,当应用程序关闭时会被销毁。因此,我们希望在应用程序启动时有一个填充的数据库。所以我们将创建一个init方法,在启动时将一些项目添加到“数据库”中:
@PostConstruct
private fun init() {
    students.add(Student("2013001","Aanand Shekhar Roy"))
    students.add(Student("2013165","Rashi Karanpuria"))
}
  1. 现在,我们将创建一些其他方法,帮助我们处理数据库:

    • getStudent:获取我们数据库中现有学生的列表:
fun getStudents()=students
    • addStudent:这个方法将学生添加到我们的数据库中:
fun addStudent(student: Student): Boolean {
    students.add(student)
    return true
}
  1. 现在,让我们将这个数据库投入使用。我们将创建一个 REST 控制器来处理请求。我们将创建一个StudentController并使用@RestController注解。使用@RestController很简单,这是创建 MVC RESTful Web 服务的首选方法。

  2. 一旦创建,我们需要通过 Spring 依赖注入提供我们的数据库,为此我们需要@Autowired注解。以下是我们的StudentController的样子:

@RestController
class StudentController {
    @Autowired
    private lateinit var database: StudentDatabase
}
  1. 现在我们将设置响应到/路径。我们将显示数据库中的学生列表。为此,我们将简单地创建一个列出学生的方法。我们需要用@RequestMapping注解它,并提供路径和请求方法(GET、POST 等)等参数:
@RequestMapping("", method = arrayOf(RequestMethod.GET))
fun students() = database.getStudents()
  1. 这就是我们现在控制器的样子。它是一个简单的 REST 控制器:
package college

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RestController

@RestController
class StudentController {
    @Autowired
    private lateinit var database: StudentDatabase
    @RequestMapping("", method = arrayOf(RequestMethod.GET))
    fun students() = database.getStudents()
}
  1. 现在当你重启服务器并访问http://localhost:8080时,我们将看到以下响应:

图片

如您所见,Spring 足够智能,能够以 JSON 格式提供响应,这使得设计 API 变得容易。

  1. 现在,让我们尝试创建另一个端点,它将根据学号获取学生的详细信息:
@GetMapping("/student/{roll_number}")
fun studentWithRollNumber( @PathVariable("roll_number")  roll_number:String) =
    database.getStudentWithRollNumber(roll_number)
  1. 现在,如果你尝试访问http://localhost:8080/student/2013001端点,你将看到以下输出:
{"roll_number":"2013001","name":"Aanand Shekhar Roy"}
  1. 接下来,我们将尝试将学生添加到数据库中。我们将通过POST方法来完成:
@RequestMapping("/add", method = arrayOf(RequestMethod.POST))
fun addStudent(@RequestBody student: Student) =
        if (database.addStudent(student)) student
        else throw Exception("Something went wrong")

还有更多……

到目前为止,我们的服务器一直依赖于 IDE。我们肯定希望让它独立于 IDE。多亏了 Gradle,只需以下步骤就可以轻松创建一个可运行的 JAR:

./gradlew clean bootRepackage

上述命令是平台无关的,并使用 Gradle 构建系统构建应用程序。现在,你只需输入提到的命令来运行它:

java -jar build/libs/gs-rest-service-0.1.0.jar 

你可以像之前一样看到以下输出:

Started AppKt in 4.858 seconds (JVM running for 5.548)

这意味着你的服务器正在成功运行。

创建 Spring Boot 的应用程序类

SpringApplication类用于引导我们的应用程序。我们在之前的菜谱中使用了它;我们将在这个菜谱中看到如何为 Spring Boot 创建Application类。

准备工作

我们将使用 IntelliJ IDE 进行编码。为了设置环境,请阅读之前的菜谱,特别是设置构建 RESTful 服务的依赖项菜谱。

如何做到这一点...

如果你之前使用过 Spring Boot,你一定熟悉在主类中使用@Configuration@EnableAutoConfiguration@ComponentScan。这些被使用得如此频繁,以至于 Spring Boot 提供了一个方便的@SpringBootApplication替代方案。Spring Boot 会查找public static main方法,我们将使用Application类外部的顶级函数。

如果你注意到了,在设置依赖项时,我们使用了kotlin-spring插件,因此我们不需要将Application类设置为公开。

这里是一个 Spring Boot 应用程序的示例:

package college

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication

@SpringBootApplication
class Application
fun main(args: Array<String>) {
    SpringApplication.run(Application::class.java, *args)
}

Spring Boot 应用程序执行静态的run()方法,它接受两个参数,并在 Spring 应用程序启动时启动一个自动配置的 Tomcat 网络服务器。

当一切设置完毕后,你可以通过执行以下命令来启动应用程序:

./gradlew bootRun

如果一切顺利,你将在控制台看到以下输出:

这与最后一条消息一起——在 xxx 秒内启动了 AppKt。这意味着你的应用程序已经启动并运行。

为了将其作为一个独立的服务器运行,你需要创建一个 JAR 文件,然后你可以按照以下方式执行:

./gradlew clean bootRepackage

现在,要运行它,你只需输入以下命令:

java -jar build/libs/gs-rest-service-0.1.0.jar 
posted @ 2025-10-09 13:23  绝不原创的飞龙  阅读(7)  评论(0)    收藏  举报