AndroidStudio3-和-Kotlin-学习手册-全-

AndroidStudio3 和 Kotlin 学习手册(全)

原文:Learn Android Studio 3 with Kotlin

协议:CC BY-NC-SA 4.0

一、Kotlin 入门

我们将介绍的内容:

  • Kotlin 语简介

  • 如何得到 Kotlin

  • 在 macOS、Windows 和 Linux 上安装 Kotlin

  • 在命令行中运行 Kotlin 程序

  • 在 IntelliJ IDEA 中创建和运行项目

本章介绍了 Kotlin 语言,并详细介绍了如何建立一个开发环境。你可以找到如何在 macOS、Windows 和 Linux 上安装 Kotlin 的说明。您还可以找到关于如何使用简单的命令行安装 Kotlin 环境的说明。每个开发人员都被某种设置所吸引,您的开发人员也不例外。这是我在整本书中使用的设置:

  • IntelliJ 2018 运行在 macOS 上(高 Sierra)。我在第 1 到 7 章中一直使用这个

  • macOS 上的 Android Studio 3(高 Siera)。我在这本书的其余部分使用了这个

你不需要完全按照我的设置来。我们已经尽力确保本书中的说明在 Linux 和 Windows 中的效果和在 macOS 中一样好。还有,我说的 Linux,并不是指 Linux 的所有发行版。事实是,我只在 Lubuntu 17 中测试了这些代码。为什么呢?因为那是我最熟悉的 Linux 发行版。我相信本书的大多数读者(使用 Linux 的)也会熟悉这个 Linux 发行版(或者它的任何近亲)。

Android Studio 3 和 IntelliJ 可以在 Windows 7、8 和 10 (32 位和 64 位)上运行,但我只测试了 Windows 10 64 位上的练习——这是我唯一可以使用的机器;我相信大多数使用 Windows 的读者也使用这种设置。

最后,让我们讨论一下 JDK 版本。在撰写本文时,JDK 10 号正处于早期访问阶段。因此,JDK 版本的选择是 8 或 9(因为 JDK 7 在 2015 年的某个时候结束了它的生命)。我选择了 9 分——没有特别的原因,我认为 8 分也可以。

关于 Kotlin

Kotlin 是一种针对 Java 平台的新语言;它的程序运行在 JVM (Java 虚拟机)上,这使它与 Groovy、Scala、Jython 和 Clojure 等语言齐名。

Kotlin 来自 JetBrains,它是 IntelliJ、PyCharm、WebStorm、ReSharper 和其他优秀开发工具的创造者。2011 年,JetBrains 推出了 Kotlin 第二年,他们在 Apache 2 许可下开源了 Kotlin。在 Google I/O 2017 上,Google 宣布了在 Android 平台上对 Kotlin 的一流支持。如果你想知道 Kotlin 这个名字是从哪里来的,那是圣彼得堡附近的一个岛屿的名字,Kotlin 团队的大多数成员都在那里。据 JetBrains 的安德烈·布雷斯拉夫(Andrey Breslav)介绍,Kotlin 是以一个岛屿命名的,就像爪哇是以印度尼西亚的爪哇岛命名的一样。然而,您可能记得 Java 语言的历史中提到它是以咖啡而不是岛命名的。

Kotlin 作为一种语言有许多特征和能力,我们有这本书的整个第一部分来探索这些,但这里有一些事情使它变得有趣。

  • 和 Java 一样,是面向对象的。所以,你在 Java 的 OOP 和设计模式上投入的所有时间都不会浪费。Kotlin 类、接口和泛型的外观和行为与 Java 非常相似。这绝对是一个优势,因为与其他 JVM 语言(例如 Scala)不同,Kotlin 看起来并不太陌生。它不会疏远 Java 程序员;相反,这让他们可以发挥自己的优势。

  • 静态强类型。Kotlin 与 Java 共享的另一个领域是类型系统。它还使用静态和强类型。然而,与 Java 不同,在使用变量之前,不必总是声明变量的类型。Kotlin 使用式推理

  • 不如 Java 隆重。我们不必(总是)写一个类;顶级函数也可以。我们不需要为数据对象显式地编写 getters 和 settersKotlin 中有一些语言特性,这使得我们可以抛弃这种锅炉板代码。此外,用 Kotlin 编写代码的自然方式阻止我们将 null 赋给变量。如果你想显式地允许一个值为 null ,你必须以一种深思熟虑的方式去做。

  • 这是一种 函数式语言。函数不仅仅是命名的语句集合;你可以在任何可能用到变量的地方使用它们。您可以将参数输入中的函数传递给其他函数,甚至可以从其他函数返回函数。这种编码方式允许不同的抽象方式。

  • 与 Java 的互操作性。Kotlin 可以使用 Java 库,您也可以从 Java 程序中使用它。这降低了 Kotlin 的准入门槛;与 Java 的互操作性使得决定使用 Kotlin 启动一个新项目变得不那么困难。

在你的下一个项目中使用 Kotlin 有很多理由,但也有反对的理由。我们不会列出在你的下一个项目中为什么应该或者不应该使用 Kotlin 的利弊;但是我将讨论一个为什么我会建议你在狂热之前慢下来并停下来的原因。

还是比较新的。一些人确信,它正在接近“膨胀的期望的顶峰”,并将很快进入“幻灭的低谷”。他们的主要论点是,如果你现在押注于 Kotlin,你将背负着学习曲线问题,你将有义务维护代码库——即使 Kotlin 消失在一股烟雾中。换句话说,你可能会把它当作技术债务。

收养 Kotlin 也要付出一些代价。你必须训练你的团队如何使用它。不管你的团队有多有经验,他们肯定会在过程中降低速度——这是一个项目管理问题。此外,因为 Kotlin 是新的,所以还没有“有效的 Kotlin”的指导文章,而 Java 程序员总是有他们的“有效的 Java”

这将归结为你的赌注。如果你打赌 Kotlin 会坚持到底,而不是在黑暗中悄悄消失,那么这个赌注就有回报了。如果你赌错了,那么你就走上了维护一门废弃语言的代码库的艰难道路——技术债务。要么这样,要么你把它重写回 Java。

谷歌已经在 Android Studio 中正式支持该语言,越来越多的开发者也加入了这一行列。收养率正在上升。这些都是好迹象,表明 Kotlin 不会悄无声息地倒下,实际上可能会坚持到底。另外,这是一种很酷的语言。

注意

“膨胀的期望的顶峰”和“幻灭的低谷”是“炒作周期”的一部分。炒作周期是由美国研究、咨询和信息技术公司 Gartner 开发和使用的品牌图形表示,用于表示特定技术的成熟、采用和社会应用。可以在 https://gtnr.it/cycleofhype 了解更多。

让我们继续为自己构建一个开发环境。

安装 Java SDK

在使用 Kotlin 之前,我们需要安装 JDK。如果您已经安装了 Java 开发工具包,那么您可以跳过这一节,跳到下一节(安装 Kotlin)。JDK 安装程序可用于 Windows、Linux 和 macOS。你可以从甲骨文网站 http://bit.ly/java9download 下载目前稳定的版本。 1

图 1-1 显示了甲骨文 JDK 的下载页面。选择适合您平台的安装程序,然后单击“接受许可协议”继续。

img/463887_1_En_1_Fig1_HTML.jpg

图 1-1

Oracle JDK 下载页面

在 macOS 上安装

要在 macOS 上安装 JDK,双击下载的 dmg 文件,按照提示进行操作。安装程序负责更新系统路径,因此安装后您不需要执行任何进一步的操作。

当你完成安装后,你可以通过启动“Terminal.app”并尝试Java命令来测试 JDK 是否已经安装好(列表 1-1 )。

$ java –version
$ javac –version

Listing 1-1Test the JDK tools on a macOS Terminal

如果终端输出如图 1-2 所示的javajavac版本,你就知道你已经安装了 JDK,没有问题。

img/463887_1_En_1_Fig2_HTML.jpg

图 1-2

终端上的 java 和 javac . app

在 Windows 10 上安装

可以在 Windows 7/8/10 (32 位和 64 位)安装 Android Studio 3;但出于本书的目的,我只用了 Windows 10 64 位。

要在 Windows 上安装 JDK,请双击下载的压缩文件,然后按照提示进行操作。与 macOS 不同,您必须在设置后执行额外的配置。你需要(1)在你的系统路径中包含 Java/bin ,( 2)在 Windows 的环境变量中包含一个类路径定义。表格 1-1 将带您了解如何做到这一点的步骤。

表 1-1

Windows 中的 JDK 配置

| one | 将 JAVA_HOME/bin 包含到系统路径中 | 1.点击**开始** ➤ **控制面板** ➤ **系统**2.点击**高级** ➤ **环境变量。**有两个变量框,上面的框写着“用户变量”,下面的框写着“系统变量”,系统`PATH`将在“系统变量”框中。3.将 bin 文件夹的位置添加到系统变量`PATH`中。4.典型的路径变量如下所示:`C:\WINDOWS\system32;C:\WINDOWS;C:\Program Files\Java\jdk-9\bin;` | | Two | 在 Windows *环境变量*中创建一个*类路径*定义 | 当**环境变量**窗口仍然打开时,点击“用户变量”部分的“新建”按钮。将弹出另一个对话框,其中有两个文本框,允许您添加一个新变量。使用下面的值填充文本框。1 .名称 2.价值➤ `C:\WINDOWS\system32;C:\WINDOWS;C:\Program Files\Java\jdk-9\jre\lib\rt.jar;` |

关闭环境变量窗口,得到一个 cmd 窗口,这样我们可以测试我们的更改是否已经生效。当 cmd 窗口打开时,键入如清单 [1-2 所示的命令。

C:\Users\yourname>java –version
C:\Users\yourname>javac –version

Listing 1-2Test the JDK tools on a Windows cmd shell

如果 cmd shell 向您显示了 java 和 javac 的版本,那么您已经成功地安装和配置了 JDK。另一方面,如果您看到一条错误消息(例如,“错误的命令或文件名”),这意味着 JAVA_HOME\bin 仍然不是系统路径的一部分。您应该重新访问表 1-1 并重新检查您的条目,然后重新测试。

在 Linux 上安装

如果您是 Linux 用户,您可能已经在下载中看到了 tar ball 和 rpm 选项,您可以像在您的 Linux 平台上安装任何其他软件一样使用和安装它,或者您可以从存储库中安装 JDK(参见清单 1-3 )。本指令适用于 Debian 及其衍生工具(如 Ubuntu、Mint 等。).

sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get install oracle-java9-installer
sudo update-alternatives --config java

Listing 1-3Installing the JDK in Ubuntu Using a PPA

下载完成后,您可以通过从命令行试用javajavac工具来测试安装(参见清单 1-4 )。打开你喜欢的终端模拟器(如 xterm、terminator、gnome-terminal、lxterminal 等)。).

$ java –version
$ javac –version

Listing 1-4Test the JDK Tools on Linux

如果安装成功,您应该能够在您的系统中看到 java 和 javac 的版本。一旦 JDK 建成并开始运行,我们现在可以得到 Kotlin。

安装 Kotlin

Kotlin 编码有几种入门方法。你可以使用在线 IDE,这是最快的,因为它不需要你安装任何东西。您也可以尝试下载一个带有 Kotlin 插件的 IDE(例如 IntelliJ、Android Studio 或 Eclipse)。最后,您可以下载 Kotlin 的命令行工具。如果你不想安装一个完整的 IDE,而只是简单地使用你最喜欢的编辑器,你当然可以使用命令行工具。我们不会探究这些选项中的每一个,但是我们将看看命令行工具和 IntelliJ。

注意

这本书是关于 Android Studio 的,所以你可能想知道为什么我们不使用 Android Studio 来试用 Kotlin。这是因为这本书的这一部分仅仅是关于 Kotlin 的,而不是关于 Android 编程的。我认为,当我们做一些编码练习时,最好更多地关注语言,不要被 Android 特定的主题所阻碍。Android Studio 无论如何都是基于 IntelliJ 的,所以我们在本书这一部分学习的任何 IDE 技术在我们学习第二部分时都应该可以很好地继续下去。

安装命令行工具

即使您选择命令行工具,也有几种安装方法可供选择。我们可以通过(1)下载压缩文件来安装它;(2)如果你的操作系统和工具支持,使用 SDKMAN 或者(3)如果你在 macOS 上,使用自制软件或 MacPorts。你只需要选择这些方法中你最习惯的一种,然后去做。

自制或 macports

如果你在 macOS 上并且已经使用了brewport,请参见清单 1-5 或 1-6 中获取 Kotlin 的终端命令。

$ sudo port install kotlin

Listing 1-6Install Kotlin Using MacPorts

$ brew update
$ brew install kotlin

Listing 1-5Install Kotlin Using HomeBrew

使用压缩安装程序

如果你去 Kotlin 网站, http://kotlinglang.org 然后“学习”➤“教程”《➤》入门“➤”使用命令行编译器”,你会发现一个网页 2 看起来可能如图 1-3 所示。压缩后的安装程序可以通过链接“GitHub releases”下载(如图 1-3 )。

img/463887_1_En_1_Fig3_HTML.jpg

图 1-3

Kotlin 命令行编译器页面

链接应该会把你带到 JetBrains/Kotlin3的 GitHub 页面(图 1-4 )。在撰写本文时,Kotlin 的版本是 1 . 2 . 10;当你读到这篇文章的时候,它可能是一个不同的版本,但是只要下载最新的稳定版本。

img/463887_1_En_1_Fig4_HTML.jpg

图 1-4

安装程序压缩文件的 GitHub 页面

下载完成后,解压缩安装程序文件,并将其放在系统中的某个位置,最好是您拥有读、写和执行权限的目录。文件应该解压到名为“kotlinc”的文件夹中。接下来要做的是将 kotlinc/bin 文件夹添加到系统路径变量中。下面几节将演示如何在 macOS、Linux 和 Windows 上做到这一点。

macOS 和 Linux

将下载的压缩文件复制到您的主目录,并在那里解压缩。清单 1-7 显示了该命令。

$ cd ~
$ unzip ~/kotlin-compiler-1.2.10.zip

Listing 1-7Unzip Kotlin Installer

注意

默认情况下,解压缩命令在 macOS 中是可用的,但是对于 Linux 系统,您可能必须首先从存储库中获取它。清单 1-8 显示了如何从存储库中提取它的命令。

$ sudo apt get update
$ sudo apt-get install unzip

Listing 1-8Getting the Unzip Tool

安装文件应该解压到一个名为“kotlinc”的文件夹中,如图 1-5 所示。

img/463887_1_En_1_Fig5_HTML.jpg

图 1-5

解压缩 Kotlin 安装程序

在使用命令行工具之前,我们需要将“kotlinc/bin”文件夹添加到系统路径变量中,如清单 1-9 所示。

$ export PATH=~/kotlinc/bin:$PATH

Listing 1-9Adding kotlinc/bin to the System Path

按下回车键,kotlinc命令现在应该可以工作了。您可以将清单 1-9 中所示的行添加到您的登录脚本中,这样每次打开终端窗口时都可以使用 Kotlin 工具。

Windows 10

将 Kotlin 安装程序的压缩文件复制到您的主目录,并在那里解压缩。使用你最喜欢的存档工具解压。它应该解压到下面的文件夹:C:\Users\yourname\kotlinc。在 kotlinc 文件夹中是 bin 文件夹,它包含了我们需要用于编译的各种脚本和批处理文件。这个 bin 文件夹就是我们需要添加的 Windows 系统路径。

要将 kotlinc\bin 文件夹添加到系统路径,点击 Windows 开始按钮➤ 控制面板系统。一旦系统对话框打开,点击高级环境变量。变量有两个框;上框显示“用户变量”,下框显示“系统变量”。系统PATH将出现在“系统变量”框中。追加kotlinc\binPATH变量。关闭系统对话框以保存更改。

使用 SDKMAN

SDKMAN 可以在 macOS、Linux、Cygwin (Windows)、FreeBSD 和其他 UNIX 系统上使用。如果您已经将它作为工具链的一部分,那么您可以使用它来获得 Kotlin 编译器。如果你还没有 SDKMAN,安装起来很简单。参见清单 1-10 安装 SDKMAN。

重要的

在从命令行安装 SDKMAN 之前,您需要获得curl工具。如果您还没有它,请使用您的平台软件包管理器来获取curl

$ curl -s "https://get.sdkman.io" | bash

Listing 1-10Installing SDKMAN From the Command Line

按照屏幕上的说明完成安装。您需要关闭当前的终端窗口并启动另一个窗口,因为 SDKMAN 安装程序对登录脚本进行了更改。为了使这些更改生效,您需要打开一个新的终端窗口。完成后,我们现在可以安装 kotlin 了。安装命令见清单 1-11 。

$ sdk install kotlin

Listing 1-11Installing Kotlin via SDKMAN

使用命令行工具编码

无论您选择哪种方式来安装命令行工具,现在您都应该已经有了一个可以工作的 Kotlin 编译器。要尝试一下,打开一个终端窗口,输入命令kotlinc。这将改变你的终端提示为三个 v 形(大于符号);参见清单 1-12 。

$ kotlinc
Welcome to Kotlin version 1.2.10 (JRE 9.0.1+11)
Type :help for help, :quit for quit
>>>

Listing 1-12Kotlin REPL

这是 Kotlin REPL——读取、评估、打印、循环的缩写。它以交互方式执行 Kotlin 命令,并立即向您显示结果。如果您以前使用过现代浏览器的控制台功能来输入 JavaScript 命令,这与那个非常相似。REPL 是互动学习语言的好方法。它在开发过程中也非常有用,因为它允许您试验表达式和语句,而不必经历完整的编写-编译-运行周期。您可能想要尝试几个表达式和语句(参见清单 1-13 )。

>>> 5 * 3
15
>>> println("Hello there")
Hello there
for (i in 1 . . 3) {
. . .println(i)
. . .}
1
2
3
>>>

Listing 1-13Simple Expressions

REPL 对于测试语句甚至清单 1-13 中所示的小段代码非常有用,但是如果您需要测试更长的程序,将它编写在程序文件中、编译并运行它会更方便,就像您测试 Java 程序一样。让我们来看看在 Kotlin 是什么样子。

首先,创建一个文件,并将其命名为“hello . kt”——kot Lin 源文件的扩展名为”。kt”。hello.kt 的内容如清单 1-14 所示。

fun main(args: Array<String>) {
  print("Hello")
}

Listing 1-14hello.kt

Kotlin 与 Java 有相似之处,所以清单 1-14 可能看起来很熟悉,但是您也会很快注意到一些明显的东西,所以现在让我们来解决这些问题。

  • 没有类构造。Kotlin 不需要类来执行函数。如清单 1-14 所示的函数称为顶级函数;main 函数很特殊,因为与 Java 的public static void main()一样,Kotlin 的fun main()是应用的入口点。当您运行一个 Kotlin 文件时,运行时将查找这个函数。

  • 函数 main 与语法略有不同。用关键字fun定义函数。类型声明在标识符(args)之后;你会习惯的。此外,Kotlin 没有特殊的语法来定义数组。数组只是 Kotlin 中的类型。

  • 函数 main 没有返回值。其实是有的,只是我们没有写在例子里。函数的默认返回值是Unit;,就像 Java 中的 void。

  • 没有分号 - 冒号。这些已经没有必要了。

下一步是编译和运行我们的源文件。清单 1-15 显示了管理它的命令。

kotlin hello.kt –include-runtime –d hello.jar
java –jar hello.jar

Listing 1-15Compile and Run hello.kt

如果您能够像前面的清单和示例中所示的那样正确地输入所有内容,您应该会在屏幕上看到“Hello World”消息。

如果您觉得命令行工具不适合您,并且您更愿意使用功能更丰富的编程环境,您可以尝试其他 ide,如 Eclipse、IntelliJ 或 Android Studio 3 (AS3)。我们将在本书中介绍 IntelliJ 和 AS3 的安装和使用。下一节将带您了解 IntelliJ IDEA 的设置。

安装 IntelliJ

JetBrains 创建了 Kotlin,所以正如你所想象的,它有很好的支持。Android Studio 基于 JetBrain 的 IntelliJ IDEA CE(社区版);然而,Android Studio 是免费和开放源码软件,由谷歌而不是 JetBrains 维护。

我们甚至可以在本书的第一部分使用 AS3 然而,这样做需要我们同时处理 Kotlin 和 Android 组件。我选择不去做,而是专注于 Kotlin。无论如何,AS3 是基于 IntelliJ 理念的,所以我们在 IntelliJ 上获得的任何知识和技能都将很好地转化为 AS3。

你可以从 JetBrains 网站( http://www.jetbrains.com )下载 IntelliJ IDEA,然后上来工具,下来 IntelliJ IDEA (见图 1-6 )。它将带你到一个页面,在那里你可以为你的平台选择合适的安装程序。您还可以选择是下载“终极版”还是“社区版”。我们将下载社区版。

img/463887_1_En_1_Fig6_HTML.jpg

图 1-6

IntelliJ IDEA 下载页面

如果您使用的是窗口,您需要:

  1. 双击您下载的ideaIC.exe

  2. 按照屏幕提示完成安装

对于 macOS ,执行以下操作:

  1. 双击您下载的 ideaIC.dmg

  2. 将 IntelliJ IDEA 复制到应用文件夹

  3. 运行 IntelliJ IDEA。

对于 Linux ,安装说明如下:

  1. 将 tar.gz 安装程序文件复制到您拥有读取、写入和执行权限的目录中。出于我们的目的,我们将把它复制到主文件夹中(参见清单 1-16 )。

  2. 打开 ideaIC.tar.gz 的包装,如清单 1-17 所示。

$ cd
$ cp ~/Downloads/ideaIC-2017.3.2.tar.gz .

Listing 1-16Copy IntelliJ Installer to Your Home Folder

  1. 将 ideaIC/bin 添加到系统路径中,如清单 1-18 所示。
tar –xzvf ideaIC.tar.gz

Listing 1-17Untar the Installer

  1. 通过运行 idea.sh 脚本启动 IntelliJ IDEA,如清单 1-19 所示。
$ export PATH=~/ideaIC-2017.3.2/bin:$PATH:.

Listing 1-18Add ideaIC/bin to the System Path

$ sh idea.sh

Listing 1-19Start idea.sh

创建项目

如果您还没有启动 IntelliJ,请启动它。它从一个欢迎屏幕开始,如图 1-7 所示。首先,让我们创建一个项目。

img/463887_1_En_1_Fig7_HTML.jpg

图 1-7

欢迎来到 intellij idea

点击“创建新项目”会将我们带到“新项目”窗口(如图 1-8 所示)。选择“Kotlin/JVM ”,然后单击“下一步”按钮。

img/463887_1_En_1_Fig8_HTML.jpg

图 1-8

新的 Kotlin/JVM 项目

这将我们带到“New Project”向导的第二个窗口,在这里我们需要输入一些信息,但是大多数信息已经预先填充了默认条目,我们可以简单地接受默认值。我们确实需要提供“项目名称”,除非您想将您的项目命名为“未命名”(这是项目名称字段的默认值—可能不是一个好主意)。

在图 1-9 中,我使用了“kotlinproject”作为项目名称。我没有更改默认的项目位置,即主文件夹下的“IdeaProjects”。我也没有对“项目 SDK”做任何更改,这是 IntelliJ 在安装时检测到的。要完成项目创建向导,请单击“完成”按钮。

img/463887_1_En_1_Fig9_HTML.jpg

图 1-9

新项目

第一次启动 IntelliJ 时,你会看到“日积月累”窗口(图 1-10 )。提示对于学习 IDE 的功能非常有用,但是我更喜欢它们只在我召唤它们的时候出现,而不是每次启动 IDE 的时候都弹出来。您可以通过取消选中“启动时显示提示”来禁用启动时显示的“每日提示”窗口让我们暂时关闭它。

img/463887_1_En_1_Fig10_HTML.jpg

图 1-10

日积月累

当日积月累对话框关闭时,我们可以更全面地看到我们新创建的项目(图 1-11 )。ide 的左侧显示“项目工具窗口”;它现在没有多少,因为我们还没有创造任何东西。

img/463887_1_En_1_Fig11_HTML.jpg

图 1-11

我们在 IntelliJ 的 Kotlin 项目

项目工具窗口允许我们更改“视图”所有的视图都显示相同的项目,但是每个视图对内容的安排都有所不同。您可以通过单击下拉按钮来更改项目工具窗口的视图(参见图 1-12 )。您应该尝试几种视图来熟悉它们。

img/463887_1_En_1_Fig12_HTML.jpg

图 1-12

项目工具窗口,视图

对于本节的其余部分,我们将使用“项目”视图。这个视图以树状结构显示我们的文件,很像你操作系统中的文件管理器(见图 1-12 )。您可以向下钻取并展开以查看文件夹的内容,如图 1-13 所示。

img/463887_1_En_1_Fig13_HTML.jpg

图 1-13

项目工具窗口。项目视图

“src”文件夹(source 的缩写)是我们放置 Kotlin 源文件的地方。右击 src 文件夹,选择新建➤·Kotlin 文件/类,如图 1-14 所示。

img/463887_1_En_1_Fig14_HTML.jpg

图 1-14

从项目工具窗口新建 Kotlin 文件

我们现在将创建一个 Kotlin 文件,并将其命名为“Hello”;“我们不用写了。kt "扩展名名称字段(见图 1-15)—扩展名将自动为我们添加。确保在对话框的“种类”栏中选择了“文件”选项(参见图 1-15 )。单击“确定”按钮创建文件。

img/463887_1_En_1_Fig15_HTML.jpg

图 1-15

新 Kotlin 文件

当源文件被创建时,你会在项目工具窗口的 src 文件夹下看到它,它也会在主编辑器窗口中被打开(见图 1-16 )。

img/463887_1_En_1_Fig16_HTML.jpg

图 1-16

Hello.kt

IntelliJ 具有出色的代码提示和自动完成功能。当它识别出你正在输入的内容时,它会给你一些建议和提示(见图 1-15 )。一旦您键入了足够多的可能是 Kotlin 关键字或构造的字符模式,IDE 就会提供建议。您可以接受当前建议的选项(在弹出窗口中高亮显示,如图 1-15 所示)或使用鼠标或箭头键选择其他自动完成选项。

清单 1-20 显示了这个例子的完整代码清单。

fun main(args: Array<String>) {
  println("Hello World")
}

Listing 1-20Hello.kt

下一步是运行这个程序;您可以通过调用 IntelliJ 主菜单栏上的 Run 菜单来实现这一点。主菜单栏位于 IDE 的顶部,顶层选项包括文件、编辑、查看和帮助。从主菜单栏中点击运行运行。你会注意到在主运行菜单上有两个运行选项,第一个运行选项是灰色的。选择另一个运行选项,它位于从顶部往下数第四个项目的位置。第一次运行选项是灰色的,因为我们还没有为项目定义任何运行时配置。我们本来可以编辑配置并提供运行时类的名称,但是我们不必这样做。选择第二个运行选项会弹出一个对话框窗口(见图 1-17 ),会要求我们输入当前项目的运行时类的名称。“HelloKt”是我们将选择作为该项目的运行时类的类。

注意

我们的源文件名是“Hello.kt”,但是 Kotlin 编译器不会生成“hello . class”;而是会生成字节码“HelloKt.class”。在处理 Kotlin 类文件时,您应该记住这一点。

img/463887_1_En_1_Fig17_HTML.jpg

图 1-17

运行 Hello.kt

IDE 将把“Hello.kt”编译成“HelloKt.class ”,然后运行。结果将显示在“运行”工具窗口中(参见图 1-18 )。

img/463887_1_En_1_Fig18_HTML.jpg

图 1-18

运行 Hello.kt 的结果

现在我们已经成功运行了一个顶级函数,让我们向应用添加一个类,并编写一个更加面向对象的代码示例版本。要添加一个类,右击项目工具窗口上的“src”文件夹(图 1-19 ,选择新建Kotlin 文件/类

img/463887_1_En_1_Fig19_HTML.jpg

图 1-19

向项目中添加新文件/类

当弹出“新建 Kotlin 文件/类”对话框窗口时,选择“类”(图 1-20);姑且称之为“迎宾”。

img/463887_1_En_1_Fig20_HTML.jpg

图 1-20

新 Kotlin 级

在主编辑器窗口中编辑欢迎类(图 1-21 )。

img/463887_1_En_1_Fig21_HTML.jpg

图 1-21

更大的阶级

然后编辑 Hello.kt 如图 1-22 所示。完成更改后,再次运行“Hello.kt”。从主菜单栏中,运行运行;或者,您可以使用Shift + F10来运行代码。

img/463887_1_En_1_Fig22_HTML.jpg

图 1-22

用更大的类运行 main

图 1-21 显示了我们更新代码的输出。本章的所有编码活动到此结束。现在您可能已经知道,IntelliJ 对 Kotlin 语言有很好的支持;如果你喜欢用不同的编辑器编写 Kotlin 程序,你可以不使用它。但是如果您选择使用它,我们不妨快速浏览一下 IDE,以便更好地使用它。这就是下一部分的内容。

智能集成电路

图 1-23 显示了 IDE 的各个部分。您需要有一个打开的项目,以便在桌面上看到类似的内容。

img/463887_1_En_1_Fig23_HTML.jpg

图 1-23

IntelliJ IDEA IDE

表 1-2 讨论了 IDE 的各个部分,因为它与图 1-22 相关。

表 1-2

IntelliJ IDE

| 主菜单栏 | 在 IDE 中有许多方法可以完成任何任务;您可以使用各种键盘快捷键或上下文菜单,但最全面的导航方式是在主菜单栏上。这个栏位于 IDE 的最顶端。 | | 工具窗口栏 | 工具窗口栏沿着 IDE 窗口的周边运行。它包含激活特定工具窗口所需的各个按钮。 | | 显示/隐藏工具窗口 | 这是查看 IDEA 中各种工具窗口的快捷方式。也可以从主菜单栏**查看** ➤ **工具窗口中查看或隐藏工具窗口。** | | 主编辑器窗口 | 这是最突出的窗口,它拥有最多的屏幕空间。在编辑器窗口中,您可以创建和修改项目文件和源文件。 | | 刀杆 | 工具栏允许您执行各种操作(例如,保存文件、运行应用、打开 AVD 管理器、打开 SDK 管理器、撤销、重做操作等)。). | | 导航栏 | 它允许您导航项目文件。这只是“项目文件”窗口的一个更紧凑的视图。这是一个水平排列的箭头框集合,类似于一些网站上可以找到的面包屑导航。您可以通过导航栏或项目工具窗口打开项目文件。 | | 项目工具窗口 | 显示项目中的文件。如果您想要打开一个特定的文件,在这个窗口中双击该文件,它将在主编辑器窗口中打开。您也可以在此窗口中的项目上使用上下文菜单。上下文菜单允许在 IDE 中完成任务的替代方法(例如,添加类文件、运行代码、调试等)。). |

章节总结

  • Kotlin 是 Android 最新的编程语言,它在 Android Studio 3 上有一流的支持。

  • 有许多方法可以在 macOS、Linux 和 Windows 上安装 Kotlin 命令行编译器和运行时。

  • 各种 ide 都支持 Kotlin 语言;在其中的一些上,你必须得到一个插件,而在一些上,它是开箱即用的。

  • Kotlin 看起来和 Java 很像,但也有区别。

  • IntelliJ 对 Kotlin 有很好的支持——毕竟是 JetBrains 创造了 Kotlin。

在下一章中,我们将了解以下内容:

  • 程序元素(例如,文字、变量、表达式、关键字、运算符等。)—构成我们代码的各种东西

  • 在 Kotlin 中我们可以使用哪些类型的数据

  • Kotlin 中为什么会有可空类型,它首先是什么?

  • 控制结构,以便您可以循环和分支

  • 异常处理和为什么你不必再用 Kotlin 写 try-catch 了(剧透)

二、Kotlin 基础知识

我们将介绍的内容:

  • 程序元素

  • 基本类型

  • 不变

  • 用线串

  • 可空类型

  • 控制结构

  • 异常处理

Kotlin 与 Java 并没有什么不同。虽然它引入了相当多的特性,但您会发现 Kotlin 和 Java 的相似之处多于不同之处。这对 Java 程序员来说是个好消息,因为这意味着 Kotlin 的学习曲线不会太陡。

您需要习惯一些新事物,比如 Kotlin 中的表达式和语句(它们与 Java 完全相反;例如,在 Kotlin 中赋值是语句,但在 Java 中是表达式)。在这一章中,我们将介绍一些 Kotlin 基础知识,这些知识可以作为后续章节的基础知识。

程序元素

当学习一门新的语言时,一门合适的语言,如法语、西班牙语等。,你可能会从词类和支配它们的规则开始。如果我们对一门语言的各个部分是如何组合在一起有一些基本的了解,那么学习这门语言就容易多了。一个 Kotlin 程序包含文字、变量、表达式、关键字和许多其他东西,我们将在这一节探讨其中的一些。

文字

Kotlin 为基本类型(数字、字符、布尔、字符串)提供了文字。

var intLiteral = 5
var doubleLiteral = .02
var stringLiteral = "Hello"
var charLiteral = '1'
var boolLiteral = true

Listing 2-1Literal Examples

在清单 2-1 中,值5, .02, " Hello " , ' 1'和true分别是整数、双精度、字符串、字符和布尔类型的文字。

变量

变量是我们用来操作数据的东西,或者更准确地说,是一个值。值是可以存储、操作、打印、推送或从网络中提取的东西。为了能够处理值,我们需要将它们放入变量中。Kotlin 中的变量是通过使用关键字var后跟类型声明一个标识符来创建的,如语句中所示

var foo: Int

在该语句中,foo是标识符,Int是类型。Kotlin 通过将类型放在标识符的右边来指定类型,并用冒号将其隔开。

既然变量已经声明,我们就可以给它赋值了,就像这样:

foo = 10

然后,在函数中使用它,如下所示:

println(foo)

我们可以在同一行声明和定义变量,就像在 Java 中一样。这里是 var foo 的例子。

var foo: Int = 10
println(foo)

我们仍然可以通过省略 type ( Int)来缩短上面的赋值语句。参见示例代码:

var foo = 10
println(foo)

我们不一定要声明或写出变量的类型;Kotlin 很聪明,当你给变量赋值的时候,它能判断出变量的类型;这叫做式推理。在我们明确告诉 Kotlin 变量类型的时候,注意它在变量名(foo)的右边,而在 Java 中,正好相反,变量类型在标识符的左边。Kotlin 没有遵循将类型放在标识符左边的 Java 约定的原因是,在 Kotlin 中,我们并不总是编写类型

var foo = 10 // compiler knows 10 is an integer literal
var boo = .02 // double literal makes boo a double type

Kotlin 使用另一个关键字来声明变量,即val关键字。用此关键字声明的变量只能在定义它们的执行块中初始化一次。这使得它们实际上是常数。把val想象成 Java 中的final关键字——一旦你把它初始化成一个值,你就不能再改变它,它们是不可变的。虽然使用var创建的变量是可变的,但是它们可以根据您的需要多次更改。

Val 变量的声明和初始化就像 var 变量一样:

val a = 10 // declaration and initialization on the same line

它们也可以在以后声明和初始化,如下所示:

val a: Int
a = 10

请记住,用val关键字声明的变量是最终变量,一旦你将它们初始化为一个值,就不能重新赋值。这里的代码片段不起作用:

val boo = "Hello"
boo = "World" // boo already has a value

如果您认为以后需要更改变量 boo 的值,请将声明从val更改为var

智能 J 型

如果你试图重新分配一个使用 val 关键字声明的变量的值,IntelliJ 会给你足够的视觉提示“val 不能被重新分配”,甚至在你试图编译代码之前。

表达和陈述

表达式是运算符、函数、文字值、变量或常数的组合,并且总是解析为一个值。它也可以是更复杂表达式的一部分。一个语句可以包含表达式,但是就其本身而言,一个语句不能解析为一个值。它不能是其他语句的一部分。它总是其封闭块中的顶级元素。

在很大程度上,您在 Java 中学到的关于表达式和语句的知识在 Kotlin 中也是适用的,但是有一些细微的差别。随着我们的深入,我将指出 Java 和 Kotlin 在语句和表达式方面的区别。这些差异包括:

赋值在 Java 中是表达式,但是在 Kotlin 中是语句。这意味着你不能将赋值操作作为参数传递给像 while 这样的循环语句。见清单 2-2 。

while ((rem = a % b) != 0) {
 a = b
 b = rem
}
println(b)

Listing 2-2Assignment Operation As Argument to While

Kotlin 不让你编译,因为while语句需要一个表达式,而赋值不是表达式。为了让前面的代码示例(清单 2-2 )在 Kotlin 中工作,您必须用另一种方式编写它,如清单 2-3 所示。

var foundGcf = false

while(!foundGcf) {
 rem = a % b
 if (rem != 0) {
 a = b
 b = rem
 }
 else {
 foundGcf = true
 }
}
println(b)

Listing 2-3Using the While Loop in Kotlin

清单 2-3 比您可能习惯的(在 Java 中)要冗长一些,它有更多的字符需要键入,但是代码的意图更清晰易懂。

Kotlin 和 Java 在表达式和语句方面的另一个显著区别是,在 Kotlin 中,大多数控制结构(除了for, do,do/while)是表达式,而在 Java 中是语句。

关键词

关键字是对编译器有特殊意义的保留术语,因此,它们不能用作任何程序元素(如类、变量名、函数名和接口)的标识符。

Kotlin 有硬、软和修饰符关键字。硬关键字总是被解释为关键字,不能真正用作标识符。例如,break, class, continue, do, else, false, while, this, throw, try, super, and when.

软关键字在它们适用的特定上下文中充当保留字;否则,它们可以用作常规标识符。软关键字的一些例子如下:file, finally, get, import, receiver, set, constructor, delegate, get, by, and where.

最后是修饰关键词。这些东西在声明的修饰符列表中充当保留字;否则,它们可以用作标识符。这些事情的一些例子如下:abstract, actual, annotation, companion, enum, final, infix, inline, lateinit, operator, and open.

智能 J 型

如果你使用 IntelliJ,你不必记住关键字列表。如果您不小心将关键字用作标识符,IDE 会给你足够的视觉提示。

空白

和 Java 一样,Kotlin 也是一种标记化语言;空白不重要,可以安全地忽略。您可以编写大量使用空格的代码,比如

fun main(args: Array<String>) {
 println( "Hello")
}

或者你可以用很少的代码来写,就像下面的例子:

fun main(args: Array<String>) {println("Hello")}

不管是哪种方式,编译器都不会在意,所以为了那些可能不幸维护我们代码的人类的利益而写你的代码吧。忘了编译器吧——它根本不在乎空白。使用空格美化代码,使其可读,可能类似于

fun main(args: Array<String>) {
 println("Hello")
}

经营者

像在 Java 和其他编程语言中一样,Kotlin 支持各种各样的操作符和符号,我们可以用它们来表达表达式和语句。表 2-1 显示了其中的一些。

表 2-1

Kotlin 算子和符号

|

运算符或符号

|

什么意思

|
| --- | --- |
| +, -, *, /, % | 这些是常见的数学运算符——它们完全按照你的期望去做。跟 Java 一点区别都没有。但是我们需要注意,星号或星号()也用于将数组传递给 vararg 参数。 |
| = | 等号用于赋值语句(赋值在 Kotlin 中是一个语句,而在 Java 中是一个表达式)。 |
| +=, -=, *=, /=, %= | 这些是增强赋值运算符。+=可以这样使用a = a + 1;的简称a += 1,``-=可以这样使用a -= 1,a = a -1的简称,以此类推。 |
| &&, &#124;&#124;, !逻辑'and''or', 'not'运算符 | 当您需要构造复杂的或复合的逻辑操作时,您将使用这些操作符。短路
和* ( &&)的行为与 Java 类似。当其中一个操作数的计算结果为 false 时,将不再计算另一个操作数,整个表达式的计算结果为 false。而逻辑'and'不进行短路评估;可以把它想象成 Java 中的&操作符。短路 or ( &#124;&#124;)的作用与 Java 中的相同。Kotlin 没有单一管道运营商;取而代之的是'or'操作符,它执行逻辑“或”运算而不会短路。 |
| ==, != | 这些是等式运算符。因为 Kotlin 没有基本类型(像 Java 中那样),所以您可以使用这些操作符来比较任何类型,基本类型或其他类型:fun main(args: Array<String>) {``var a = "Hello"``var b = "Hello"``if (a == b) { // this evaluates to true``println("$a is equal to $b")``}``}在 Java 中,我们不能像这样使用 double equals 操作符进行对象比较。对象(如字符串)应该使用。如果我们想测试相等性。然而,在 Kotlin,我们不需要担心这些事情。我们使用双等号运算符来比较字符串。Kotlin 在内部将其翻译为调用。equals()方法。 |
| ===, !=== | 引用相等性由=运算(及其反运算)检查。).当且仅当 a 和 b 指向同一个对象时,a === b 的计算结果为 true。例如,var p1 = Person("John")``var p2 = Person("John")``if(p1 === p2) { // false``println("p1 == p2")``}在上面的例子中,p1 和 p2 没有指向同一个对象;因此,三重等于不会评估为真。 |
| <, >, <=, >= | 比较运算符。Kotlin 将这些转换成对compareTo()的调用——没有原始类型,记得吗? |
| [ ]``[,] | 索引访问操作符是一种访问列表元素或 map 值的便捷方式。我们可以使用数组索引来检索条目,而不是使用 Java 风格的get(index)get(key),fun main(args: Array<String>) {``val fruits = listOf("Apple", "Banana", "Orange")``println(fruits.get(2)) // Banana``println(fruits[2]) // Banana``} |

阻碍

通常,你可能需要写一堆陈述,你需要把它们组合在一起。积木让我们可以做到这一点。块词汇符号是一对花括号;他们有时也被称为法国或弯曲的括号。块可以在许多 Kotlin 构造(如类)中找到,如下面的代码:

class Person(val name: String) {

}

当定义接口时,例如

interface Human {
 fun walk()
 fun talk()
}

在函数中,比如

fun main(args: Array<String>) {
 greet("John")
}

fun greet(name:String) {
 println("Hello $name")
}

在循环结构中,如 while 循环

var counter = 0
while (counter++ != 5) {
 println("counter $counter")
}

当使用 try-catch 构造时

val num = "1"

val ans = try {
 Integer.parseInt(num)
}
catch(e:Exception) {
 e.printStackTrace()
}

以及可能需要对语句进行分组的任何其他控制结构。

评论

注释对编译器没有用;它忽略了它们。但是它们对阅读这些代码的其他人(和你)是有用的。这使它们成为使代码更容易理解的优秀工具,因为您可以在编写代码时使用注释来转储您的思维过程。它阐明并传达了你的意图。写评语有三种方式,它们是:

  1. 单行注释,也称行内注释。这些是用两个正斜杠写的。编译器将忽略斜线右侧的所有内容,直到该行结束,参见示例:

    // This statement will be ignored
    var a = 0 // so will this line
    
    
  2. 多行注释,也称 C 风格注释。之所以这样称呼它们,是因为它们主要来自 C 语言。如果您的注释跨越多行,这种样式很有用。参见示例:

    /*
     Everything inside the pair of these slashes
     and asterisks will be ignored by the
     compiler
    */
    
    
  3. KDoc 就像 Javadoc 一样,以/**开始,以*/结束。这种形式的注释非常类似于多行注释(如上),但这是用来为 Kotlin 代码提供 API 文档的。清单 2-4 展示了如何使用 KDoc 语法。

    /**
    This is an example documentation using KDoc syntax
    
    @author Ted Hagos
    @constructor
    */
    class Person(val name: String) {
     /**
     This is another KDoc comment
     @return
     */
     fun foo(): Int{
    
     }
    
    }
    
    Listing 2-4KDoc Syntax
    
    

智能 J 型

您可以在 IntelliJ 中注释多行代码,方法是选择要注释的行,并使用键盘快捷键之一来注释掉代码。

在 Windows 和 Linux 中,这些键是:

CTRL + / — comment using //
CTRL + Shift + / — comment using /* */

在 macOS 中,键是:

⌘ + / — comment using //
⌘ + ⌥ + / — comment using /* */

基本类型

Kotlin 有一些基本类型,但它们与 Java 的原始类型不一样,因为 Kotlin 中的所有类型都是对象。它们之所以被称为基本类型,是因为它们的用法非常普遍。这些类型是数字、字符、布尔值、数组和字符串——我们将在本节中讨论它们。

数字和文字常量

有处理数字的内置类型(如表 2-2 所示)。它们可能在运行时被表示为原始值,但是对于所有的意图和目的,它们在程序员看来并不是原始的。它们作为真正的对象出现,具有成员函数和属性。

表 2-2

Kotlin 数字内置型

|

类型

|

位宽

|
| --- | --- |
| 两倍 | Sixty-four |
| 浮动 | Thirty-two |
| 长的 | Sixty-four |
| (同 Internationalorganizations)国际组织 | Thirty-two |
| 短的 | Sixteen |
| 字节 | eight |

Kotlin 处理数字的方式非常接近 Java 处理数字的方式,但是有一些显著的不同。例如,扩大转换不再是隐式的;您需要谨慎地执行转换。

var a = 10L // a is a Long literal, note the L postfix
var b = 20

var a = b // this won't work
var a = b.toLong() // this will work

当整数用作文字常量时,它们会自动成为整数。要声明一个文本,使用 L 后缀,比如

var a = 100 // Int literal
var b = 10L // Long literal

您可以在数字文本中使用下划线,以提高可读性。这个特性是在 Java 7 及其更高版本中引入的。

var oneMillion = 1_000_000
var creditCardNumber = 1234_5678_9012_3456

带有小数位置的文字会自动成为的双精度值。要声明浮点文字,请使用 F 后缀,比如

var a = 3.1416 // Double literal
var b = 2.54 // Float literal

每种数字类型都可以转换成任何一种数字类型。这意味着所有的 Double、Float、Int、Long、Byte、Short 类型都支持以下成员函数:

  • toByte() : Byte

  • toShort() : Short

  • toInt() : Int

  • toLong() : Long

  • toFloat() : Float

  • toDouble() : Double

  • toChar() : Char

特性

kot Lin 中的字符不能直接当作数字。你不能做如下事情:

fun checkForKey(keyCode:Char) {
 if (keyCode == 97) { // won't work, keyCode is not a number
 }
}

字符文字使用单引号创建,比如

var enterKey = 'a'

像在 Java 中一样,您可以使用转义序列,如\t, \b, \n, \r, \", \", \\,\$,如果您需要编码任何其他字符,您可以使用 Unicode 语法(例如\uFF00))。

我们不要忘记字符是 Kotlin 中的对象,所以你可以在它们上面调用成员函数。清单 2-5 显示了演示一些使用场景的片段。

val a = 'a'

println(a.isLowerCase()) // true
println(a.isDigit()) // false
println(a.toUpperCase()) // A

val b: String = a.toString() // converts it to a String

Listing 2-5Member Functions of the Character Type

布尔运算

布尔值由文字truefalse表示。与 Python 或 JavaScript 等其他语言不同,Kotlin 没有 truthy 和 falsy 值的概念。这意味着,对于需要一个布尔类型的构造,你必须提供一个布尔文字、变量或者表达式来解析true或者false

var count = 0

if (count) println("zero") // won't work
if ("") println("empty") // won't work either

数组

Kotlin 没有像 Java 中使用方括号语法创建的数组对象。Kotlin 数组是一个泛型类,它有一个类型参数。我们使用 Kotlin 数组已经有一段时间了,因为前一章中的小代码片段和“Hello World”示例都使用了数组。主函数的参数实际上是一个由字符串组成的数组。让我们再来看看这个主函数,作为复习。

fun main(args:Array<String>) {

}

有几种方法可以创建数组。可以使用arrayOf()arrayOfNulls()函数创建它们,最后,可以使用数组构造函数创建它们。清单 2-6 提供了一些如何使用它们的示例代码。

| -什么 | 我们使用`arrayOfNulls`函数创建了一个包含两个元素的数组。 | | ➋ | 我们可以给数组的特定元素赋值。我们只需要使用元素的索引来指定元素在数组中的位置。访问数组元素的语法与 Java 中的相同。 | | ➌ | 我们可以使用 for 循环来遍历数组的内容。在这个例子中,我们使用了`indices`来访问数组的元素。 | | -你好 | 这是访问数组元素的更直接的方式。一个数组对象有一个迭代器,所以我们可以使用迭代器直接访问数组元素。 | | ➎ | 这使用`arrayOf()`函数创建了一个整型数组。 | | ➏ | 这个例子使用了`forEach`函数来遍历数组的元素。使用`forEach`函数被认为更习惯(也更有效)。 | | -好的 | 这将使用 ArrayList(arrayWords)创建一个数组。列表 arrayWords 是通过调用字符串的`split()`成员函数创建的。 |
fun main(args: Array<String>) {

 var emptyArray = arrayOfNulls<String>(2) ➊
 emptyArray[0] = "Hello" ➋
 emptyArray[1] = "World"

 for (i in emptyArray.indices) println(emptyArray[i]) ➌

 for (i in emptyArray) println(i) ➍

 var arrayOfInts = arrayOf(1,2,3,4,5,6) ➎
 arrayOfInts.forEach { e -> println(e) } ➏

 var arrayWords = "The quick brown fox".split(" ").toTypedArray() ➐
 arrayWords.forEach { item -> println(item) }

}

Listing 2-6Working With the Array Type

字符串和字符串模板

我们所学的关于 Java 字符串的很多知识在 Kotlin 中仍然适用;因此,这一节将会很短。

创建字符串最简单的方法是使用转义字符串——转义字符串实际上是我们从 Java 中知道的那种字符串。这些字符串可能包含转义字符,如\n, \t, \b,等。请参见下面的代码片段。

var str: String = "Hello World\n"

Kotlin 有另一种叫做原始弦的弦。使用三重引号分隔符创建原始字符串。它们可能不包含转义序列,但可以包含新行,比如

var rawStr = """Amy Pond, there's something you'd
 better understand about me 'cause it's important,
 and one day your life may depend on it:
 I am definitely a mad man with a box!
 """

关于 Kotlin 琴弦,我们还需要知道以下几件事:

  1. 它们有迭代器,所以我们可以使用循环 :

    val str = "The quick brown fox"
    for (i in str) println(i)
    
    

    中的遍历字符

  2. 它的元素可以被索引操作符(str[elem]),访问,非常像数组

    println(str2)) // returns 'e'
    
    
  3. 我们不能再简单地通过添加一个空的字符串文字来将数字(或其他任何东西)转换成字符串:

    var strNum = 10 + "" // this won't work anymore
    var strNum = 10.toString() // we have to explicitly convert now
    
    

在 Kotlin 中我们仍然可以使用String.formatSystem.out.printf;毕竟,我们可以在 Kotlin 内部使用 Java 代码。仍然可以编写类似清单 [2-7 中所示代码片段的程序。

var name = "John Doe"
var email = "john.doe@gmail.com"
var phone = "(01)777-1234"

var concat = String.format("name: %s | email: %s | phone: %s", name, email, phone)
println(concat)
// prints
// name: John Doe | email: john.doe@gmail.com | phone: (01)777-1234

Listing 2-7Using String.format and printf

在 Kotlin 中编写字符串的首选方式是使用字符串模板,比如

var concat = "name: $name | email: $email | phone: $phone"
println(concat)
// prints
// name: John Doe | email: john.doe@gmail.com | phone: (01)777-1234

Kotlin 字符串可能包含模板表达式。这些是被评估的代码片段。评估的结果被插入(连接)到字符串中。模板表达式以美元符号($)开头,后跟一个表达式。参见清单 2-8 中的示例。

| -什么 | 显示模板字符串的基本用法。模板表达式是通过使用后面紧跟一个标识符的$符号创建的。对标识符的值进行计算、解析,最后插入到声明模板表达式的字符串体中。 | | ➋ | 在这个例子中,`name.length`用花括号括起来。这是因为$符号是右关联的——它将计算紧挨着它右边的表达式。这在我们的情况下是行不通的,因为我们不想对变量`name`求值;相反,我们想要解决的是`name.length`——因此,需要用花括号将它括起来。 | | ➌ | 我们不局限于简单的变量。我们甚至可以在模板表达式中编写函数。 |
fun main(args:Array<String>) {
 var name = "John Doe"

 println("Hello $name") ➊
 println("The name '$name' is ${name.length} characters long") ➋
 println("Hello ${foo()}") ➌
}

fun foo(): String {
 return "Boo"
}

Listing 2-8Using Template Expressions

控制程序流程

默认情况下,程序语句以线性方式依次执行。有些结构会导致程序偏离线性流程。有些可能会导致流程分叉或分支,而其他构造可能会导致程序流程像在循环中一样绕圈。这些结构是本节的主题。

使用 ifs

if 构造的基本形式是

if (expression) statement

其中表达式解析为布尔型。如果表达式为真,则执行该语句;否则,该语句将被忽略,程序控制将流向下一个可执行语句。当您需要执行多条语句时,您可以使用带有 if 结构的块,比如

if (expression) {
 statements
}

让我们看看它在代码中的样子。

val theQuestion = "Doctor who"
val answer = "Theta Sigma"
val correctAnswer = ""

if (answer == correctAnswer) {
 println("You are correct")
}

到目前为止,Kotlin 中的 if 构造的行为与 Java 中的完全一样。它还支持 else ifelse 子句,如以下代码片段所示:

val d = Date()
val c = Calendar.getInstance()
val day = c.get(Calendar.DAY_OF_WEEK)

if (day == 1) {
 println("Today is Sunday")
}
else if (day == 2) {
 println("Today is Monday")
}
else if ( day == 3) {
 println("Today is Tuesday")
}

关于 Kotlin 的 if 的新东西是它是一个表达式,这意味着我们可以这样做

val theQuestion = "Doctor who"
val answer = "Theta Sigma"
val correctAnswer = ""

var message = if (answer == correctAnswer) {
 "You are correct"
}
else{
 "Try again"
}

如果条件为真, if 构造的第一块上的字符串将返回到message变量;否则,第二个块上的字符串将是返回值。我们甚至可以省略块上的花括号,因为块只包含单个语句。

var message = if (answer == correctAnswer) "You are correct" else "Try again"

上面的代码示例可能会让您想起 Java 中的三元运算符。顺便说一句,Kotlin 不支持三进制运算符,但既然你不需要,也不用担心。if 构造是一个表达式,如果您觉得需要编写需要三元运算符的代码,只需遵循前面的代码示例即可。

when 语句

Kotlin 没有一个 switch 语句,但是它有一个构造时的。其形式和结构与开关语句惊人的相似。最简单的形式是,它可以这样实现:

val d = Date()
val c = Calendar.getInstance()
val day = c.get(Calendar.DAY_OF_WEEK)

when (day) {
 1 -> println("Sunday")
 2 -> println("Monday")
 3 -> println("Tuesday")
 4 -> println("Wednesday")
}

对所有分支依次匹配自变量(变量day)直到遇到匹配;注意,与 switch 语句不同,当找到匹配时,它不会流过或级联到下一个分支——因此,我们不需要放置 break 语句。

when 构造也可以用作表达式,这样使用时,每个分支都成为表达式的返回值。请参见代码示例:

val d = Date()
val c = Calendar.getInstance()
val day = c.get(Calendar.DAY_OF_WEEK)

var dayOfweek = when (day) {
 1 -> "Sunday"
 2 -> "Monday"
 3 -> "Tuesday"
 4 -> "Wednesday"
 else -> "Unknown"
}

只要记住当用作表达式时包含 else 子句即可。编译器会彻底检查所有可能的路径,并且需要做到详尽无遗,这就是为什么 else 子句成为一个要求的原因。

你不局限于数字文字;您可以为分支使用多种类型,如清单 2-9 所示。

| -什么 | `readLine()`从控制台读取输入。现在不要担心问号;我们将在接下来的章节中讨论这个问题。 | | ➋ | 分支条件可以用逗号组合。 | | ➌ | 我们可以检查它是某个系列还是某个系列的成员。 | | -你好 | 当用作表达式时*需要 *else* 子句。* |
fun main(args: Array<String>) {

 print("What is the answer to life? ")
 var response:Int? = readLine()?.toInt() ➊

 val message = when(response){
 42 -> "So long, and thanks for the all fish"
 43, 44, 45 -> "either 43,44 or 45" ➋
 in 46 .. 100 -> "forty six to one hundred" ➌
 else -> "Not what I'm looking for" ➍
 }

 println(message)
}

Listing 2-9How to Write Branches Inside the When Construct

while 语句

来做。。虽然语句的工作方式与它们在 Java 中完全一样——而且与 Java 一样,它们也是语句而不是表达式。我们不会在上花太多时间,而会。。而在这里循环。

这里显示了 while 循环的一个基本用法,作为复习。

fun main(args: Array<String>) {
 var count = 0
 val finish = 5

 while (count++ < finish) {
 println("counter = $count")
 }
}

对于循环

Kotlin 没有 Java 7 及更低版本中传统的循环的——如下所示:

for (int i = 0; i < 10; i++) {
 statements
}

相反,Kotlin 的 for 循环处理有迭代器的东西。如果你见过 JavaScript、C#或 Java 8 中每个循环的,Kotlin 的更接近于此。清单 2-10 中显示了一个基本示例。

| -什么 | String 类的`split()`方法返回一个 *ArrayList* 类型,我们可以迭代它。 | | ➋ | 对于集合(`words`)中的每一项(`word`),我们; | | ➌ | 打印项目。 |
fun main(args: Array<String>) {

 val words = "The quick brown fox".split(" ") ➊

 for(word in words) { ➋
 println(word) ➌
 }
}

Listing 2-10Basic for Loop

如果您需要使用循环的上的数字,您可以使用范围。范围是一种表示整数算术级数的类型。范围是用rangeTo()函数创建的,但是我们通常以它的操作符形式使用它。。).为了创建一个从 1 到 10 的整数范围,我们这样写:

var zeroToTen = 0..10

我们可以使用in关键字来执行成员资格测试。

if (9 in zeroToTen) println("9 is in zeroToTen")

要在 for 循环中使用范围,我们可以从清单 2-11 中所示的代码开始。

fun main(args: Array<String>) {
 for (i in 1..10) {
 println(i)
 }
}

Listing 2-11Using Ranges in for Loop

异常处理

Kotlin 的异常处理与 Java 非常相似:它也使用了 try-catch-finally 构造。我们所学到的关于 Java 异常处理的任何东西都可以很好地用于 Kotlin。然而,Kotlin 通过简单地使用未检查的异常简化了异常处理。这意味着编写 try-catch 块现在是可选的。你可以做,也可以不做。让我们考虑清单 2-12 中显示的代码。

| -什么 | 我们可以在 Kotlin 中使用 Java 的标准库。 | | ➋ | 这个可能会抛出“ *FileNotFoundException* ”。 | | ➌ | 这可能会抛出“ *IOException* ”,但是 Kotlin 很乐意让我们编码,而不处理可能抛出的*异常*。 |
import java.io.FileReader ➊

fun main(args: Array<String>) {

 var fileReader = FileReader("README.txt") ➋

 var content = fileReader.read() ➌
 println(content)

}

Listing 2-12I/O Operations Without Try-Catch Blocks

虽然 Kotlin 让我们不必处理异常,但我们仍然可以这样做,在某些情况下,我们可能真的必须这样做。当发生这种情况时,只需像在 Java 中那样编写异常处理代码;参见清单 2-13 中的示例。

import java.io.FileNotFoundException
import java.io.FileReader
import java.io.IOException

fun main(args: Array<String>) {

 var fileReader: FileReader

 try {
 fileReader = FileReader("README.txt")
 var content = fileReader.read()
 println(content)
 }
 catch (ffe: FileNotFoundException) {
 println(ffe.message)
 }
 catch(ioe: IOException) {
 println(ioe.message)
 }
}

Listing 2-13Kotlin’s Try-Catch Block

处理空值

Java 中错误和昂贵的返工活动的一个常见来源可能是程序员处理空值的方式。我们中的一些人真的很勤奋,这样的防御性程序员,这种讨论可能没有必要了。但是并不是所有的程序员都是生来平等的,对于我们大多数人来说,我们需要被提醒要记住 NullPointerExceptions 的可能性。空值的处理在 Java 中是一个很大的问题,以至于 Kotlin 非常慎重地决定引入一个可空类型的概念。在 Kotlin 中,当我们声明一个变量

var str: String = "Hello"
str = null // won't work

我们永远无法将这个变量的值设置为 null。我们可以给它分配一个不同的字符串值,但是 Kotlin 保证str永远不会为空。如果出于某种原因,你真的需要这个变量为空,你必须明确地告诉 Kotlin,str 是一个可空的类型。为了使字符串(或任何类型)可空,我们使用问号符号作为类型的后缀,比如

var str: String? = "Hello"

在将类型声明为 Nullable 之后,我们现在必须做一些 Kotlin 曾经为我们做过的事情。对于非可空的类型,Kotlin 确保在诸如赋值、打印、包含在表达式中等操作中使用它们是非常安全的。当我们使类型可空时,Kotlin 假设我们知道自己在做什么,并且我们足够负责任地编写必要的保护条件来防止 NullPointerExceptions 。Kotlin 假设我们会做类似清单 2-14 中所示的代码。

| -什么 | 我们声明`Array`是可空的。这意味着我们可以将 null 传递给`printArr().` | | ➋ | 因为`arr`不再保证为非空,所以我们必须在执行一些涉及到`arr`局部变量的操作之前,手动检查空值。 | | ➌ | 如果`arr`不为空,我们可以安全地执行这个操作。 |
fun main(args: Array<String>) {
 var a = arrayOf(1,2,3)
 printArr(null) 

}

fun printArr(arr: Array<Int>?) { ➊
 if(arr != null) { ➋
 arr.forEach { i -> println(i) } ➌
 }
}

Listing 2-14Demonstration of Nullable Types

Kotlin 引入了一个操作符,我们可以用它来处理可空的类型。它被称为安全调用操作符,写为问号符号后跟一个点?

我们可以用一条语句替换执行空值检查的整个 if 块:

arr?.forEach { i -> println(i) }

安全调用所做的是首先检查arr是否为空;如果是,就不会经过 forEach 操作。只有当arr不为空时,才会遍历数组。

清单 2-15 显示了清单 2-14 的重构代码。

fun main(args: Array<String>) {

 var a = arrayOf(1,2,3)
 printArr(null)
}

fun printArr(arr: Array<Int>?) {
 arr?.forEach { i -> println(i) }
}

Listing 2-15
Safe Call Operator

Kotlin 关于对象可空性的默认行为应该可以防止我们中的许多人做出让自己丢脸的事情,因为它不允许变量默认为空。然而,如果我们认为我们知道我们在做什么,某些情况会迫使我们使用可空的类型,我们仍然可以这样做。只要记得使用安全呼叫接线员;与使用 ifs 执行空检查相比,这是惯用的方法。

章节总结

  • Kotlin 的程序元素和 Java 并没有那么大的区别;它还有运算符、块、语句、表达式等。然而,在 Kotlin 中,一些在 Java 中被认为是语句的结构在 Kotlin 中是表达式,而一些在 Java 中被认为是表达式的结构在 Kotlin 中是语句(例如,赋值操作)。

  • Kotlin 的基本类型与 Java 的原始类型不同。Kotlin 的一切都是物体。

  • 在 Kotlin 中有两种方法声明变量。当使用 var 关键字时,变量是可变的。当使用 val 关键字时,变量是不可变的。

  • Kotlin 中的字符串有迭代器。此外,在模板表达式的帮助下,它们更容易组合和组合。

  • 当在 Kotlin 中声明变量时,默认情况下它们是不可空的,除非我们另外声明它们。

  • Kotlin 没有 switch 语句,但是它有一个 when 构造。

  • 在 Kotlin 中,我们不必再编写 try-catch,因为它基本上使用了未检查的异常。

在下一章,你会发现:

  • 如何(轻松地)在 Kotlin 中创建函数

  • 为什么我们不需要在 kotlin 中做大量的方法 overlordn

  • 我们如何摆脱编写实用函数,转而使用 Kotlin 的扩展函数(Java 没有这种功能)

三、函数

我们将介绍的内容:

  • 声明函数

  • 默认参数

  • 命名参数

  • 扩展函数

  • 中缀函数

  • 中缀运算符

Kotlin 的函数几乎与 Java 方法相同,尽管它在行为上更接近于 JavaScript 中的函数,因为在 Kotlin 中,函数不仅仅是一个命名的语句集合。在 Kotlin,职能是一等公民;你可以在任何可以使用变量的地方使用函数。您可以将它们作为参数传递给其他函数,也可以从其他函数返回函数。但是在我们深入这个主题之前,我们需要从 Kotlin 函数的基础开始——例如,它们是如何声明的,它们如何处理参数,它们与 Java 方法有何不同(或相似),以及一些其他细节。这就是我们将在本章讨论的内容。

声明函数

函数可以写在三个地方。你可以把它们写在(1)一个类中,像 Java 中的方法一样——这些被称为成员函数;(2)外部类——这些被称为顶级函数;(3)它们可以被写在其他函数中——这些函数被称为局部函数。不管你把函数放在哪里,声明它的机制不会有太大的变化。基本表单 a 函数如下:

fun functionName([parameters]) [:type] {
  statements
}

使用保留字 fun 声明函数,后跟一个标识符,即函数名。函数名包含圆括号,您可以在其中声明可选参数。您也可以声明函数将返回的数据类型,但这是可选的,因为 Kotlin 可以通过简单地查看函数体声明来推断函数的返回类型。接下来是一对花括号,以及函数体中的一些语句。

您应该按照与编写 Java 方法相同的准则来命名您的函数——也就是说,函数名(1)不应该是保留字;(2)不能以数字开头;和(3)不应该有特殊字符。最后,从文体的角度来看,它的名字应该包含一个动词或表示一个动作的东西——而不是当你命名一个变量时,它的名字包含一个名词。清单 3-1 显示了一个函数的基本声明,它带有一个字符串Int 参数。为了便于比较,清单 3-3 显示了清单 3-1 的等价 Java 代码。

fun displayMessage(msg: String, count: Int) {
  var counter = 1
  while(counter++ <= count ) {
    println(msg)
  }
}

Listing 3-1displayMessage Function

清单 3-1 中的displayMessage()为非生产函数;它不返回任何东西——注意在函数体中没有一个 return 关键字。在 Java 中,当一个函数不返回任何东西时,我们仍然指明返回类型是 void(参见清单 3-3 )。然而,在 Kotlin 中,我们并不真的必须这样做,因为 Kotlin 能够进行类型推断——它可以自己找出答案。但是作为一个学术练习,让我们重写清单 3-1 来完整地告诉编译器displayMessage()有哪种返回类型。参见清单 3-2 中的代码示例。

fun displayMessage(msg: String, count: Int) : Unit {
  var counter = 1
  while(counter++ <= count ) {
    println(msg)
  }
}

Listing 3-2displayMessage With an Explicit Return Type

清单 3-1 和 3-2 的唯一区别是displayMessage()函数的单元返回类型。单元对应 Java 的 void

public class DisplayMessage {

  public static void main(String []args) {
    displayMessage("Hello", 3);
  }

  static void displayMessage(String msg, int count) {
    int counter = 1;
    while(counter++ <= count) {
      System.out.println(msg);
    }
  }
}

Listing 3-3DisplayMessage in Java

为了调用displayMessage()函数,我们通过它的名字调用它并传递适当的参数,如清单 3-4 所示。

| -什么 | “Hello”被传递给`displayMessage()`的`msg`参数 | | ➋ | `3`被传递给`displayMessage();`的`count`参数。像在 Java 中一样,传递给函数的参数按照它们被定义的顺序与它的参数匹配,从左到右。 |
fun main(args: Array<String>) {
  displayMessage("Hello", 3) ➊ ➋
}

fun displayMessage(msg: String, count: Int) {
  var counter = 1
  while(counter++ <= count ) {
    println(msg)
  }
}

Listing 3-4Calling the displayMessage Function

为了使函数更有效(返回一些东西),只需在函数体的某个地方放一个 return 语句,并声明函数的返回类型。参见清单 3-5 中的示例。

fun main(args: Array<String>) {
  println(getSum(listOf(1,2,3,4,5,6)))
}

fun getSum(values: List<Int>) : Int { // return type is Int
  var total = 0;
  for (i in values)  total += i
  return total                        // return value
}

Listing 3-5
getSum, A Productive Function

你可以从函数中返回任何东西;我们不局限于基本类型。另一个例子见清单 3-6 。

| -什么 | 这个函数告诉编译器它返回一个*对*。一个*对*是一个数据类,它表示一个通用对。如果您以前使用过 Python,这可能会让您想起元组。 | | ➋ | 如果参数 *a* 大于 *b* ,那么我们使用参数 *a* 作为第一个组件,b 作为第二个组件来创建*对*,然后我们将它返回给调用者。 | | ➌ | 如果参数 *a* 小于 *b* ,那么我们使用参数 *b* 作为第一个组件,使用 *a* 作为第二个组件来创建*对*,然后我们将它返回给调用者。 | | -你好 | 可以将一对返回给赋值语句左侧的两个命名变量。这个析构声明允许我们一次将多个值保存到多个变量中。在这种情况下,变量 *x* 将接收返回对的第一个分量,变量 *y* 将接收*对的第二个分量*。 |
fun bigSmall(a: Int, b:Int) : Pair<Int, Int> { ➊

  if(a > b) return Pair(a,b) ➋
  else {
    return Pair(b,a) ➌
  }
}

fun main(args: Array<String>) {
  var (x,y) = bigSmall(5,3) ➍

  println(x)
  println(y)
}

Listing 3-6Using Pairs As a Return Type

单一表达式函数

在本章的前面,我们确实说过函数遵循基本形式

fun functionName([parameters]) [:type] {
  statements
}

在 Kotlin 中有第二种形式的编写函数,它允许更简洁的语法。有些情况下我们可以省略(1)return语句;(2)花括号;(3)将返回式合在一起。这第二种形式叫做单表达式函数。正如您可能已经从其名称中推断出的那样,该函数只包含一个表达式,如下面的代码片段所示:

fun sumInt(a: Int, b: Int) = a + b

单个表达式函数省略了这对花括号,而是使用赋值运算符来代替。它也不再需要 return 语句,因为赋值右边的表达式自动成为返回值。最后,像这样的函数不需要显式的返回类型,因为编译器可以从 expression 的值推断出返回的类型。省略显式返回类型无论如何都不是一个硬性规则。如果你喜欢的话,你仍然可以写一个明确的返回,就像这样:

fun sumInt

(a: Int, b: Int): Int = a + b

默认参数

在 Kotlin 中,函数参数可以有默认值,这允许(函数的)调用者在调用点省略一些参数。通过为函数的参数赋值,可以将默认值添加到函数的签名中。清单 3-7 显示了这样一个函数的例子。

fun connectToDb(hostname: String = "localhost",
                username: String = "mysql",
                password:String = "secret") {
}

Listing 3-7
connectToDb

请注意,“localhost”、“mysql”和“secret”分别被分配给主机名、用户名和密码。可以像这样调用该函数:

connectToDb("mycomputer","root")

调用 connectToDb()函数的所有参数都可以省略,因为它的所有参数都有默认值。但在这种情况下,我们只省略了第三个。

我们甚至可以不传递任何参数就调用函数,就像这样:

connectToDb()

Kotlin 为函数提供默认参数的能力允许我们避免创建函数重载。我们不能在 Java 中做到这一点,这就是为什么我们不得不求助于方法重载。在 Kotlin 中重载函数仍然是可能的,但是我们现在可能没有什么理由这么做了,这都要感谢默认参数。

命名参数

让我们回到清单 3-7 。如果我们调用该函数并提供所有参数,调用可能如下所示:

connectToDb("neptune", jupiter", "saturn")

这是一个有效的调用,因为 connectToDb()的所有参数都是字符串,并且我们传递了三个字符串参数。你能发现问题吗?从调用站点看不出哪一个是用户名、主机名或密码。在 Java 中,这个不明确的问题通过各种变通方法得到了解决,包括注释调用站点。

connectoToDb(/* hostname*/, "neptune,
             /* username*/ "jupiter",
             /*password*/ "saturn")

我们不必在 Kotlin 中这样做,因为我们可以在调用点命名参数。

connecToDb(hostname = "neptune",
           username = "jupiter",
           password = "saturn")

重要的是要记住,当我们开始指定参数名称时,为了避免混淆,我们需要在其后指定所有参数的名称。此外,如果我们这样做,Kotlin 不会让我们编译。例如,像这样的呼叫

connectToDb(hostname = "neptune",
           username = "jupiter",
           "saturn")

因为一旦我们命名了第二个参数(username),我们就需要提供跟在它后面的所有参数的名称。在上面的调用示例中,第二个参数被命名,但第三个没有。另一方面,像这样的电话

connectToDb("neptune",
           username = "jupiter",
           password = "saturn")

是允许的。没关系,我们没有命名第一个参数,因为 Kotlin 会把它当作一个常规的函数调用,并使用参数的位置值来解析参数。然后我们给剩下的所有参数命名。

可变数量的参数

与 Java 一样,Kotlin 中的函数也可以接受任意数量的参数。语法与 Java 稍有不同,我们没有在类型...后使用三个点,而是使用 vararg 关键字。清单 3-8 展示了一个如何声明和调用 vararg 函数的例子。

| -什么 | 关键字`vararg`让我们接受这个函数的多个参数。在这个例子中,我们声明了一个具有类型化参数的函数;它是通用的。我们不必为了处理变量参数而将它声明为泛型——我们只是选择这样做,以便它可以处理各种类型。 | | ➋ | 这是一个简单的循环机制,因此我们可以打印参数中的每一项。 | | ➌ | 我们可以传递`Ints`,我们可以传递任意多的参数,因为`manyParams`接受可变数量的参数。 | | -你好 | 它也适用于`Strings`。 | | ➎ | 像在 Java 中一样,我们可以将一个数组传递给一个接受变量参数的函数。我们需要使用扩展操作符`*`来解包数组。这就像手动地一个接一个地传递数组的各个元素。 | | ➏ | `split()`成员函数将返回一个*数组列表*,你可以将它转换成一个*数组*,然后使用 spread 运算符,这样你就可以将它传递给一个 *vararg* 函数。 |
fun<T> manyParams(vararg va : T) { ➊
  for (i in va) { ➋
    println(i)
  }
}

fun main(args: Array<String>) {
  manyParams(1,2,3,4,5) ➌
  manyParams("From", "Gallifrey", "to", "Trenzalore") ➍
  manyParams(*args) ➎
  manyParams(*"Hello there".split(" ").toTypedArray()) ➏
}

Listing 3-8Demonstration of a Variable Argument Function

扩展函数

在 Java 中,如果我们需要向类添加功能,我们可以向类本身添加方法,或者通过继承来扩展它。Kotlin 中的扩展函数允许我们在不使用继承的情况下将行为添加到现有的类中,包括用 Java 编写的类。它本质上让我们定义一个可以作为类成员调用的函数,但是这个函数是在类之外实现的。为了演示这一点,让我们从一个简单的代码开始,chanthofy,terminatorify(如清单 3-9 所示);这是一个人为的应用,但它应该为我们探索扩展函数奠定基础。

fun main(args: Array<String>) {
  val msg = "My name is Maximus Decimus Meridius"
  println(homerify(msg))
  println(chanthofy(msg))
  println(terminatorify(msg))

}

fun homerify(msg: String) = "$msg -- woohoo!"
fun chanthofy(msg: String) = "Chan, $msg , tho"
fun terminatorify(msg: String) = "$msg -- I'll be back"

Listing 3-9homerify, chanthofy, terminatorify

清单 3-9 中的应用有三个函数,它们接受一个字符串参数,向它添加一些字符串,然后将它们返回给调用者;很简单。它本身是可用的,但是我们可以通过将所有这三个函数放在一个公共类中来进一步整合它,这个类将成为我们的工具类。这样一个类可能看起来像清单 3-10 中的代码。

fun main(args: Array<String>) {
  val msg = "My name is Maximus Decimus Meridius"

  val util = StringUtil()
  println(util.homerify(msg))
  println(util.chanthofy(msg))
  println(util.terminatorify(msg))
}

/*
  The StringUtil class consolidates our three methods as member functions.
  This is a very common Java practice

*/
class StringUtil {
  fun homerify(msg: String) = "$msg -- woohoo!"
  fun chanthofy(msg:String) = "Chan, $msg , tho"
  fun terminatorify(msg: String) = "$msg -- I'll be back"
}

Listing 3-10Our Very Own StringUtil Class

我们已经可以使用清单 3-10 中的代码;事实上,这是 Java 中非常常见的做法。虽然 Java 程序员可能已经将homerify(), chanthofy(),terminatorify()实现为静态方法,而不是实例方法,就像我们在这里所做的那样,但是将有些相关的方法合并到一个工具类中(就像清单 3-10 中我们自己的 StringUtil 类)被认为是一个好主意。那是小事,我们可以放心地忽略它。关键是,在 Kotlin 中,我们可以用更简单的方式重写我们的方法,而不是为我们的三个方法编写一个工具类(参见清单 3-11 )。

fun String.homerify() = "$this -- woohoo!"

Listing 3-11homerify As an Extension Function

这看起来似乎很简单,但这确实是编写一个扩展函数所需要的。扩展函数引入了接收者类型和接收者对象的概念。清单 3-11 中接收器类型为;这是我们想要添加扩展函数的类。接收器对象是该类型的实例,在我们的例子中是“我的名字是 Maximus Decimus Meridius ”。当您将一个扩展函数附加到一个类型上时,比如在我们的例子中是一个字符串,扩展函数可以使用关键字 this 引用 receiver 对象。对于所有意图和目的,扩展函数看起来就像在接收器类型上定义的任何成员函数。因此,扩展函数能够引用这个是有意义的。清单 3-12 显示了我们的扩展字符串类的完整代码。

fun main(args: Array<String>) {
  val msg = "My name is Maximus Decimus Meridius"

  println(msg.homerify())
  println(msg.chanthofy())
  println(msg.terminatorify())

}

fun String.homerify() = "$this -- woohoo!"
fun String.chanthofy() = "Chan, $this , tho"
fun String.terminatorify() = "$this -- I'll be back"

Listing 3-12Extended String Class

用 Kotlin 编写实用函数完全没问题,但是有了扩展函数,使用它们似乎更自然,因为它增加了代码的语义价值。使用扩展函数语法感觉更自然。

中缀函数

“中缀”符号是数学和逻辑表达式中使用的符号之一。它是操作数之间运算符的位置(例如,a+b;加号是“不固定的”,因为它在操作数 ab 之间。相反,操作可以遵循“后固定”符号,其中表达式可以写成这样 (+ a b) ,或者它们可以是“后固定”,其中我们的表达式可以写成这样 (a b +)。

在 Kotlin 中,成员函数可以是“固定的”,这允许我们编写如下代码:

john say "Hello World"

如果约翰是一个变量,它指向一个类型为的对象(我们一会儿会看到定义),并且是一个采用字符串参数的方法,就像"Hello World",一样,那么上面的语句是一种更自然的写法

john.say("Hello World")

为了开始我们对中缀函数的探索,让我们从实现允许我们使用传统的点符号调用say()成员函数的代码开始。然后我们会写代码让我们使用这个内嵌版本。清单 3-13 展示了 Person 类的经典实现,我们可以用点符号来调用它。

fun main(args: Array<String>) {
  val john = Person("John Doe")
  john.say("Hello World")
}

class Person(val name : String) {
  fun say(message: String) = println("$name is saying $message")
}

Listing 3-13Person Class Without infix Function

这并不奇怪,这种调用是我们大多数人在 Java 编程中接触到的。这不需要任何进一步的评论。现在,让我们看看允许我们以“内嵌”方式调用 say 方法的实现。

fun main(args: Array<String>) {
  val john = Person("John Doe")
  john say "Hello World"
}

class Person(val name : String) {
  infix fun say(message: String) = println("$name is saying $message")
}

Listing 3-14Person Class With an infix Function

为了以“中缀”的方式使用say()函数,你需要做的唯一一件事就是在函数的开头添加中缀关键字,如清单 3-14 所示。话虽如此,你不能把每个函数都转换成中缀。只有在以下情况下,函数才能转换为中缀

  • 它是一个成员函数(类的一部分)或扩展函数,并且

  • 它只接受一个参数。如果你在考虑一个漏洞,比如“我可能会在函数中定义一个参数,然后使用 vararg”,那就不行。不允许将变量参数转换为中缀函数。

顺便说一下,您不能像这样使用命名参数调用中缀函数

john say msg = "Hello World" // won't work

记住中缀函数只接受一个参数;在调用点命名参数没有太大意义。

明智地使用中缀函数可以实现更直观的编码,因为它们可以将程序逻辑隐藏在类似关键字的语法后面。你可以用中缀符号创建某种元语言;小心不要过度。

运算符重载

在这一章里,运算符重载的话题似乎有点不合适,因为这一章讲的都是函数。但是在 Kotlin 中,这个主题很好地融入了中缀函数的讨论,因为它们在实现中有共同的机制,我们很快就会看到。

运算符重载允许我们适当地使用一些标准运算符,如数学运算符的加法、除法、乘法、。例如,我们可以编写一段代码,允许使用加号来添加两个 Employee 对象或任何其他自定义类型。考虑清单 3-15 中的代码。

fun main(args: Array<String>) {

  var e1 = Employee("John Doe")
  var e2 = Employee("Jane Doe")
  var e3 = e1 + e2
  println(e3.name)
}

Listing 3-15Adding Two Employee Objects

不知何故,我们直觉地知道陈述e3 = e1 + e3是什么意思;如果我们将一个 employee 对象添加到另一个对象中,那么我们应该得到雇员 e1e2 的组合信息或状态——如果这是您希望能够在代码中完成的事情。从程序上来说,我们知道这个语句不应该工作,因为加法运算符不知道任何关于 Employee 对象的信息,更不知道如何对它们执行加法操作。然而,在 Kotlin 中,我们可以教加法运算符如何将两个 Employee 对象相加。这显示在清单 3-16 中。

| -什么 | 这与*中缀函数*的语法非常相似,正如我们在上一节中看到的。这里唯一的新东西是*操作符*关键字。 |
class Employee(var name: String) {

   infix operator  fun plus(emp: Employee) : Employee { ➊
    this.name += "\n${emp.name}" //
    return this
  }
}

Listing 3-16
class Employee

我们已经知道 infix 关键字会对函数产生什么影响。事实上是一个中缀的函数,允许我们像这样写代码(见清单 3-16 ):

var e1 = Employee("John Doe")
var e2 = Employee("Jane Doe")

var e3 = e1 plus e2

然而,函数名加上并不是一个普通的函数名。这不仅仅是我们想出来的另一个名字。这对 Kotlin 来说有着特殊的意义。函数名是一个固定标识符,对应数学运算符+。而当这个特殊的函数名与关键字中缀运算符组合在一起时,它允许我们编写这样的代码

var e3 = e1 + e2

Kotlin 允许我们覆盖相当多的操作符,而且不仅限于数学操作符。表 3-1 显示了其中的一些。这不是一个完整的列表,但它应该让你知道你可以超载多少。

表 3-1

可以重载的运算符及其对应的函数名

|

操作员

|

函数名

|

表示

|

翻译成

|
| --- | --- | --- | --- |
| + | Plus | a + b | a.plus(b) |
| - | Minus | a – b | a.minus(b) |
| / | Div | a / b | a.div(b) |
| * | Times | a * b | a.times(b) |
| % | rem | a % b | a.rem(b) |
| .. | rangeTo | a .. b | a.rangeTo(b) |
| ++ | inc | a++ | a.inc() |
| -- | dec | a-- | a.dec() |
| += | plusAssign | a += b | a.plusAssign(b) |
| -+ | minusAssign | a -= b | a.minusAssign(b) |
| /= | divAssign | a /= b | a.divAssign(b) |
| *= | timesAssign | a *= b | a.timesAssign(b) |
| %= | remAssign | a %= b | a.remAssign(b) |
| > | compareTo | a > b | a.compareTo(b) > 0 |
| < | compareTo | a < b | a.conpareTo(b) < 0 |
| >= | compareTo | a >= | a.conpareTo(b) >= 0 |
| <= | compareTo | a<= b | a.conpareTo(b) <= 0 |

运算符重载是多态的一个特例,不同的运算符,如数学运算符,可以根据参数(或操作数的类型)有不同的实现,正如我们在清单 3-14 和 3-15 中看到的。正确使用操作符重载可以产生更容易理解的代码,因为它们是用业务对象域的语言编写的。它们有更高的语义价值。

Kotlin 并不是第一种实现运算符重载的语言。以前像 C++ 这样的语言也做过。应该注意的是,运算符重载的使用,或者更恰当地说,过度使用和滥用已经招致了很多批评。正是因为如果你能重新定义像加号、减号等众所周知的运算符的动作和行为。,它会导致难以处理的代码。因此,当您选择操作符重载的路线时,请行使良好的判断力。

章节总结

  • Kotlin 函数可以在三个地方编写。像在 Java 中一样,它们可以是类的成员,但也可以作为顶级构造来编写。第三,它们可以嵌入到其他函数中——我们在本章中没有深入研究本地函数,但是我们将在后面的章节中详细讨论这个主题。

  • Kotlin 通过添加对默认参数、命名参数甚至可变数量的参数的支持,使得声明和调用函数变得更加容易。位置参数、命名参数和默认参数的组合允许我们避免过度使用参数重载,就像我们在 Java 中所做的那样。

  • 扩展函数提供了一种新的方式来扩展现有类型的行为。我们可以在类之外添加额外的行为,但是我们可以调用扩展函数,就好像它是被烘焙到类定义中一样。

  • 中缀函数和中缀运算符允许我们在不使用点符号的情况下编写函数调用,从而增加了代码的语义值。通过允许函数调用被中缀 -ed,结果代码变得更有表现力,更接近领域语言。

在下一章,我们将看看 Kotlin 面向对象的一面。我们将学习 Kotlin 如何处理类、构造函数和接口。我们还将了解 Kotlin 中新的数据类。

四、使用类

我们将介绍的内容:

  • 接口

  • 数据类别

  • 访问修饰符

  • 对象声明

Kotlin 和 Java 一样,是一种基于类的面向对象的语言。它使用接口和类来定义自定义类型。Kotlin 处理类型的方式与我们在 Java 中处理类型的方式非常相似,但是也有一些地方 Kotlin 会觉得我们不熟悉。在这一章中,我们将探讨这些相似之处和不同之处。

接口

与 Java 一样,Kotlin 中接口的基本形式类似于清单 4-1 中的代码。

interface Fax {
  fun call(number: String) = println("Calling $number")
  fun print(doc: String) = println("Fax:Printing $doc")
  fun answer()
}

Listing 4-1Interface Fax

它仍然使用接口关键字,并且它还包含抽象函数。Kotlin 接口的显著之处在于,它们可以(1)包含属性,( 2)具有带实现的功能——换句话说,具体的功能。尽管 Java 8 确实允许 Java 中的默认实现,所以最后一个不再是 Kotlin 独有的,但仍然非常有用,我们将在后面看到。不要太担心接口有属性——你会习惯的。虽然我们不会在这一节讨论属性,但是我们将在后面的一节(类)中讨论它们。为了实现一个接口,Kotlin 使用冒号操作符,如清单 4-2 所示。

| -什么 | 使用冒号操作符,而不是 Java 的*实现*关键字。冒号也用于继承类。 | | ➋ | 我们必须为`answer()`函数提供一个实现,因为它在接口定义中没有实现。另一方面,我们不必为`call()`和`print()`提供实现,因为它们在接口定义中有实现。您可能还注意到,我们在这个函数中使用了`override`关键字。为了向编译器澄清我们不打算隐藏或掩盖接口定义中的`answer()`函数,使用它是必要的。相反,我们打算替换它,这样它就可以是多态的。我们想在这个类中为`answer()`函数提供我们自己的行为。 |
class MultiFunction : Fax { ➊
  override fun answer () { ➋

  }
}

Listing 4-2class MultiFunction Implementing Fax

你可能想知道为什么 Kotlin 允许我们在接口中提供实现。难道接口不应该只包含抽象函数并将实现留给将实现接口的类吗?这样,您就可以在类型之间实施契约。嗯,在 Java 的早期,这正是接口的使用方式;它们纯粹是一种抽象的结构。然而,从 Java 8 开始,您已经可以在接口上提供默认实现

允许这样做有一些实际的原因。接口上的默认实现将允许我们随着时间的推移来发展接口。想象一下,如果我们今天用成员函数 a(),【b(),c()接口 Foo ,并且这个发布给其他开发者。将来,如果我们给接口 Foo 添加函数 d() ,那么所有使用 Foo 的代码现在都将被中断。然而,如果我们为 d(),提供一个默认实现,那么现有的代码就不必中断。这是接口上的函数实现可能有用的用例之一。

钻石问题

当一个类继承了两个超类型,并且这两个超类型实现了完全相同的函数或方法时,就会出现“菱形问题”。代码示例见清单 4-3 。

interface A {
  fun foo() {
    println("A:foo")
  }
}

interface B {
  fun foo() {
    println("B:foo")
  }
}

class Child : A, B {

}

Listing 4-3Diamond Problem

清单 4-3 中显示的代码不会编译,因为不清楚从类的实例调用函数 foo() 时会有什么行为; foo()接口 AB 定义,两个接口都为函数提供默认实现。这就是所谓的“钻石问题”一个类继承自两个超类型,一个行为被定义在一个以上的类型上,这些类型是的来源。在清单 4-3 中,如果我们从的一个实例中调用 foo() ,那么它将表现出哪种行为是不明确的——它将打印“A:foo”还是“B:foo”。在 Kotlin 中,解决这个问题的方法是让子类提供冲突函数的实现——在本例中,函数 foo() 。清单 4-4 给出了解决方案。

interface A {
  fun foo() {
    println("A:foo")
  }
}

interface B {
  fun foo() {
    println("B:foo")
  }
}

class Child : A, B {
  override fun foo () {
    println("Child:foo")
  }
}

fun main(args: Array<String>) {
  var child: Child = Child()
  child.foo()
}

Listing 4-4Diamond Problem, Solved

调用超级行为

像 Java 一样,Kotlin 的函数可以调用其父类型的函数,如果它有实现的话。此外,像在 Java 中一样,Kotlin 使用 super 关键字来实现这一点。Kotlin 中的 super 关键字与 Java 中的含义相同——它是对超类型实例的引用。要调用超类型的函数,你需要三样东西:(super 关键字;(2)包含在一对尖括号中的超类型的名称;以及(3)您想要在超类型上调用的函数的名称。它看起来像下面的代码片段:

super<NameOfSuperType>.functionName()

让我们扩展一下本章前面的传真和多功能示例。

interface Printable {
  fun print(doc:String) = println("Printer:Printing $doc")
}

interface Fax {
  fun call(number: String) = println("Calling $number")
  fun print(doc: String) = println("Fax:Printing $doc")
  fun answer() = println("answering")
}

class MultiFunction : Printable, Fax {

  override fun print(doc:String)  {
    println(“Multifunction: printing”)
  }
}

Listing 4-5Printable, Fax, and MultiFunction

清单 4-5 显示了之前的传真和多功能示例。我们添加了一个名为 Printable 的新接口,它还定义了一个print()函数。我们修改后的代码清单显示了从 Fax 和新的 Printable 接口继承而来的多功能类。多功能类覆盖print()功能;它必须这样做,因为print()函数继承自 PrintableFax 接口,并且在这两个接口上都有默认的实现。

多功能中被覆盖的print()功能有一个简单的 println 语句。为了演示如何调用超类型上的函数,我们将从多功能中被覆盖的print()内调用两个超类型上的print()函数。清单 4-6 向我们展示了如何做到这一点。

class MultiFunction : Printable, Fax {

  override fun print(doc:String)  {
    super<Fax>.print(doc)
    super<Printable>.print(doc)
    println("Multifunction: printing")
  }
}

Listing 4-6MultiFunction, Calling Functions on Supertype

现在,当我们调用print()函数时,它将调用传真中的print(),然后是可打印,最后是多功能中被覆盖的print()中的任何语句。清单 4-7 显示了这个例子的完整代码。

interface Printable {
  fun print(doc:String) = println("Printer:Printing $doc")
}

interface Fax {
  fun call(number: String) = println("Calling $number")
  fun print(doc: String) = println("Fax:Printing $doc")
  fun answer() = println("answering")
}

class MultiFunction : Printable, Fax {

  override fun print(doc:String)  {
    super<Fax>.print(doc)
    super<Printable>.print(doc)
    println("Multifunction: printing")
  }
}

fun main(args: Array<String>) {
  val mfc = MultiFunction()
  mfc.print("The quick brown fox")
  mfc.call("12345")
}

Listing 4-7MultiFunction, Printable, and Fax

使用(1)关键字 class 定义一个类;(2)标识符,这将是它的名称;(3)可选的报头;和(4)任选的主体。清单 4-8 显示了一个基本类。

class Person() {
}

Listing 4-8A basic class in Kotlin

类的头部是一对括号。头可能包含参数,但是在这个例子中,它没有任何参数。这对花括号组成了类的主体。头文件和类主体都是可选的,但是我们在书中使用的大多数代码都会包含它们。

要实例化 Person 类,我们可以编写如下代码:

var person = Person()

如果不是因为明显缺少 new 关键字,它看起来很像我们在 Java 中创建对象的方式。类型名( Person )后面的一对括号是对无参数构造函数(ctor)的调用。让我们稍微回顾一下清单 4-8 ,仔细看看类定义的头部。这是少数几个 Kotlin 看起来和感觉上与 Java 有点不同的地方之一。Java 类没有头文件,但是 Kotlin 有。这个头实际上是一个构造函数定义。

构造器

Kotlin 类的定义中可以有多个构造函数。这与 Java 没有太大的不同,因为它的类也可以包含多个 ctor。然而,Kotlin 区分了初级和次级细胞。主 ctor 写在类的头部,就像你在清单 4-8 中看到的那样,而次 ctor 写在主体中。清单 4-9 显示了一个带有主构造函数的类。

| -什么 | 当一个构造函数写在类头上时,就像这样,它是主 ctor。这种编写 ctor 的方式与我们在清单 4-8 中的例子基本相同,除了清单 4-8 不包含*构造函数*关键字,并且在这里(清单 4-9 ,我们的 ctor 接受一个参数。 | | ➋ | 这是一个成员变量,将保存`_name`的值。 | | ➌ | 这是一个*初始化器*块,类似于 Java 的*初始化器*。每当创建一个类的实例时,就会执行这个操作。在你的类中可以有不止一个初始化器块,当这种情况发生时,*初始化器*将按照它们在类中被定义的顺序被执行。一个*初始化器*块是一对以关键字`init`为前缀的花括号,当你拥有的唯一构造函数是主构造函数时,你通常会使用它们,因为主构造函数不能包含任何代码(无论是语句还是表达式)。 | | -你好 | 我们可以访问从初始化器块传递给主 ctor 的参数。 |
class Person constructor(_name: String) { ➊
  var name:String  ➋
  init {           ➌
    name = _name   ➍
  }
}

Listing 4-9Person Class with Primary Constructor

当主 ctor 没有(或不需要)注释或可见性修饰符时,我们可以省略构造函数关键字,如下所示:

class Person (_name: String) {
  var name:String
  init {
    name = _name
  }
}

我们可以通过在语句中加入 init 块和 name 变量的声明来进一步简化和缩短代码。Kotlin 很聪明。

class Person (_name: String) {
  var name:String = _name
}

构造函数也可以在类体内定义,就像在 Java 中那样。当它们被写成这样时,它们被称为二级构造函数。清单 4-10 显示了一个带有辅助 ctor 的示例代码。

class Employee {
  var name:String
  constructor(_name: String) {
    name = _name
  }
}

Listing 4-10Employee Class, with Secondary Constructor

注意,在清单 4-11 中,我们不必使用 init 块,因为 name 成员变量的初始化是在构造函数体中完成的。与主 ctor 不同,辅助 ctor 可以包含代码。

| -什么 | 我们必须初始化我们的成员变量,因为 Kotlin 不能告诉我们正在做什么初始化。 | | ➋ | 二级构造函数需要有*构造函数*关键字。这个 ctor 没有身体;这样写没问题。此外,这个 ctor 调用另一个 ctor—一个接受两个参数的 ctor。 | | ➌ | 为 Employee 类定义了另一个辅助构造函数。这个函数有两个参数:一个名字和一个雇员 id。 |
class Employee  {
  var name:String = ""    ➊
  var empid:String = ""

  constructor(_name: String) : this(_name, "1001") ➋
  constructor(_name:String, _id: String) {         ➌
    name = _name
    empid = _id
  }
}

Listing 4-11class Employee, with Two Secondary Constructors

你可以在 Kotlin 中重载你的构造函数,就像我们在 Java 中做的那样,正如你在清单 4-11 中看到的。而且,和在 Java 中一样,我们可以使用 this 关键字调用其他构造函数。Kotlin 中的 this 关键字与 Java 中的相同,它指的是你自己的一个实例——这并不奇怪。不过,请注意,我们是如何使用 this 构造将调用委托给另一个二级构造函数的。您需要使用冒号将这个调用链接到构造函数定义(参见清单 4-11 的第 2 项)。

虽然 Kotlin 允许我们通过重载在构造函数上进行参数多态性,但这并不是真正惯用的 Kotlin,因为使用 Kotlin 为函数参数提供默认值的能力可以获得相同的结果。参见清单 4-12 中 Employee 类示例的简化版本。

class Employee (_name:String, _empid:String = "1001")  {
  val name = _name
  val empid = _empid
}

Listing 4-12Simplified Employee class

清单 4-12 中的代码更短更简洁。此外,通过将构造函数参数移动到主构造函数,它允许我们使用 val 而不是 var 来声明成员变量。在 Kotlin 中,使用不可变变量是一种首选技术,因为它可以减少整体编码错误。如果一个属性的值一开始就是不可变的,那么你不可能意外地改变它。

遗产

默认情况下,Kotlin 类是 final ,而 Java 类是“开放的”或非 final。如清单 4-13 所示的代码不会编译,因为 Person 类是 final

class Person {
}

class Employee : Person() {
}

Listing 4-13Person and Employee class

为了编译我们的代码样本,我们必须显式地告诉 Kotlin 类 Personopen ,这意味着我们打算让它被扩展或继承(参见清单 4-14 )。Kotlin 类的这种默认行为被认为是正确的行为和良好的实践。套用 Joshua Bloch 的有效 Java (Addison-Wesley,2008)中的一句话:“为 继承 设计和文档,否则禁止它。”这实际上意味着所有您不打算扩展或覆盖的类和方法都应该被声明为 final 。在 Kotlin,这是自动行为。清单 4-14 再次显示了 Person 类,但是这一次,它具有 open 修饰符,这意味着类 Person 可以被扩展。

open class Person {
}

class Employee : Person() {
}

Listing 4-14Person and Employee class

final 作为默认行为的行为不只是针对类;Kotlin 中的成员函数也是如此。当一个函数没有使用 open 修饰符时,它就是最终的。

| -什么 | 函数需要特别标记为 open,这样它们就可以被子类型覆盖。 | | ➋ | 子类型需要用 *override* 关键字来标记函数,以使其具有多态性。IntelliJ 足够聪明,当它感觉到您正在子类型上定义一个函数,而该子类型在父类型上有一个精确的签名,并且没有使用 *override* 关键字时,它会阻止编译发生。 | | ➌ | 我们可以从这里称超行为;这有效地调用了类 *Person 中的`talk()`函数。* | | -你好 | 我们正在覆盖`toString()`函数。这个行为是从 *Person* 类继承来的,而后者又是从 *Any* 类继承来的。您可以将类 *Any* 视为 *java.lang.Object.* 的模拟 |
open class Person(_name:String) {
  val name = _name

  open fun talk() {  ➊
    println("${this.javaClass.simpleName} talking")
  }
}

class Employee(_name:String, _empid:String = "1001") : Person(_name) {
  val empid = _empid

  override fun talk() { ➋
    super.talk() ➌
    println("Hello")
  }

  override fun toString():String{  ➍
    return "name: $name | id: $empid"
  }
}

Listing 4-15Method Overriding

你需要记住,当一个函数已经被标记为 open 时,它将保持开放,以便被它的直接子类型甚至间接子类型覆盖,除非该函数再次被标记为 final 。为了说明这一点,让我们考虑列出 4-16 。

| -什么 | `talk()`功能第一次被标记为打开。 | | ➋ | 我们可以从这里超越`talk()`。 | | ➌ | 即使类 Employee 没有将函数标记为 open,我们仍然可以在这里覆盖`talk()`。函数`talk()`在继承层次结构中保持隐式打开,除非它在继承链中的某处被标记为 final。 |
open class Person(_name:String) {
  val name = _name

  open fun talk() { ➊
    println("${this.javaClass.simpleName} talking")
  }
}

open class Employee(_name:String, _empid:String = "1001") : Person(_name) {
  val empid = _empid

   override fun talk() { ➋
    super.talk()
    println("Employee overriding talk()")
  }

  override fun toString():String{
    return "name: $name | id: $empid"
  }
}

class Programmer(_name:String) : Employee(_name) {
  override fun talk() { ➌
    super.talk()
    println("Programmer overriding talk()")
  }
}

Listing 4-16class Person, Employee, and Programmer

清单 4-17 演示了如何在继承链中再次“关闭”一个函数。

| -什么 | 在同一行上看到 final 和 override 关键字确实有点奇怪,但这是完全合法的。这意味着我们正在覆盖函数,同时为了进一步的继承而“关闭”它。该函数中的 final 关键字只影响 Employee 类的子类型,而不影响 Employee 类本身。 | | ➋ | 这个不会再编译了。 |
open class Person(_name:String) {
  val name = _name

  open fun talk() {
    println("${this.javaClass.simpleName} talking")
  }
}

open class Employee(_name:String, _empid:String = "1001") : Person(_name) {
  val empid = _empid

   override fun talk() {
    super.talk()
    println("Employee overriding talk()")
  }

  final override fun toString():String{ ➊
    return "name: $name | id: $empid"
  }
}

class Programmer(_name:String) : Employee(_name) {
  override fun talk() { ➋
    super.talk()
    println("Programmer overriding talk()")
  }
}

Listing 4-17How to Make a Function Final, Again

性能

传统上,类或对象中的属性是通过定义成员变量并为其提供访问器方法来创建的。这些方法通常会遵循一些命名约定,其中成员变量的名称会以 getset 为前缀。

class Person {
  private String name;

  public String getName() {
    return this.name;
  }
  public void setName(String arg) {
    this.name = arg;
  }

  public static void main(String []args) {
    Person person = new Person();
    person.setName("John Doe");
    System.out.println(person.getName());
  }
}

Listing 4-18Person Class in Java with a Single Property

清单 4-18 显示了一个简单的 Java 类,它定义了一个名为 name 的属性。这是通过定义一个私有的成员变量来实现的,这样对这个状态的访问就只能通过访问器— getName()setName()来控制。这种编码在 Java 中是惯用的,因为它没有对属性的本地语言支持。我们仍然可以在 Kotlin 中遵循这种编码风格,但我们不必这样做,因为 Kotlin 有对属性的语言支持。

如果我们用 Kotlin 重写清单 4-18 ,它看起来会像清单 4-19 中的代码。

| -什么 | 构造函数接受一个参数。这允许我们在创建时设置对象的名称。 | | ➋ | 我们可以从这里通过构造函数访问参数。 | | ➌ | 这看起来像是我们在直接访问 name 成员变量,但实际上不是。这实际上调用了 get 访问器方法。 |
class Person(_name:String) { ➊
  val name:String = _name ➋
}

fun main(args: Array<String>) {
  var person = Person("John Smith")
  println(person.name) ➌
}

Listing 4-19Person class With a Single Property

清单 4-19 中的 Person 类定义可以进一步简化为清单 4-20 中的定义。

class Person(val name:String)

fun main(args: Array<String>) {
  var person = Person("John Smith")
  println(person.name)
}

Listing 4-20Simplified Person class

这里的代码是用 Kotlin 定义属性的最简洁的方式。也被认为是惯用的。请注意我们在代码中所做的更改:

  1. 主构造函数中的参数现在有了一个 val 声明。这实际上使构造函数参数成为一个属性。我们本可以使用 var ,它也会同样有效。

  2. 我们不再需要区分构造函数参数中的标识符和成员变量;因此,我们删除了_name变量中的前导下划线。

  3. 我们可以放弃整个类,因为我们不再需要它了。类体只包含将构造函数参数的值传递给成员变量的代码。由于 Kotlin 将自动为构造函数参数定义一个支持字段,我们不必在类体中做任何事情。

清单 4-20 中的代码展示了在 Kotlin 中定义数据对象的最基本的方法(java 程序员称之为 POJOs 或普通 Java 对象)。通过简单地在主构造函数参数中使用 valvar ,我们可以用适当的赋值函数方法自动定义属性。然而,在某些情况下,您仍然需要对这些属性的“获取”和“设置”过程进行更多的控制。Kotlin 也允许我们这样做。

我们可以通过执行以下操作来接管“获取”和“设置”的自动过程:

  1. 在类体中声明属性,而不是在主构造函数中。

  2. 在类体中提供 getter 和 setter 方法。

声明属性的完整语法如下:

var <property name>:[<property type>][=<initializer>]
  [<getter>]
  [<setter>]

清单 4-21 展示了定制访问器方法的一些基本用法。

| -什么 | 我们在类体内声明并定义了*属性*,而不是在主构造函数中将其作为参数捕获。我们首先将其初始化为一个空字符串。 | | ➋ | `get()`的语法看起来很像定义*函数、*的语法,除了我们没有在它前面写 *fun* 关键字。 | | ➌ | 这是您编写自定义代码的地方。每当有人试图访问`name`属性时,就会执行该语句。 | | -你好 | *字段*关键字是一个特殊的关键字。它指的是*支持字段*,当我们定义一个名为`name`的属性时,Kotlin 会自动提供这个字段。`name`成员变量不是简单变量;Kotlin 为它创建了一个自动的*支持字段*,但是我们不能直接访问那个变量。然而,我们可以通过*字段*关键字来访问它,就像我们在这里所做的那样。 | | ➎ | `value`参数对应于创建 Employee 对象后将分配给该属性的值(参见项目符号➐). | | ➏ | 在我们执行了自定义逻辑之后,我们现在可以设置字段的值。 | | -好的 | 这将触发我们的 set 访问器逻辑,参见➎. | | -好的 | 这将触发我们的 get 访问器逻辑,参见➋. |
class Employee {
  var name: String = ""   ➊
    get() {  ➋
      Log("Getting lastname") ➌
      return field  ➍
    }
    set(value) {  ➎
      Log("Setting value of lastname")
      field = value ➏
    }
}

fun Log(msg:String) {
  println(msg)
}

fun main(args: Array<String>) {
  var emp = Employee()
  emp.name = "John Doe"  ➐
  println(emp.name) ➑
}

Listing 4-21Custom Accessor Methods

您可能想知道为什么我们在 gettersetter 方法中使用字段关键字。为什么我们不能像在 Java 中那样编写访问器方法呢(参见清单 4-22 )?这是为属性编写 getter 和 setter 代码的错误方式。

| -什么 | 这会导致递归调用,最终会抛出 *StackOverflowError。* | | ➋ | 这个也会 |
class Employee {
  var name: String = ""
    get() {
      Log("Getting lastname")
      return this.name        ➊
    }
    set(value) {
      Log("Setting value of lastname")
      this.name = value  ➋
    }
}

在清单 4-22 中,表达式this.name并不真正访问成员变量名。相反,当您为类定义属性时,它调用 Kotlin 自动提供的默认访问器方法。因此,从访问函数内部调用this.name将导致递归调用的混乱,最终运行时将抛出一个 StackOverflowError 。为了防止这种情况发生,当从访问器函数中引用属性名的后台字段时,应该使用 field 关键字。

数据类别

当 POJOs 被创建时,有时它们会被存储在集合中(例如, ArrayListHashMapHashSet,等等)。).为了正确利用这些 POJOs,在 Java 中,我们需要覆盖equals(), hashCode(),toString()方法。请记住,在 Java 中,当它们存储在集合中时,我们可以正确地使用它们——特别是对 hashCode 敏感的集合。

在上一节中,我们已经看到了在 Kotlin 中创建 POJOs 的模拟是多么容易。我们可以简单地在类中定义属性,这样就可以了。对于简单的用例,我们在上一节中创建的数据对象应该足够好了。但是当您需要做一些事情,比如在集合中存储值对象,或者比较对象之间的内容是否相等时,您会发现有属性的类是不够的。为了在集合对象中正确地利用值对象,我们需要能够可靠地比较对象。在 Java 中,我们通过覆盖java.lang.Object的一些方法来解决这类问题,即equals()hashCode()方法。当我们进行对象比较时,这些方法是关键。

class Employee(val name:String)

fun main(args: Array<String>) {

  val e1 = Employee("John Doe")
  val e2 = Employee("John Doe")

  println(e1 == e2) // output is false
}

Listing 4-22Comparing Two Employee Objects

记住,在 Kotlin 中,double equals 操作符实际上调用被比较的操作数的equals()函数——由于 Kotlin 中的所有东西都是对象,它们都有equals()函数,因为它是从父类型 Any 继承的。如果我们让 Employee 类像清单 4-22 中那样,它将使用来自类 Anyequals()函数的实现,并且它不知道如何比较 Employee 对象。为了解决这个问题,我们可以重写 equals()方法,并提供一个关于如何比较 Employee 对象的实现。

注意

像 Java 一样,Kotlin 遵循单根类继承。如果我们不在类定义中指定一个超类,这个类将隐式地扩展 Any 。这个类是 Kotlin 中所有不可空类型的超类型。

要修复清单 4-22 中的代码,我们通常必须覆盖清单 4-23 中所示的equals()hashCode()函数。

| -什么 | 类*中的`equals()`函数任意*被*打开*,我们可以覆盖它。 | | ➋ | 我们首先检查是否将一个*雇员*对象与另一个*雇员*对象进行比较。 *is* 关键字执行两个功能:(1)它检查 *obj* 是否确实是 *Employee* 的实例,以及(2)它自动将 *obj* 转换为 *Employee* 对象。 | | ➌ | `Obj`被自动转换成一个*雇员*对象。*是*关键词已经做到了。现在,我们可以安全地比较两个对象的`name`成员变量了。 | | -你好 | 如果您打算将该对象存储在哈希代码比较很重要的集合中(例如, *HashSet* 、 *HashMap、*等),通常需要覆盖`hashCode()`函数。).对于我们的小例子,这是不必要的。但是,无论何时重写`equals()`函数,重写`hashCode()`函数都是一个好习惯。 | | ➎ | 调用*雇员*对象的`toString()`函数。在父类型 *Any* 上可以找到`toString()`函数。`toString()`的默认实现给了我们一个类似“Employee@ae805cc4”的输出。 | | ➏ | 现在,这个打印“真”。 |
import java.util.*

class Employee(val name:String){
  override fun equals(obj:Any?):Boolean { ➊
    var retval = false
    if(obj is Employee) { ➋
      retval  = name == obj.name ➌
    }
    return retval
  }
  override fun hashCode(): Int { ➍
    return Objects.hash(name)
  }
}

fun main(args: Array<String>) {

  val e1 = Employee("John Doe")
  val e2 = Employee("John Doe")

  println(e1)          ➎
  println(e1 == e2)    ➏
}

Listing 4-23Overriding the hashCode() and equals() Functions

这种编码实践在 Java 中非常常见,因此,相当多的 ide 都具有生成 toString()、equals()和 hashCode()样板代码的能力。虽然我们仍然可以在 Kotlin 做这些事情,但我们没有必要这样做。在 Kotlin 中,我们需要做的唯一一件事就是让 Employee 成为一个数据类。清单 4-24 向我们展示了如何操作。

| -什么 | 要使 Kotlin 中的任何类成为数据类,只需在类声明中使用关键字 ?? 数据。 | | ➋ | 我们得到了一个额外的好处,数据类的输出更好。这个现在打印“雇员(姓名=John Doe)”。 | | ➌ | 另外,`equals()`比较返回 true。 |
data class Employee(val name:String) ➊

fun main(args: Array<String>) {
  val e1 = Employee("John Doe")
  val e2 = Employee("John Doe")

  println(e1)       ➋
  println(e1 == e2) ➌
}

Listing 4-24Employee Data Class

可见性修改器

Kotlin 使用与 Java 几乎相同的关键字来控制可见性。关键字 publicprivate、protected 在 Kotlin 中的意思和在 Java 中完全一样。但是,缺省可见性是区别所在。在 Kotlin 中,无论何时省略可见性修饰符,默认可见性都是 public

class Foo {
  var bar:String = ""
  fun doSomething() {

  }
}

Listing 4-25Class Foo

在清单 4-25 中,Foo 类及其成员是公开可见的。如果你想改变可见性,你必须显式声明。相比之下,Java 的默认可见性是包私有,这意味着它只对同一个包中的类可用。Kotlin 没有包-私有等价,因为 Kotlin 不使用包来管理可见性。Kotlin 中的包只是一种组织文件和防止名称冲突的方法。

代替 Java 的 package-private,Kotlin 引入了 internal 关键字,这意味着它在模块中是可见的。模块只是文件的集合,它可以是(IntelliJ 模块或项目;(2)一个 Eclipse 项目;(3)一个 Maven 项目;或者(4)一个 Gradle 项目。为了演示一些可见性修改器的运行,请参见清单 4-26 。

| -什么 | 类 *Foo* 被标记为 *internal,*这使得它只在同一个模块中的类和顶级函数中可见,并且它们的可见性也被标记为 *internal。* | | ➋ | 这是一个错误。*扩展函数*被标记为 *public,*但是函数(Foo)的接收者被标记为 internal。类 *Foo* 比扩展函数更不可见;因此 Kotlin 不允许我们。 | | ➌ | 对该类是私有的,因此我们无法从这里访问它。 | | -你好 | 受到保护,我们无法从这里到达。 |
internal open class Foo {  ➊
  private fun boo() = println("boo")
  protected fun doo() = println("doo")
}

fun Foo.bar() { ➋
  boo() ➌
  doo() ➍
}

fun main(args: Array<String>) {
  var fu = Foo()
  fu.bar()
}

Listing 4-26Demonstrating Visibility Modifiers

为了让清单 4-26 顺利运行,我们需要修复可见性错误。清单 4-27 显示了解决方案。

internal open class Foo {
  internal fun boo() = println("boo")
  internal fun doo() = println("doo")
}

internal fun Foo.bar() {
  boo()
  doo()
}

fun main(args: Array<String>) {
  var fu = Foo()
  fu.bar()
}

Listing 4-27class Foo, Corrected Visibility Errors

访问修饰符

Kotlin 的访问修饰符是 final、open、abstract、override 。它们影响遗传。我们已经在本章前面使用了 final、open、覆盖,所以我们唯一没有使用的关键字是 abstract 。abstract 关键字在 Kotlin 中的含义与在 Java 中的含义相同。它适用于类和函数。

当你将一个类标记为抽象时,它也变成了隐式开放,所以你不需要使用开放修饰符,这就变得多余了。接口不需要声明为抽象开放,因为它们已经是隐式的了,抽象开放

对象声明

Java 的 static 关键字没有在 Kotlin 的关键字列表中出现。在 Kotlin 没有静态对等;取而代之的是,Kotlin 引入了对象伴侣关键字。

对象关键字允许我们同时定义一个类和它的实例。更具体地说,它只定义了该类的一个实例,这使得这个关键字成为在 Kotlin 中定义单例的好方法。清单 4-28 显示了对象关键字的基本用法。

object Util {
  fun foo() = println("foo")
}

fun main(args: Array<String>) {
  Util.foo() // prints "foo"
}

Listing 4-28Using the Object Keyword to Define a Singleton

我们用对象关键字代替关键字。这实际上是定义类并创建它的一个实例。为了调用这个对象中定义的函数,我们给点加上前缀(.)加上对象的名称——非常类似于我们在 Java 中调用静态方法的方式。

对象声明可以包含您可以在类中编写的大部分内容,比如初始化器、属性、函数和成员变量。唯一不能写在对象声明中的是构造函数。这样做的原因是因为你不需要构造函数。对象声明已经在定义时创建了一个实例,因此不需要构造函数。清单 4-29 显示了对象声明的一些基本用法和定义。

object Util {
  var name = ""
    set(value) {
      field = value
    }

  init {
    println("Initializing Util")
  }

  fun foo() = println(name)
}

fun main(args: Array<String>) {
  Util.name = "Bar"
  Util.foo() // prints "Bar"
}

Listing 4-29Initializers, Properties, Functions, and Member Variables in Object Declarations

章节总结

  • Kotlin 接口与 Java 的接口几乎相似,只是你可以在接口中声明属性,尽管它们仍然不允许有后台字段。像 Java 8 一样,Kotlin 接口可以有默认的实现。

  • Kotlin 类的定义与 Java 类略有不同。默认情况下,类是 final 和 public。

  • Kotlin 有两种构造函数:可以定义主构造函数和次构造函数。主构造函数是创建简单值对象的好方法。然而,要创建真正有用的值对象,Kotlin 的数据类是一个很好的方法。

  • Kotlin 拥有与 Java 几乎相同的控制可见性的机制,除了 Kotlin 用内部关键字替换了 Java 的包私有

在下一章,我们将涉足函数式编程的世界。

五、Lambdas 和高阶函数

我们将介绍的内容:

  • 高阶函数

  • 希腊字母的第 11 个

  • 关闭

  • 使用并应用

在第二章中,我们讨论了 Kotlin 函数的机制,你已经看到了它们与 Java 函数是多么的相似;你也看到了他们是多么的不同。在这一章中,我们将回到对函数的讨论,但是是一种不同的函数——支持函数编程的函数。你可能在 Java 8 中用过 lambdas 同样,Kotlin 也有对兰姆达斯的支持。在本章中,我们将探讨这两个主题。

高阶函数

高阶函数是对其他函数进行操作的函数,要么将它们作为参数接受,要么返回它们。术语高阶函数来自数学界,在数学界,函数和其他值之间有更正式的区别。

在我们开始讨论“为什么我们需要高阶函数?”我们需要注意它的结构。我们需要知道如何写它们,它们看起来像什么。关于高阶函数“为什么”的讨论可能会在后面的章节中出现,当我们讨论 Android 编程时,有很多机会可以很好地使用高阶函数。

下面的清单 5-1 展示了一个将另一个函数作为参数的函数的例子。

fun executor(action:() -> Unit) {
  action()
}

Listing 5-1A Function That Accepts Another Function

注意清单 5-1 中参数是怎么写的,动作是参数的名称,其类型写成()-> Unit,也就是说它的类型是函数。一个函数类型是用一对括号写的,后跟箭头操作符(一个破折号加上大于号),然后是函数应该返回的类型。在清单 5-1 的例子中,我们的函数参数不返回任何东西——因此它被声明为单元

这乍一看可能很奇怪,尤其是如果你没有使用过函数和变量被同等对待的语言。在 Kotlin 中,像任何支持高阶函数的语言一样,函数是一等公民。我们可以从任何可以传递(或返回)变量的地方传递(或返回)函数。在任何可以使用变量的地方,也可以使用函数。

让我们回到清单 5-1 。如果我们希望动作参数的类型为字符串,那么我们可以编写类似清单 5-2 中的内容。

fun executor(action:String) {
  action()
}

Listing 5-2If Action Was of Type String

但事实并非如此;我们希望动作是类型功能。在 Kotlin 中,函数不仅仅是一个命名的语句集合,它还是一种类型。所以,就像 StringInt、Float 一样,我们可以将一个变量声明为类型 function 。一个函数类型有三个组成部分:(1)带括号的参数类型列表;(2)箭头运算符;以及(3)返回类型。

在清单 5-1 中,带括号的参数类型列表是空的,但并不总是这样。它现在是空的,因为我们打算传递给executor()的函数不接受任何参数。executor()的返回类型是 Unit,因为我们打算传递给它的函数不返回任何值——也不总是这样,有时你可能想返回一个 Int 或 String。

现在我们已经了解了如何将参数声明为函数类型,让我们来看看如何将变量声明和定义为函数类型。参见清单 5-3 。

val doThis:() -> Unit  = {
  println("action")

}

Listing 5-3How to Declare and Define a Function Type

LHS(左手边)不需要太多解释,我们只是简单地声明一个名为doThis的变量是类型函数,这个函数不返回任何东西,所以它声明的返回类型是 Unit。RHS(右侧)看起来像一个没有标题的函数(关键字 fun 和函数名),这是一个 lambda。我们将在下一节讨论兰姆达斯。回到我们的代码示例,清单 5-4 展示了如何将executor()doThis放在一起。

| -什么 | `doThis`被声明并定义为*函数类型*。该函数的实现作为 RHS 上的 lambda 表达式给出。函数体不返回任何东西;因此,为函数指定的返回类型是`Unit` `.` | | ➋ | `executor()`是接受另一个函数作为参数的函数;该参数命名为`action`,类型为*函数*,写法为`()` → `Unit`。更具体地说,这个*函数类型*不返回任何东西——这就是为什么它被声明为`Unit.` | | ➌ | 通过在参数名上附加一对括号,我们可以调用函数。 | | -你好 | 这是调用`action`函数的另一种方式,但是像`action()`那样调用它更习惯,因此更受欢迎。 | | ➎ | 在主函数内部,我们可以调用`executor()`并传递`doThis`。注意,我们没有用括号传递`doThis()`。我们不想调用`doThis`,然后将结果值传递给`executor()`。我们想要传递的不是结果值,而是函数定义。这个想法是在`executor()`函数体内调用`doThis`。 |
val doThis:() -> Unit  = { ➊
  println("action")
}

fun executor(action:() -> Unit) { ➋
  action() ➌
  action.invoke() ➍
}

fun main(args: Array<String>) {
  executor(doThis) ➎
}

Listing 5-4Complete Code for doThis and executor() Examples

在清单 5-4 中,我们将doThis写成一个值为λ的属性。这很好,但是这可能不像是编写函数的自然方式。编写清单 5-4 的另一种方式如清单 5-5 所示。

| -什么 | `doThis`现在是以我们编写函数的通常方式定义的,在头中有`fun`关键字和函数名。 | | ➋ | 用双冒号调用`::doThis`。这意味着我们正在解析当前*包*中的函数。 |
fun doThis() { ➊
  println ("action")
}

fun executor(action:() -> Unit) {
  action()
}

fun main(args: Array<String>) {
  executor(::doThis) ➋
}

Listing 5-5Another Way of Writing the doThis and executor() Examples

Lambda 和匿名函数

Lambdas 和匿名函数被称为函数文字。这些函数没有声明,而是作为表达式直接传递给更高阶的函数。因此他们不需要名字。我们已经在本章前面使用了 lambda 表达式。在清单 5-3 中,我们定义了一个名为 doThis 的属性,它的类型是一个函数,但是这是一个相当冗长的处理函数类型的方法。我们实际上不需要显式地编写函数的返回类型,因为 Kotlin 可以为我们推断出它。清单 5-6 显示了清单 5-3 的更简洁版本。

val doThis = {
  println("action")

}

Listing 5-6Concise Version of Listing 5-3

正如您在上一节中看到的,这种代码旨在作为参数传递给更高阶的函数。但是您实际上可以使用它,而不用将它传递给更高阶的函数。要调用它,您可以做类似下面的事情——大概是在 function main 或任何其他顶级函数内部

doThis()

或者类似这样的东西

doThis.invoke()

前者看起来更自然;它也被认为更符合习惯,所以我们可能应该使用它。无论如何,lambda 表达式不应该这样使用。当用在高阶函数的上下文中时,它们真的闪闪发光。在清单 5-5 中,当我们将一个命名的 lambda 表达式传递给一个高阶函数时,我们使用了 lambda 表达式的完整语法形式。虽然你当然可以这样做,但这可能不是你在野外遇到 lambda 表达式的通常方式。清单 5-7 是清单 5-5 的重写,但是这一次,我们没有声明和定义一个已命名的 lambda,而是简单地将它作为参数传递给高阶函数执行器,如清单 5-7 所示。

| -什么 | 这是*函数的字面意思*。在清单 5-5 中,我们传递了一个属性`doThis,`,它的值是一个 lambda 表达式。在这个例子中,我们将 lambda 表达式本身直接传递给高阶函数。lambda 表达式包含在一对花括号中,就像函数体一样。 |
fun main(args: Array<String>) {
  executor(
    { println("do this") } ➊
  )
}

fun executor(action:() -> Unit) {
  action()
}

Listing 5-7Pass a lambda to a Higher Order Function

Lambda 表达式中的参数

考虑清单 5-8 中的代码。如果我们把它写成一个 lambda,它看起来像清单 5-9 。

{ msg:String -> println("Hello $msg") }

Listing 5-9display Function Written As lambda

fun display(msg:String) {
  println("Hello $msg")
}

Listing 5-8Simple Function to Display a String

您会注意到整个函数头、关键字 fun 和函数名都完全消失了,参数列表被重新定位在 lambda 表达式中。整个表达式用一对花括号括起来。在 lambda 表达式中,参数列表写在箭头操作符的左边,函数体写在右边。您还会注意到,lambda 表达式中的参数不需要在一对括号内,因为箭头操作符将参数列表与 lambda 的主体分开。

同样,在清单 5-9 中,可以省略参数中 String 的类型声明,这样就可以像清单 5-10 中那样写了。

{ msg -> println("Hello $msg") }

Listing 5-10Omitted Type Declaration in Parameter List

在某些情况下,lambda 表达式只接受一个参数,比如清单 5-10 中所示的代码示例,Kotlin 允许我们省略参数声明,甚至省略箭头操作符。我们可以用更短的方式重写清单 5-10 (参见清单 5-11 )。

{ println("Hello $it") }

Listing 5-11The Implicit It

如果上下文需要一个只有一个参数的 lambda,并且可以推断出它的类型,则生成参数名。清单 5-12 展示了如何在高阶函数的上下文中声明和使用 lambda 表达式的完整代码。现在我们有了 Hello World 示例的函数编程版本。

fun main(args: Array<String>) {
  executor({ println("Hello $it") })
}

fun executor(display:(msg:String) -> Unit) {
  display("World")
}

Listing 5-12Full Code for the lambda Example

编写和使用带有多个参数的 lambdas 与我们的单参数示例没有太大区别,只要您在箭头操作符的左侧编写参数列表。参见清单 5-13 中的示例。

fun main(args: Array<String>) {
  doer({ x,y -> println(x + y) })
}

fun doer(sum:(x:Int,y:Int) -> Unit) {
  sum(1,2)
}

Listing 5-13lambdas With More Than One Parameter

有时,高阶函数会将一些其他参数与函数类型一起接受。这样一个函数看起来像清单 5-14 。

fun executor(arg: String = "Mondo", display:(msg:String) -> Unit) {
  display(arg)
}

Listing 5-14Higher Order Function With Multiple Parameters

我们可以用这个调用这个函数

executor("Earth", {println("Hola $it")})

由于执行器的第一个参数有一个默认值,我们仍然可以像这样调用它

executor({println("Hola $it")})

Kotlin 允许我们对 lambdas 的语法更加精确。如果 lambda 是高阶函数中的最后一个参数,我们可以将 lambda 写在调用函数的括号之外,如下所示:

executor() { println("Hello $it")}

如果 lambda 是唯一的参数,我们甚至可以完全省略括号,就像这样:

executor { println("Hello $it")}

这种简化现在可能看起来没什么大不了的,但是我相信随着你编写越来越多的 lambda 表达式,你会欣赏到语法上的改进。Kotlin 标准库大量使用了这些东西。

关闭

当您在函数中使用 lambda 表达式时,lambda 可以访问它的闭包。闭包由外部作用域中的局部变量以及封闭函数的所有参数组成。参见清单 5-15 中的示例。

|

-什么

|

我们向executor()函数传递了一个int的列表。使用运算符形式(..)rangeTo 函数是一种生成从 1 到 1000 的整数列表的简便方法。但是你必须使用flatten()函数把它变成一个整型数的列表。

|
| --- | --- |
| ➋ | forEach是高阶函数;它接受一个 lambda,允许我们遍历列表中的项目。forEach只有一个参数,我们可以使用隐式的it参数名来访问这个参数。 |
| ➌ | sum变量是闭包的一部分;它在定义 lambda 的函数体内。兰姆达斯可以使用他们的闭包??。 |

fun main(args: Array<String>) {
  executor(listOf(1..1000).flatten()) ➊
}

fun executor(numbers:List<Int>) {
 var sum = 0;
 numbers.forEach {      ➋
   if ( it % 2 == 0 ) {
     sum += it          ➌
   }
 }
  println("Sum of all even numbers = $sum")
}

Listing 5-15lambda Accessing Its Closure

注意

在 Java lambdas 中,只有当一个变量是 final 时,才能在它的闭包中访问该变量。在 Kotlin 没有这种限制。

使用并应用

兰姆达斯在 Kotlin 被大量使用,他们的足迹遍布 Kotlin 的图书馆。在这一节中,我们将看看来自标准库的函数 withapply ,特别是来自 Standard.kt 的函数。这些函数展示了 Kotlin 的 lambdas 的功能,以及是什么让它们从 Java 同类产品中脱颖而出。Kotlin lambdas 能够调用不同对象的方法,而无需在 lambda 的主体中添加额外的限定符。这种兰姆达斯被称为带接收器的兰姆达斯

带有和 apply 的函数特别令人感兴趣,不是因为它们允许我们在同一个对象上执行多个操作,而不用重复对象的名称——这是一个受欢迎的特性——而是因为它们看起来像是被融入了语言中,但事实并非如此。它们只是由扩展函数λs使之变得特殊的函数。

清单 5-16 展示了一个简单类的定义以及如何设置它的一些属性。一个事件实例的创建及其各种属性的设置都发生在函数 main 内部。请注意,对于我们设置的每个属性,我们都必须显式地将属性解析回对象引用,这可能很好——毕竟,这就是我们在 Java 中的编码方式,这是意料之中的事情。

import java.util.Date

data class Event(val title:String) {
  var date = Date()
  var time = ""
  var attendees = mutableListOf<String>()

  fun create() {
    print(this)
  }
}

fun main(args: Array<String>) {

  val mtg = Event("Management meeting")

  mtg.date = Date(2018,1,1)
  mtg.time = "0900H"
  mtg.attendees.add("Ted")

  mtg.create()
}

Listing 5-16class Event

如果我们使用带有函数的来重构代码,它将看起来像清单 5-17 中的代码。

fun main(args: Array<String>) {

  val mtg = Event("Management meeting")

  with(mtg) {
    date = Date(2018,1,1)
    time = "0900H"
    attendees.add("Ted")

  }
}

Listing 5-17Using the With Function

带有函数的接受一个对象( mtg )和一个 lambda。在 lambda 内部,我们可以使用 mtg 对象,而不需要显式引用它。这之所以成为可能,是因为 mtg 对象被做成 lambda 的接收器——还记得第三章中的扩展函数吗?而且因为 mtg 是接收方,在 lambda 内部,这个关键字指向 mtg 对象。我们可以在代码中显式地引用这个,但是这不会比我们第一次使用这个例子时更好。通过省略对这个的显式引用,得到的代码要干净得多。此外,将 lambda 放在括号外的约定在这种情况下肯定有效,因为它使构造看起来好像带有的是 Kotlin 语言的一部分。

应用功能可以实现同样的事情;除了返回接收者(传递给它的对象)之外,它几乎与带有函数的非常相似——带有函数的不返回接收者。

| -什么 | `Apply`是一个扩展函数,而 *mtg* 对象成为它的*接收者。* | | ➋ | 又因为 *mtg* 对象是*接收者*,*这个*指的是 *mtg* 对象。 | | ➌ | lambda 返回时,返回*接收器*,是一个 *mtg* 对象;因此,我们可以将一些调用链接到其中。 |
 fun main(args: Array<String>) {

  val mtg = Event("Management meeting")

  mtg.apply {            ➊
    date = Date()        ➋
    time = "0900H"
    attendees.add("Ted")
  }.create()             ➌
}

标准版还有更多功能。Kt 喜欢等。,但是这两个使用 with 和 apply 的例子应该让我们对 lambdas 的能力有所了解。

章节总结

  • Kotlin 中的函数不仅仅是一个命名的语句集合。他们也是一个类型。一个函数类型可以在其他任何可以使用其他类型的地方使用——函数在 Kotlin 中是一等公民。

  • Lambdas 和匿名函数是函数文字。它们就像普通的函数,但是没有名字。它们可以作为表达式立即传递给其他函数。

  • Kotlin lambdas 不像他们的 Java lambdas(至少在撰写本文时是 Java 9),可以在其闭包中变异变量。

  • 高阶函数是对其他函数进行运算的函数。它们可以接受函数类型作为参数,或者返回函数类型。

在下一章,我们将探索 Kotlin 的集合类。

六、集合和数组

我们将介绍的内容:

  • 数组

  • 收集

  • 过滤并应用

现实世界中收藏的一个类比是一个钱包或一个装满硬币等各种东西的袋子。硬币将会是物品,而袋子本身就是收藏。因此,基于这个类比,我们可以说集合是各种各样的容器,其中可能有零个、一个或多个条目。你可能记得我们已经有了类似的东西——一个数组。该数组完全符合这一描述,因为它可以包含零个、一个或多个项目。如果是这样的话,我们真的需要学习其他容器吗?在这一章中,我们将看看 Kotlin 集合框架中的数组、集合和一些函数。

数组

从 Java 开始,在使用 Kotlin 数组之前,您需要稍微后退一步。在 Java 中,这些是特殊类型;他们在语言层面有一流的支持。在 Kotlin 中,数组只是类型;更确切地说,它们是参数化类型。如果您想创建一个字符串数组,您可能会认为下面的代码片段可能有用:

var arr = {"1", "2", "3", "4", "5"}

这段代码对 Kotlin 来说没有意义——它没有把数组当作特殊类型。如果我们想创建一个类似例子的字符串数组,我们可以用几种方法来实现。Kotlin 有一些库函数,如 arrayOf、emptyArrayarrayOfNulls ,我们可以用它们来简化数组的创建。清单 6-1 展示了如何使用 emptyArray 函数创建并填充一个数组。

var arr = emptyArray<String>();
arr += "1"
arr += "2"
arr += "3"
arr += "4"
arr += "5"

Listing 6-1Using the emptyArray Function

向 Kotlin 数组添加元素不像在 Java 中那样冗长,但是不要被漂亮的语法所迷惑。数组在创建时仍然是固定大小的,即使在 Kotlin 中也是如此。通过创建一个比旧数组更大的新数组,然后将旧数组的元素复制到新数组中,可以将元素添加到数组中。所以,你看,这仍然是一个昂贵的操作——即使我们有一个漂亮的甜语法。清单 6-2 展示了如何使用 arrayOfNulls 函数来做同样的事情。

var arr2 = arrayOfNulls<String>(2)
arr2.set(0, "1")
arr2.set(1, "2")

Listing 6-2Using the arrayOfNulls Function

arrayOfNulls 函数的整数参数是要创建的数组的大小。与清单 6-1 中的空数组不同,这个函数让您有机会为将要创建的数组提供一个大小。顺便说一下,你仍然可以对 Kotlin 数组使用括号语法,数组getset 方法只是方便的函数。清单 6-3 展示了括号语法以及新的 getset 函数的使用。

var arr2 = arrayOfNulls<String>(2)

// arr2.set(0, "1")
// arr2.set(1, "2")

arr2[0] = "1"
arr2[1] = "2"
println(arr2[0]) // same as arr2.get(0)
println(arr2[1])

Listing 6-3Get and Set Methods of Array

创建数组的另一种方法是使用函数的array。清单 6-4 显示了代码片段。

var arr4 = arrayOf("1", "2", "3")

Listing 6-4Using the arrayOf Function

这个函数可能是我们能得到的最接近 Java 数组文字的语法,这可能是为什么它被程序员更多地使用的原因。您可以将逗号分隔的值列表传递给函数,这样会自动填充新创建的数组。

最后,可以使用数组构造函数创建数组。构造函数接受两个参数,第一个参数是要创建的数组的大小,第二个参数是一个 lambda 函数,它可以返回每个元素的初始值。

var arr3 = Array<String>(5, {it.toString()})

Listing 6-5Using the Array Constructor

在大多数需要处理数字数组的情况下,使用数组类就足够了。然而,你需要记住,例如,Array<Int>,将 int 表示为整数对象,而不是整数原语。因此,如果您需要从代码中挤出更多的性能,并真正使用原始数字类型,您可以使用 Kotlin 的专用数组类型。

ByteArrayIntArray、ShortArray、LongArray 这样的专用类表示原始类型的数组(就像 Java 中的数组)。这些类型让您可以使用数组,而不需要像使用 number 原语的对象对应物的数组那样的装箱和拆箱开销。这些专用类型实际上并不继承自数组,但是它们有相同的方法和属性集。此外,它们有专门的工厂功能,使它们更容易使用。参见清单 6-6 中的示例。

var z = intArrayOf(1,2,3)
var y = longArrayOf(1,2,3)
var x = byteArrayOf(1,2,3)
var w = shortArrayOf(1,2,3)

println(Arrays.toString(z))
println(Arrays.toString(y))
println(Arrays.toString(x))
println(Arrays.toString(w))

Listing 6-6Special Array Types

我使用了Arrays.toString()函数,这样我们在打印内容时就可以得到可读的输出。如果你只是简单地打印没有帮助函数的数组,它看起来就像胡言乱语,就像这样

println(z) // outputs Ljava.lang.String;@6ad5c04e

遍历数组有几种方法。首先,您可以将可信任的用于循环,如清单 [6-7 所示。

for (i in z) {
  println("$i zee")
}

Listing 6-7Using a for Loop to Process Each Array Element

或者你可以使用 forEach 函数,就像这样。

y.forEach { i -> println("$i why") }

如果需要跟踪数组的索引和元素,可以使用 forEachIndexed 函数,如清单 6-8 所示。

x.forEachIndexed { index, element ->
  println("$index : $element")

}

Listing 6-8Using the forEachIndexed Function to Traverse the Array

在我们离开数组的主题之前,我们需要记住,如果您不希望数组的内容有任何重复,您必须自己编写程序逻辑。数组不能保证内容的唯一性。

虽然数组在许多情况下非常有用,但正如您在前面的讨论中看到的,它们也有局限性。向数组中添加新元素虽然语法友好,但仍然是一项开销很大的操作。如果不使用助手函数,就无法打印出来(虽然这没什么大不了的)。最后,它没有约束元素的工具(例如,强制唯一性)。在某些情况下,这些限制可能没什么大不了的,但在某些情况下,这些限制可能会成为交易杀手。因此,当我们遇到数组的限制时,我们就进入了集合的领域——它们帮助我们处理这样的限制。

作为开发工具包的一部分,集合框架的可用性对您来说可能不是一件大事。毕竟,你来自 Java,它有一个令人印象深刻的集合框架。但是你需要记住,在 Java、C#、Python 等语言之前。没有集合框架。程序员不得不编写他们自己的程序逻辑来处理诸如可调整大小的数组、后进先出访问、哈希表或哈希表等问题。这些不是简单的存储问题,而是数据结构问题。自己实现这种数据结构逻辑相当困难;有很多边缘情况需要纠正。尽管仍然有合理的理由实现自己的数据结构(可能是因为性能原因),但在大多数情况下,使用内置的集合框架会更好。

收集

Kotlin 收藏馆实际上是 JDK 收藏馆的直接实例。不涉及包装的转换。因此,如果您在使用 Java 时没有忽略对集合的研究,那现在肯定会派上用场。尽管 Kotlin 没有定义自己的集合代码,但它确实为框架添加了相当多的便利函数,这是一个受欢迎的附加功能,因为它使集合更容易使用。

在我们讨论代码示例和更多细节之前,需要说明一下为什么它被称为集合框架。之所以称之为框架,是因为数据结构本身非常多样化。其中一些限制了我们浏览整个系列的方式;它们强加了特定遍历顺序。一些集合约束数据元素的唯一性;他们不允许你放复制品。其中一些让我们成对地使用集合——就像在字典条目中,你将有一个具有相应值的键。

img/463887_1_En_6_Fig1_HTML.jpg

图 6-1

集合框架

图 6-1 显示了 Kotlin 集合框架的层次结构。在层次的顶端是接口 Iterablemutable talible——它们是我们将使用的所有集合类的父类。正如您在图中注意到的,每个 Java 集合在 Kotlin 中都有两种表示:一种是只读的,一种是可变的。可变接口直接映射到 Java 接口,而不可变接口缺少可变接口的所有 mutator 方法。

Kotlin 没有创建列表或集合的专用语法,但是它为我们提供了方便创建的库函数。表 6-1 列出了其中的一些。

表 6-1

Kotlin 收藏及其创作功能

|

募捐

|

只读

|

易变的

|
| --- | --- | --- |
| 目录 | listOf | mutableListOf, arrayListOf |
| 设置 | setOf | mutableSetOf, hashSetOf, linkedSetOf, sortedSetOf |
| 地图 | mapOf | mutableMapOf, hashMapOf, linkedMapOf, sortedMapOf |

注意

尽管 map 类没有从 Iterablemutable talible(图 6-1 )继承,但它在 Kotlin 中仍然表现为两个不同的版本:一个是可变的,一个是不可变的。

列表

列表是一种具有特定迭代顺序的集合。这意味着,如果我们向列表中添加几个元素,然后遍历它,这些元素会以非常特定的顺序出现——即它们被添加或插入的顺序。它们不会以随机的顺序或颠倒的时间顺序出现,而是精确地按照它们被添加的顺序出现。它意味着列表中的每个元素都有一个放置顺序,一个指示其顺序位置的索引号。要添加的第一个元素的索引为 0,第二个元素的索引为 1,第三个元素的索引为 2,依此类推。所以,就像数组一样,它是从零开始的。清单 6-9 显示了列表的基本用法。

| -什么 | 创建一个可变列表,构造函数允许我们传递一个变量参数来填充列表。在这种情况下,我们只通过了一个论点——我们本可以通过更多的论点。 | | ➋ | 向列表中添加元素;“Orange”将紧跟在“Apple”之后,因为我们没有指定插入的顺序位置。 | | ➌ | 向列表中添加另一个元素,但是这一次,我们告诉它应该将元素放在哪里。这一个碰撞“橙色”元素,然后插入它自己。自然,它后面的所有元素的序号位置或索引将会改变。 | | -你好 | 您可以按名称删除元素。当一个元素被移除时,它旁边的元素将取代它的位置。它后面的所有元素的顺序位置将相应地改变。 | | ➎ | 您也可以通过指定元素在列表中的位置来移除元素。 | | ➏ | 你可以问`first()`柠檬是否等于“草莓”。 | | -好的 | 还可以测试一下`last()`柠檬是否等于“香蕉”。 |
fun main(args: Array<String>) {

  val fruits = mutableListOf<String>("Apple") ➊
  fruits.add("Orange")      ➋
  fruits.add(1, "Banana")   ➌
  fruits.add("Guava")

  println(fruits)  // prints [Apple, Banana, Orange, Guava]

  fruits.remove("Guava")    ➍
  fruits.removeAt(2)        ➎

  println(fruits.first() == "Strawberries") ➏
  println(fruits.last() == "Banana")        ➐

  println(fruits) //  prints [Apple, Banana]
}

Listing 6-9Basic Usage of Lists

设置

集合和列表在操作和结构上都非常相似,所以我们所学的关于列表的所有东西也适用于集合。集合与列表的不同之处在于它们对元素的唯一性进行约束。它们不允许在一个集合中有重复的元素或相同的元素。对许多人来说,“相同”意味着什么似乎是显而易见的,但是 Kotlin 和 Java 一样,对“相同性”有特定的含义。当我们说两个对象相同时,这意味着我们已经对这两个对象进行了结构相等的测试。Java 和 Kotlin 都定义了一个叫做equals(),的方法,它允许我们确定对象之间的等价关系。这就是我们通常所说的“相同”清单 6-10 显示了集合的一些基本操作。

| -什么 | 创建一个可变集合,并通过向 creator 函数传递一个变量参数来初始化它。 | | ➋ | 这没有任何作用。它不会将“two”添加到集合中,因为元素“two”已经在集合中。 | | ➌ | 不管你加多少次“二”,集合都会拒绝它,因为它已经存在了。 | | -你好 | 另一方面,这将被添加,因为“三”在元素中还不存在。 | | ➎ | 从一个范围创建一个可变集。这是一种创建包含许多数字元素的集合(或列表)的简便方法。 | | ➏ | 这演示了如何使用 lambda 删除集合中的所有偶数。 |
val nums = mutableSetOf("one", "two")  ➊
nums.add("two")                        ➋
nums.add("two")                        ➌
nums.add("three")                      ➍

println(nums) // prints [one, two, three]

val numbers = (1..1000).toMutableSet() ➎
numbers.add(6)
numbers.removeIf { i -> i % 2 == 0 }   ➏

println(numbers)

Listing 6-10Basic Usage for Sets

地图

与列表或集合不同,映射不是单个值的集合;相反,它们是成对值的集合。把地图想象成字典或电话簿。它的内容使用键值对来组织。对于映射中的每个键,有且只有一个对应的值。在一个字典示例中,键将是术语的,其值将是术语的含义或定义

映射中的键是唯一的。像集合一样,贴图不允许重复的键。然而,映射中的值不受相同的唯一性约束;map 中的两对或更多对可能具有相同的值。清单 6-11 显示了地图的一些基本用法。

| -什么 | Ca 可变映射 | | ➋ | 向映射添加新的键和值 | | ➌ | 将`dict`图分配给一个新变量。这不会创建新的地图。它仅向现有地图添加一个对象引用。 | | -你好 | 向映射添加另一个键值对 | | ➎ | 打印{bar = 2,baz = 3,foo=1} | | ➏ | 还打印{bar = 2,baz = 3,foo=1},因为`snapshot`和`dict`都指向同一个地图。 | | -好的 | 使用键从映射中获取值 |
val dict = hashMapOf("foo" to 1)  ➊
dict["bar"] = 2                   ➋

val snapshot: MutableMap<String, Int> = dict ➌
snapshot["baz"] = 3                          ➍

println(snapshot)                 ➎
println(dict)                     ➏
println(snapshot["bar"]) // prints 2   ➐

Listing 6-11Basic Operations on a Map

现在我们已经看到了集合的一些基本用法的例子,您可能已经注意到它们有一些共同的特征——可能不是 100%像地图一样,但是列表和集合有相当多的重叠。使用集合框架的一个好处是整个集合中某些操作的一致性或规律性。例如,我们从列表工作中学到的技能和知识也可以很好地在集合和地图之间转换。因此,熟悉集合协议是一个好主意。表 6-2 列出了集合上一些更常见的操作。

表 6-2

集合的常见操作

|

功能或属性

|

描述

|
| --- | --- |
| Size | 告诉您集合中有多少元素。使用列表、集合和地图。 |
| isEmpty() | 如果集合为空,则返回 True,否则返回 False。使用列表、集合和地图。 |
| contains(arg) | 如果 arg 在集合中,则返回 True。使用列表、集合和地图。 |
| add(arg) | 向集合中添加参数。如果添加了 arg,则该函数返回 true 对于列表,将始终添加 arg。在集合的情况下,第一次将添加 arg 并返回 true,但是如果第二次添加相同的 arg,它将返回 False。在地图上找不到此成员函数。 |
| remove(arg) | 如果从集合中移除了 arg,则返回 True 如果集合未被修改,则返回 False。 |
| iterator() | 返回对象元素的迭代器。这是从 Iterable 接口继承的。使用列表、集合和地图。 |

集合遍历

到目前为止,我们已经知道如何使用基本集合。我们知道如何创建它们,以及如何在其中添加和删除项目。我们有效处理集合需要的另一项技能是循环遍历集合的能力。为此,让我们回到图 6-1 并回忆集合框架的继承结构。

在图 6-1 中,你会注意到集合继承了可迭代接口。一个可迭代的定义了一些可以被迭代或跳过的东西。当一个类继承了一个 Iterable 接口时,不管是直接的还是间接的,这意味着我们可以从中取出一个迭代器,并一个接一个地遍历它的元素。在每一步中,我们还可以提取每个元素的值——这取决于您的程序逻辑,您想用这些值做什么;例如,您可以转换它们,在算术运算中使用它们,或者将它们保存在存储器中。

我们可以使用各种方法来遍历集合中的元素。如果你喜欢,我们可以使用可靠的用于循环,但是使用更现代的用于每个更习惯——而且有点流行。清单 6-12 展示了如何使用 while 和 for 循环遍历列表。

val basket = listOf("apple", "banana", "orange")
var iter = basket.iterator()
while (iter.hasNext()) {
  println(iter.next())
}

for (i in basket) {
  println(i)
}

Listing 6-12Using while and for Loops for Collections

清单 6-12 可能与您在 Java 中处理集合的方式很相似,所以看起来应该很熟悉。清单 6-13 显示了使用 forEach 函数时的等效代码。

| -什么 | `forEach`的 lambda 表达式有一个隐式的`it`参数。`it`参数是当前元素的值。这个语句的意思是,对于*水果*中的每一项,执行 lambda 中的操作,在我们的例子中,lambda 就是`println().` | | ➋ | 同样的事情也适用于*集合* | | ➌ | 同样的事情也适用于*地图* | | -你好 | 这是上面第 3 条的变体,但是这一条允许我们分别处理*键*和*值*。 |
fruits.forEach { println(it) } ➊
nums.forEach { println(it) }   ➋

// for maps

dict.forEach { println(it) }   ➌
dict.forEach { t, u -> println("$t | $u") } ➍

Listing 6-13Using forEach

过滤和映射

为了有效地使用集合,过滤和映射是您需要掌握的基本技能的一部分。过滤允许我们有选择地使用集合中的元素。它缩小了范围。它基本上返回原始集合的子集。另一方面,映射允许我们转换元素或集合本身。

比方说,我们有一个数字列表——确切地说是整数,就像这样

val ints = (1..100).toList()

变量ints包含从 1 到 100 的整数列表,增量为 1。如果我们只想处理这个列表中的偶数,我们可以这样做:( 1)创建一个新列表;(2)迭代整数列表并对偶数执行模校验;然后(3)如果正在处理的当前元素是偶数,我们将其添加到新列表中。这些代码可能看起来像清单 6-14 。

val evenInts2 = mutableListOf<Int>()
for (i in ints) {
  if (i % 2 == 0) {
    evenInts2.add(i)
  }
}

Listing 6-14Using a for Loop to Sieve Out the Even Numbers

清单 6-14 可以被称为过滤事物的“必要”方式。没什么不好——就是有点啰嗦,仅此而已。但是它可读性很好,即使对于刚开始编程的人也是如此。然而,在 Kotlin 中,更惯用的缩小集合的方法是使用过滤器函数。如果我们用过滤器来做这件事,它会像这样

val evenInts = ints.filter { it % 2 == 0 }

我甚至不再给它贴上列表标签,因为这是不必要的——它只是一行。filter 函数是集合库中的标准函数。你已经知道花括号里的表达式是λ。然而,对于过滤器,更恰当的术语是 lambda 谓词。lambda 谓词也是一个函数文字,但是其中的表达式必须产生一个布尔值。

回到我们的例子,过滤器是针对一个集合调用的——例如,一个 int 列表。过滤操作的结果是一个较小的列表或子集。通过遍历每个元素并根据 lambda 谓词中指定的条件对它们进行测试,对列表进行了精简。任何通过谓词测试的项目都将包含在结果子集中。

让我们继续我们的例子,并与我们的偶数整数较小的列表。假设我们现在想要的是对偶整数列表中的每个元素求平方。这要求我们操作并转换列表中的每个元素,然后返回一个包含已转换元素的新列表。如果我们使用 for 循环来解决这个问题,它将看起来像清单 6-15 。

val squaredInts2 = mutableListOf<Int>()
for (i in evenInts2) {
  squaredInts2.add( i * i )
}
println(squaredInts2)

Listing 6-15Generate a List of Squared Ints Using a for Loop

或者我们可以在集合中使用 forEach 函数来解决这个问题。它看起来就像清单 6-16 。

val squaredInts2 = mutableListOf<Int>()
evenInts2.forEach { squaredInts2.add(it * it) }

Listing 6-16Generate a List of Squared Ints Using forEach

这实际上看起来更好,但是转换集合中的元素实际上是 map 函数的范围。所以,让我们用地图来解决整数平方的问题。清单 6-17 显示了代码。

val squaredInts = evenInts.map { it * it}
println("Sum of squares of even nos <= 100 is ${squaredInts.sum()}")

Listing 6-17Using the Map Function

清单 6-17 中唯一相关的语句是第一条。第二条语句只打印出从 1 到 100 的所有偶数的总和。另外,第二行展示了集合框架中的另一个内置函数,sum()函数。它的作用非常明显——它总结了集合中的值。

章节总结

  • 当处理一组值时,我们可以使用数组或集合。对于简单的数据结构使用数组,但是当您需要动态调整数据组的大小时,或者您需要对它施加更多的约束时,例如唯一性约束,集合可能会更好。

  • Kotlin 中的数组不同于 Java 中的数组;他们不享受特殊待遇。在 Kotlin 中,数组只是类。

  • 如果您觉得需要使用数组而不需要装箱和拆箱的开销,Kotlin 为数组提供了专门的类。

  • Kotlin 集合与 Java 集合非常相似,但是每个 Java 集合类都以两种方式表示:可变的和不可变的。

  • Kotlin 集合具有像 filter、map 和 sum 这样的内置函数,这使得使用集合变得更加容易。

在下一章,我们将探索 Kotlin 如何处理泛型。

七、泛型

我们将介绍的内容:

  • 使用泛型

  • 限制

  • 变化

  • 具体化的泛型

啊,仿制药。这个迂回的话题甚至出现在初学者的文章中。这个题目让许多初学者犯了错误,因为它很难理解,解释起来更难。但是我们需要处理它,因为没有泛型,就很难使用集合。

在很大程度上,Kotlin 泛型的工作方式与 Java 泛型相同;但是他们有一些不同。在这一章中,我们将看看如何使用泛型,以及 Kotlin 的泛型与 Java 的有多相似(或不同)——另外,不要太担心泛型的复杂性,在这一章中我们不会做任何疯狂的事情。

为什么是仿制药

泛型是在 2004 年左右来到 Java 的,当时 JDK 1.5 发布了。在泛型出现之前,你可以编写清单 7-1 中的代码。

List v = new ArrayList();
v.add("test");
Integer i = (Integer) v.get(0); // Run time error

Listing 7-1Using a Raw List, Java

你可能会说,“但是你为什么要做如此粗心和明显愚蠢的事情呢?从清单 7-1 中可以清楚地看到,我们在 ArrayList 中放了一个字符串;所以,不要做任何不适合字符串的操作。问题解决了。”这可能并不总是那么容易。示例代码显然是精心设计的,现在很容易发现错误,但是如果您正在做一些重要的事情,列表包含的内容可能并不总是很明显。

关于示例代码,需要注意的另一点——实际上也是最重要的一点——是代码可以顺利编译。你只能在运行时发现错误。编译器没有办法警告我们将要做的事情不是类型安全的。这是泛型试图解决的主要问题:类型安全。

回到清单 7-1 ,我们知道变量 v 是一个列表。如果我们知道列表中存储了哪些内容,那会更有用。正是在这些情况下,泛型是有帮助的。它允许我们说类似“这是一个字符串列表”或“这是一个整型数列表”这样的话——编译器事先就知道;因为编译器知道这一点,所以它可以防止我们做一些不恰当的事情,比如把一个字符串转换成 Int 或者用字符串做减法等等。清单 7-2 展示了如何在我们的代码中使用泛型。

List<String> v = new ArrayList<String>();
v.add("test");
Integer i = v.get(0); // (type error)  compilation-time error

Listing 7-2List, with Generics: Java

现在编译器已经预知了列表中有哪些内容,它可以防止我们在列表上进行不受支持的操作。

清单 7-1 和 7-2 中的代码在 Java 中都是有效的,这意味着您可以选择不在集合中使用泛型(原始类型)。Java 必须这样做,因为它需要保持与 JDK 5 之前编写的代码的向后兼容性。另一方面,Kotlin 不需要维护任何与遗留代码的兼容性。所以,在 Kotlin 中,你不能使用原始类型。所有 Kotlin 集合都需要类型参数。你总是必须使用泛型。

术语

泛型编程是 Kotlin 的一个语言特性。有了它,我们可以定义接受类型参数的类、函数和接口。参数化类型允许我们重用算法来处理不同的类型;这确实是参数多态性的一种形式。图 7-1 显示了类型参数和类型实参在泛型类中的位置。

| -什么 | **尖括号**。当一个类的名字后面有尖括号时,它被称为泛型类(也有泛型函数和接口)。 | | ➋ | **类型参数**。它定义了这个类可以处理的数据类型。您可以将其视为类实现的一部分。现在,我们使用字母 **T** 来表示类型参数,但是这是任意的。你可以随便叫它什么,可以是任何字母,也可以是字母的组合;如果我是你,我会坚持使用 **T** ,因为这是许多开发人员遵循的惯例。你可以在类内的整个代码中使用 **T** ,就像它是一个真实的类型一样。这是一个类型的占位符*。在本例中,我们使用 *T* 作为**项目**属性的类型,并作为 **getLeaf** 函数的返回类型。* | | ➌ | **类型自变量**。为了使用泛型类,你必须提供**类型参数**。现在我们正在创建节点类的一个实例, **T** 将被*类型参数*所替代(在本图中为 *Int* 和 *String* )。 |

img/463887_1_En_7_Fig1_HTML.png

图 7-1

类型变量和类型参数

在前面的章节中你已经看到了泛型代码,具体在 6 (集合)。Kotlin 的所有集合类都使用泛型。我之前说过,Kotlin 没有原始类型。不可能只创建一个列表—你必须明确它是哪种列表(例如,一个“字符串列表”列表<字符串> 或“整数列表”列表< Int >)。

在函数中使用泛型

若要创建泛型函数,请在函数名之前声明类型参数。然后,您可以在函数中的任何地方使用类型参数。

| -什么 | 类型参数 **T** 用作函数参数 **arg 的类型。** | | ➋ | 我们只是返回串接在字符串中的**参数**。 |
fun <T> fooBar(arg:T) : String { ➊
  return "Heya $arg"  //  ➋
}

println(fooBar("Joe"))  // prints "Heya Joe"
println(fooBar(10))     // prints "Heya 10"

这很容易理解。我们只是在一个地方使用了 param 类型,不管 param 是什么类型,函数都返回一个字符串。另一个例子,见清单 7-4 。

| -什么 | 在这个例子中,我们使用*类型参数*作为**arg**(fooBar 函数的参数)的类型以及函数本身的返回类型。 | | ➋ | 我们正在测试 **arg** 是否是字符串类型。如果是的话,我们也有效地把它转换成一个字符串;聪明的演员,记得吗? | | ➌ | 我们将返回“Hello world”,并(强制)将其转换为 **T** 。我们不能在这里返回“字符串”类型,因为 fooBar 期望返回类型 **T** 给它的调用者,而不是字符串。 |
fun <T> fooBar(arg:T) : T {  ➊
  var retval:T = 0 as T
  when (arg) {
    is String -> {          ➋
      retval = "Hello world" as T  ➌
    }
    is Number -> {
      retval = 100 as T
    }
  }
  return retval
}

Listing 7-4A More Complex fooBar Function

您还可以将泛型用于扩展函数。如果你正在创建一个处理列表的函数,你可能希望它能处理任何类型的列表,而不仅仅是字符串或整型。清单 7-5 展示了如何在扩展函数中使用泛型。

| -什么 | 可以使用*接收方* **(列表< T > )** 中的类型参数和扩展函数的返回类型。 | | ➋ | 我们不要做任何花哨的事情;让我们返回一个给定索引的项。在生产代码中,您可能希望在返回索引之前检查它是否存在。如果您忘记了这个指的是什么,它指的是列表本身(它是 receiver 对象)。 | | ➌ | 我们的扩展函数处理字符串列表。 | | -你好 | 它也适用于一个整型列表。 | | ➎ | 这个有点花里胡哨,但是最后还是返回了一个列表,所以我们的扩展函数应该还是可以的。 |
fun <T> List<T>.getIt(index:Int): T { ➊
  return this[index] ➋
}

fun main(args: Array<String>) {

  val lfruits = listOf("Apples", "Bananas", "Oranges") ➌
  val lnumbers = listOf(1,3,5)    ➍
  val lnumlist = (1..100).toList().filter { it % 5 == 0 } ➎

  println(lnumlist.getIt(5))
  println(lfruits.getIt(1))
}

Listing 7-3fooBar, Generic Function

在类中使用泛型

像在 Java 中一样,您可以通过在类名后面放一对尖括号并将类型参数放在尖括号之间来创建 Kotlin 泛型类。之后,您可以在类中的任何地方使用类型参数。清单 7-6 展示、注释并解释了如何编写一个泛型类。

| -什么 | 类型参数声明在类名**节点之后。我们使用 **T** 作为参数**项的类型。**** | | ➋ | 我们还使用 **T** 作为函数 **getLeaf 的返回值。** | | ➌ | 我们向 Node 的构造函数传递一个 Int。我们可以详细一点,指定 Int 作为类型参数,**节点< Int >。** | | -你好 | 节点可以推断出类型参数是什么,所以我们可以跳过尖括号。这样写也没问题。 | | ➎ | 因为它是一个泛型类,所以它也可以处理字符串。 |
class Node<T>(val item:T) {   ➊
  fun getLeaf() : T {         ➋
    return item
  }
}

fun main(args: Array<String>) {
  val m = Node<Int>(1)           ➌
  val n = Node(1)                ➍
  val o = Node<String>("World")  ➎
}

Listing 7-6Writing a Generic Class

您可以约束或限制可用作类或函数的类型参数的类型。目前,我们的节点类应该可以处理任何类型,因为类型参数的默认父类(或上界)如果不指定约束,是 any 吗?(可空类型,所以包含问号)。

当您为类型参数指定上限约束时,这将限制可用于实例化该类的类型。例如,如果我们希望节点类只接受整型、双精度型或浮点型,我们可以使用 Number 作为上限约束。代码示例见清单 7-7 。

| -什么 | 现在我们对类型参数 **<** **T:编号** **>** 进行约束。我们可以用来实例化这个类的唯一类型必须是数量为**的**的子类型。 | | ➋ | Int 是数字的子类型,所以没问题。 | | ➌ | 浮动也可以。 | | -你好 | 这已经行不通了。IntelliJ 会告诉你“类型参数不在界限内”。 | | ➎ | 这对 Double 仍然有效,因为它是 number 的子类。 |
class Node<T:Number>(val item:T) { ➊
  fun getLeaf() : T {
    return item
  }
}

fun main(args: Array<String>) {
  val m = Node<Int>(1)           ➋
  val n = Node(1.0F)             ➌
  val o = Node<String>("World")  ➍
  val p = Node(1.0)              ➎
}

Listing 7-7Node Class, with Constraint

如果除了类型参数的可空性之外没有任何限制,可以简单地使用 Any 作为类型参数的上限;参见清单 7-8 。

class Node<T:Any>(val item:T) {
  fun getLeaf() : T {
    return item
  }
}

Listing 7-5Generics in Extension Function

Listing 7-8Prevent Null Type Arguments

变化

我们需要回顾一些面向对象编程(OOP)的基础知识,为讨论差异做准备。希望我们能唤起你的记忆,记住一些 OOP 的基本原则。

OOP 是开发者的福音;正因为如此,我们可以编写类似清单 7-9 的代码。

val a:Int =  1
val b:Number = a

println("b:$b is of type ${b.javaClass.name}")

Listing 7-9Assign an Int Variable to Number Type

我们也可以编写类似清单 7-10 的函数。

foo(1)
foo(100F)
foo(120)

fun foo(arg:Number) {
  println(arg)
}

Listing 7-10Function That Accepts a Number Type

清单 7-9 和 7-10 中的代码是可能的,因为利斯科夫替代原理 (LSP)。这是 OOP 中最重要的部分之一——在需要父类型的地方,你可以用子类型来代替它。我们使用更一般化的类型(如清单 7-10 中的)的原因是,将来如果需要,我们可以编写一个子类型的实现,并插入到现有的工作代码中。这是开闭原则的精髓(声明一个类必须对扩展开放,但对修改关闭)。

注意

利斯科夫替代原理开闭原理是立体设计原理的一部分。这是 OOP 中最流行的设计原则之一。实线代表(S)单一责任(O)开/闭(L)利斯科夫替代(I)界面分离和(D)依赖性倒置

再举一个例子,见清单 7-11 。

| -什么 | employee_1 的类型是 **Employee** ,我们给它分配了一个**程序员**对象。这没关系。程序员是员工的一个*子类型*。 | | ➋ | 这里同样的事情,类型**测试者**是**雇员**的一个子类型,所以分配应该没问题。 |
open class Employee(val name:String) {
  override fun toString(): String {
    return name
  }
}

class Programmer(name:String) : Employee(name) {}
class Tester(name:String) : Employee(name) {}

fun main(args: Array<String>) {
  val employee_1 :Employee = Programmer("Ted")  ➊
  val employee_2 :Employee = Tester("Steph")    ➋

  println(employee_1)
  println(employee_2)
}

Listing 7-11Employee, Programmer, and Tester

毫无疑问,利斯科夫原理仍然在起作用。即使您将程序员和雇员放在一个列表中,类型关系也会保留。

val list_1: List<Programmer> = listOf(Programmer("James"))
val list_2: List<Employee> = list_1

Listing 7-12Employee and Programmer in Lists

目前为止,一切顺利。下一个代码是什么?你认为这行得通吗?(参见清单 7-13 。)

class Group<T>
val a:Group<Employee> = Group<Programmer>()

Listing 7-13Group of Employees and Programmers

这是泛型最棘手的部分之一。清单 7-13 ,按照目前的情况,是行不通的。即使我们知道程序员雇员的子类型,并且我们所做的是类型安全的,编译器也不会让我们通过,因为代码中的第二条语句有问题。

当你使用泛型时,永远记住默认情况下组<雇员>,组<程序员>,组<测试员> 没有任何类型关系——即使我们知道测试员和程序员是雇员的子类型。默认情况下,类中的类型参数是不变量。为了使第二条语句(在清单 7-13 中)起作用,组< T >** 必须是协变的。我们将在清单 7-14 中解决。**

| -什么 | 当您将 **out** 关键字放在类型参数之前时,这使得类型参数*协变。* | | ➋ | 这段代码之所以有效,是因为,**组<程序员>** 是现在*组**员工<>**的一个子类型,这要归功于 **out** 关键字。* |
class Group<out T>    ➊

open class Employee(val name:String) {
  override fun toString(): String {
    return name
  }
}
class Programmer(name:String) : Employee(name) {}
class Tester(name:String) : Employee(name) {}

fun main(args: Array<String>) {
  val a:Group<Employee> = Group<Programmer>() ➋
}

Listing 7-14Classes Employee, Programmer, Tester, and Group

从这些例子中,我们现在可以归纳出,如果 type Programmer 是 Employee 的一个子类型,并且是协变的,那么组<程序员>是组< Employee >的一个子类型。此外,我们可以归纳出,如果对于给定的类型雇员程序员组<程序员> 不是组<雇员>的子类型,那么泛型类就像 Group 一样,在类型参数上是不变的。****

现在我们已经处理了不变量协变量。我们需要处理的最后一个术语是逆变。如果组< T > 的类型参数是逆变的,对于相同的给定类型雇员和程序员,那么我们可以说组<雇员>组<程序员> 的一个子类型——与共变正好相反。

| -什么 | 关键字中的**使类型参数**<****T****>**逆变,意思是;** | | ➋ | 类型**组<雇员>** 现在是**组<程序员>的子类型。** |
class Group<in T> ➊

open class Employee(val name:String) {
  override fun toString(): String {
    return name
  }
}
class Programmer(name:String) : Employee(name) {}
class Tester(name:String) : Employee(name) {}

fun main(args: Array<String>) {
  val a:Group<Programmer> = Group<Employee>()  ➋
}

Listing 7-15Use the in Keyword for Contravariance

子类与子类型

好吧。我怀疑你在过去 10 分钟里读到的东西让你觉得很苦涩。怎么会出现程序员员工的子类型,列表<程序员>是列表<员工>、的子类型,而组<程序员>** 不是**组<员工>的子类型?让我们通过回到类、类型、子类和子类型的概念来尝试回答这个问题。****

我们认为类在某种程度上是类型的同义词,一般来说是这样的——至少对于非泛型类来说是这样,在大多数情况下也是这样。我们知道一个类至少有一种类型——它与类本身的类型相同。回到你第一次学习 Java 类的时候——你的老师、导师或者可能是你最喜欢的作者一定是这样定义一个对象的类型的:“它是它所有公共行为的总和,或者被称为对象的方法或契约”或者类似的东西。我们姑且说它是对象拥有的行为集合。

回到“一个类至少有一种类型”,它可以有更多类型。只看图 7-2 。

img/463887_1_En_7_Fig2_HTML.png

图 7-2

一堆类和接口的层次结构

从图 7-2 中,我们可以说:

  • 任何都在类图表的顶部;类 Any 相当于 java.lang.Object

  • 员工Any 的子类。Employee 有两种类型:一种是从 Any 继承的,另一种是它自己——因为 Employee 类可以定义自己的一组行为(方法),所以可以算作一种类型。

  • 程序员Employee 的子类,Any 的子类,也就是说程序员有三种类型:一种来自 Any,一种来自 Employee,还有一种来自程序员类本身。

  • Any 的子类型,但它也实现了compatible接口。因此,Number 有三种类型:一种来自 Any,另一种来自自身,还有一种来自可比接口。我们可以说,数字是任何的一个子类型,也是可比的一个子类型——无论你期望可比做什么,数字都能做;任何人能做的事,数字也能做。这是基本的 OOP。

  • 字符串类有四种类型:一种来自 Any ,另一种来自 Comparable ,另一种来自 CharSequence,,最后来自自己的类。

根据陈述和图表,可以互换使用子类和子类型。两者没有太大区别。当我们开始考虑可空类型时,它们的区别将变得明显。

可空类型就是一个子类不同于子类型的例子。见图 7-3 。

img/463887_1_En_7_Fig3_HTML.png

图 7-3

可空类型

当你在一个类型的名字后面加上一个问号时,它就变成了该类型的可空版本。在 Kotlin 中,我们可以从同一个类中创建两种类型:可空版本和不可空版本。我们真的不能说程序员程序员的子类?因为程序员只有一个类定义,而程序员(不可空版本)是程序员的子类型?(可空的那个)。同样, AnyAny 的子类型?但是有吗?不是 Any 的子类型——反方向不成立。

写没问题

var j:Programmer? = Programmer("Ted") // assign non-null to nullable Programmer
j = null. // then we assign a null to j

但是不可以写

var i:Programmer = j // assign j (which is null) to non-nullable Programmer

现在我们来看泛型。图 7-4 应该有助于我们阐明我们需要解决的下一组概念。

img/463887_1_En_7_Fig4_HTML.png

图 7-4

泛型类型

我们知道第一个关系雇员程序员的父类型。我们还知道列表<员工>会接受列表<程序员>;我们在清单 7-12 中对此进行了测试——您可能不太清楚它为什么会工作,所以在我们处理完第三组盒子后,我将回到这一点。****

现在,给定密码

class Group<T>
val a:Group<Employee> = Group<Programmer>() // not sure

为什么我们不能可靠地回答“难道集团<员工>集团<程序员> 的超类型?”

这是因为虽然是一个类,但是组<雇员> 不是,并且推而广之,组<程序员> 不是组<雇员> 的子类——如果你现在想列出<雇员>和<程序员>,请停止。我说过我会回到那个话题。先和团队<员工>和团队<程序员>在一起。表 7-1 应该可以帮助我们总结其中的一些东西。

表 7-1

类别与类型

|   |

是类

|

是 a 型

|
| --- | --- | --- |
| Programmer | 是 | 是 |
| Programmer? | 不 | 是 |
| List | 是 | 是 |
| List<Programmer> | 不 | 是 |
| Group | 是 | 是 |
| Group<Programmer> | 不 | 是 |

现在我们可以确定组与组没有类型关系,即使类 Employee 与程序员有类型关系。默认情况下,组中的类型参数是不变量(没有类型关系)。为了改变< T >的方差,你需要使用 out (使其协变)或 in(使其逆变)关键字。

所以,如果我们想让组成为组的子类型,我们需要这样写类:

class Group<out T>
val a:Group<Employee> = Group<Programmer>() // this is ok now

现在我们可以循环回到列表和列表问题。为什么以及如何工作?为什么写这个可以?

var m:List<Employee> = listOf(Programmer("Ted"))

简单的答案在于 List 接口的定义,为了方便你,我复制了 List7-16 中 List 接口的源代码;我把所有的评论都删了。

| -什么 | 类型参数是协变的。List 在类型参数 **E.** 前使用 **out** 关键字 |
public interface List<out E> : Collection<E> { ➊
    override val size: Int
    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>
    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
    public operator fun get(index: Int): E
    public fun indexOf(element: @UnsafeVariance E): Int
    public fun lastIndexOf(element: @UnsafeVariance E): Int
    public fun listIterator(): ListIterator<E>
    public fun listIterator(index: Int): ListIterator<E>
    public fun subList(fromIndex: Int, toIndex: Int): List<E>
}

Listing 7-16Excerpt of the List Interface Source Code

之所以把 List 赋给 List 没问题,是因为 List 上的类型参数是协变。因此,如果类型员工程序员的超类型,并且列表< E > 是协变的,那么列表<程序员>列表<员工>的子类型。

现在我们对类型和子类型有了更好的理解,就像在昆汀·塔伦蒂诺的电影中一样,我希望你们回到 20 分钟前,再读一遍关于“方差”的部分。

具体化的泛型

我们先来处理一下“具体化”的含义。它的意思是“让事情变得真实”,我们在同一个语句中使用 rify 和 generics 的原因是因为 Java 的类型擦除

类型擦除的意思和你想的完全一样。Java 和 Kotlin 在运行时清除泛型类型信息。这有很好的理由,但不幸的是,我们不会讨论语言设计如此的原因——但我们会讨论它的影响。因为类型擦除,你不能执行任何反射活动,也不能对一个类型做任何运行时检查,如果它是泛型的话。参见清单 7-17 中的示例。

| -什么 | 这不会编译。错误是"*无法检查擦除类型的实例。* |
fun checkInfo(items:List<Any>) {
    if(items is List<String>) {       ➊
      println("item is a list of Strings")
    }
  }
}

Listing 7-17Check for Type at Runtime

在运行时, is 关键字对泛型类型不起作用;由于类型擦除,智能转换中断。如果你对列表的运行时类型有一些信心,你可以做一个推测性的决定,使用作为关键字进行转换,就像这样:

val i = item as List<String>

编译器会让你通过,但这是一件危险的事情。让我们再考虑一个例子,我们可以建立一个更强的案例来解释为什么我们需要在运行时保留类型信息。

假设我有一个对象列表,程序员和测试员对象。我想创建一个函数,可以传递一个类型参数,并使用该类型参数过滤列表。我希望函数返回过滤后的列表。清单 7-18 向我们展示了如何做到这一点的代码示例——由于类型擦除问题,该代码示例当然不会工作,但是先通读一遍,我们稍后会修复它。

| -什么 | 让我们创建一个程序员和测试人员对象的列表。 | | ➋ | 让我们调用一个名为 **typeOf** 的扩展函数(列表类型的)。我们将**程序员**作为类型参数传递,这意味着我们希望这个函数只返回程序员对象的列表。 | | ➌ | 我们只是遍历列表中的每一项。我们打印了 *name* 属性和 Java simpleName。 | | -你好 | 现在我们来看扩展函数的定义。我们正在定义一个类型参数,我们使用 **T** 作为这个函数的返回类型。此外,我们希望这个函数可以处理任何类型的列表——这就是语法。 | | ➎ | 让我们定义一个可变列表;我们将用它来保存过滤后的列表。 | | ➏ | 这是无法编译的代码,因为我们不知道在运行时这是什么类型的列表。像 Java 一样,Kotlin 删除类型信息。但是让我们假设 Kotlin 确实保留了泛型类型信息;如果是这样的话,那么这段代码是没问题的。 | | -好的 | 如果条件没问题,我们把当前项加到返回值上。 | | -好的 | 最后,让我们返回过滤后的列表。 |
fun main(args: Array<String>) {
  val mlist = listOf(Programmer("Ted"), Tester("Steph"))    ➊
  val mprogs = mlist.typeOf<Programmer>()                   ➋

  mprogs.forEach {                                          ➌
    println("${it.toString()} : ${it.javaClass.simpleName}")
  }
}

fun <T> List<*>.typeOf() : List<T> {                         ➍

  val retlist = mutableListOf<T>()                           ➎
  this.forEach {
    if (it is T) {                                           ➏
      retlist.add(it)                                        ➐
    }
  }
  return retlist                                             ➑
}

open class Employee(val name:String) {
  override fun toString(): String {
    return name
  }
}
class Programmer(name:String) : Employee(name) {}
class Tester(name:String) : Employee(name) {}

Listing 7-18Filtering a List Using a Type Parameter

如果 List.typeOf 能够在运行时记住它是什么类型的列表,列表 7-18 将会完美地工作。为了解决这个问题,我们将使用内联具体化关键字。清单 7-19 向我们展示了如何做到这一点。

| -什么 | 使函数**内联**并在类型参数前使用**具体化的**关键字。这样做之后,函数可以在运行时保留类型信息。 |
inline fun <reified T> List<*>.typeOf() : List<T> { ➊

  val retlist = mutableListOf<T>()
  this.forEach {
    if (it is T) {
      retlist.add(it)
    }
  }
  return retlist
}

Listing 7-19How to Use Reified and Inline in a Function

您只能具体化内联函数。当您内联一个函数时,编译器会用它的实际字节码(不仅仅是函数的地址)替换对该函数的每个调用。这就像在调用函数的地方复制并粘贴函数的字节码一样。这就是编译器知道您用作类型参数的确切类型的方式。因此,编译器可以为用作类型参数的特定类生成字节码。

所以,如果我们打这样一个电话:

val mprogs = mlist.typeOf<Programmer>()

如果我们对编译器将为我们的具体化函数生成的字节码进行逆向工程,它可能看起来像清单 7-20 。

val retlist = mutableListOf<Programmer>()
this.forEach {
  if (it is Programmer) {
    retlist.add(it)
  }
}
return retlist

Listing 7-20Reified Function

如你所见,我们不再测试是否是 T——我们测试是否是程序员。生成的字节码引用了特定的类(程序员),而不是类型参数(T)。这就是具体化函数不受类型擦除影响的原因。这当然会增加运行时程序的大小,所以要谨慎使用。清单 7-21 显示了具体化示例的完整和修改后的代码。

fun main(args: Array<String>) {
  val mlist = listOf(Programmer("Ted"), Tester("Steph"))
  val mprogs = mlist.typeOf<Programmer>()

  mprogs.forEach {
    println("${it.toString()} : ${it.javaClass.simpleName}")
  }
}

inline fun <reified T> List<*>.typeOf() : List<T> {

  val retlist = mutableListOf<T>()
  this.forEach {
    if (it is T) {
      retlist.add(it)
    }
  }
  return retlist
}

open class Employee(val name:String) {
  override fun toString(): String {
    return name

  }
}
class Programmer(name:String) : Employee(name) {}
class Tester(name:String) : Employee(name) {}

Listing 7-21Filtering a List Using a Type Parameter

章节总结

  • 泛型编程让我们可以重用算法。

  • Kotlin 中的所有集合都使用泛型。

  • Kotlin 没有原始类型,像 Java。

  • 有三个方差你需要知道:(1)不变性;(2)协方差;(3)逆变。

  • Kotlin 和 Java 一样,在运行时删除泛型类型信息;但是如果你想保留类型信息,内联你的函数并使用具体化的关键字。

这是本书 Kotlin 部分的结尾。在下一章,我们将开始讨论 Android 编程。我们将通过设置 Android Studio 开发环境来解决这个问题。

八、Android Studio 简介和设置

我们将介绍的内容:

  • Android 概述

  • 历史

  • 工具作业

  • 设置

对于不同的人来说,Android 可能意味着很多事情,但是既然你拿着这本书,我想你会对 Android 中适合开发者的部分感兴趣。Android 是一个由操作系统、软件库、应用框架、软件开发工具包、预建应用和参考设计组成的平台。平台及其发展生态系统都随着时间的推移而演变。

在这一章,我们将看看 Android 的历史和架构。我们还将讨论 Android Studio 以及如何设置它。

历史

2003 年的某个时候,安迪·鲁宾创立了一家名为 Android Inc .的公司,Android 由此诞生。当时,谷歌已经在支持安卓公司,但还没有拥有它。谷歌在 2005 年的某个时候收购了安卓公司;然后在 2007 年,开放手机联盟诞生了,Android OS 正式开源。在这段时间里,Android 还没有达到 1.0 版本,还远未成为主流。Android 在 2008 年达到了 1.0 版本——当时甜点的名字还没有成为文化的一部分,但用不了多久它们就会成为文化的一部分。

接下来的两年,从 2009 年到 2010 年,见证了一股快速发行的洪流:纸杯蛋糕、甜甜圈、甜甜圈、圣克莱尔和姜饼版本在此期间发行。

2011 年是一个重要的里程碑,因为在那之前,Android 操作系统仍然局限于手机。Honeycomb 是 Gingerbread 的继任者,是第一个安装在平板电脑上的 Android 版本。Honeycomb 引起了一些争议,因为谷歌没有立即开放源代码。

表 8-1 显示了 Android 历史的简要总结。

表 8-1

安卓的历史

| **2003 年** | 由安迪·鲁宾创建并得到谷歌支持的安卓公司诞生了 | | **2005 年** | 谷歌收购了安卓公司。 | | **2007 年** | Android 是官方开源的。谷歌将其所有权移交给了开放手机联盟(OHA) | | **2008 年** | 发布了 1.0 版本 | | **2009 年** | 发布了版本 1.1、1.5(纸杯蛋糕)、1.6(甜甜圈)和 2.0(艾克蕾尔) | | **2010 年** | 版本 2.2 (Froyo)和 2.3 (Gingerbread)已经发布 | | **2011 年** | 发布了 3.0 版(蜂巢)和 4.0 版(冰淇淋三明治) | | **2012 年** | 版本 4.1 (Jellybean)发布了 | | **2013 年** | 发布了 4.4 版(KitKat) | | **2014 年** | 5.0-5.1 版本(棒棒糖)发布;Android 变成了 64 位 | | **2015 年** | 6.0 版本(棉花糖)发布 | | **2016 年** | 发布了 7.0-7.1.2 版本(牛轧糖) | | **2017 年** | 版本 8(奥利奥)发布 | | **2018** | 版本 9 (Android P,beta)发布 |

体系结构

Android 最明显的部分,至少对开发者来说,是它的操作系统。操作系统是一个复杂的东西,但在大多数情况下,它是用户和硬件之间的桥梁。这太简单了,但对我们的目的来说已经足够了。我所说的“用户”并不是指最终用户或个人。我的意思是应用,程序员创造的一段代码,就像文字处理器或电子邮件客户端。

以电子邮件应用为例:当你在键盘上键入每个字符时,该应用需要与硬件通信,以便消息到达你的屏幕和硬盘,并最终通过你的网络发送到云端。这是一个比我在这里描述的更复杂的过程,但这是基本的想法。最简单地说,操作系统做三件事:

  1. 代表应用管理硬件

  2. 为网络、安全、内存管理等应用提供服务。

  3. 管理应用的执行;这是允许我们几乎同时运行多个应用的部分

图 8-1 展示了 Android 平台的逻辑架构。

img/463887_1_En_8_Fig1_HTML.jpg

图 8-1

Android 的逻辑架构

图的最底层是 Linux 内核。它负责与硬件接口,以及其他事情。它还负责各种服务,如内存管理和进程执行。

Linux 是一个非常稳定的 OS,并且相当普遍;你会发现这个操作系统被广泛使用。它可以在小到手表,大到服务器农场的东西上运行。Android 内部有一个嵌入式 Linux,处理硬件接口和其他一些内核功能。

在 Linux 内核之上是低级库,如 SQLite、OpenGL 等。这些不是 Linux 内核的一部分,但仍然是低级的,因此,大部分是用 C/C++ 编写的。在同一层面上,你会发现 android 运行时(android 类库+ dalvik 虚拟机),这是 Android 应用运行的地方。

接下来是应用框架层。它位于底层库和 android 运行时之上,因为它需要这两者。这是我们作为应用开发人员将与之交互的层,因为它包含了我们编写应用所需的所有库。

最后,最上面是应用层。这是我们所有应用的所在地,包括我们编写的应用和预构建的应用。应该指出的是,与我们将要编写的应用相比,设备自带的预构建应用没有任何特权。如果你不喜欢手机的电子邮件应用,你可以自己编写并替换它。安卓就是这样民主的。

Android Studio IDE

为 Android 开发应用并不总是像今天这样方便。当 Android 1.0 在 2008 年发布时,开发人员通过开发工具包获得的只是一堆命令行工具和 Ant 构建脚本。如果你已经习惯了使用 Vim、Ant 和其他命令行工具来构建应用,这并没有那么糟糕,但是很多开发人员并不习惯这样。缺少代码提示、项目设置和集成调试等 IDE 功能在某种程度上是入门的障碍。

令人欣慰的是,Eclipse IDE 的 android 开发工具(ADT)也是在 2008 年发布的。Eclipse 过去是,现在仍然是许多 Java 开发人员最喜欢的 IDE 选择。很自然地,它也将成为 Android 开发者的首选 IDE。

从 2009 年到 2012 年,Eclipse 一直是开发 IDE 的选择。android SDK 在结构和范围上也经历了重大和渐进的变化。2009 年,SDK 管理器发布;我们用它来下载工具、单个 SDK 版本和可以用于模拟器的 android 图像。2010 年,发布了针对 ARM 处理器和 X86 CPUs 的附加映像。

2012 年是重要的一年,因为 Eclipse 和 ADT 最终被捆绑在一起,这是一件大事,因为在那之前,开发人员必须分别安装 Eclipse 和 ADT,安装过程并不总是顺利的。因此,将两者捆绑在一起使得开始 Android 开发变得更加容易。2012 年也值得纪念,因为它标志着 Eclipse 成为 android 主流 IDE 的最后一年。

2013 年 Android Studio 发布;可以肯定的是,它仍然处于测试阶段,但是不祥之兆已经很明显了。它将成为 Android 开发的官方 IDE。Android Studio 基于 JetBrains 的 IntelliJ。IntelliJ 是一个商业 Java IDE,也有一个社区(非付费)版本。这个版本将作为 Android Studio 的基础。

有相当多的 JVM 语言,但 Java 一直是 Android 开发的首选语言——直到 2017 年,谷歌 I/O 宣布 Android 将对 Kotlin 提供一流的支持。Android Studio 3 (AS3)自动支持 Kotlin。

设置

JDK 是 Android Studio 的必备软件,但是因为我们已经在第一章中介绍了 JDK 的安装,我们将继续安装 AS3。安装程序可用于 macOS、Windows 和 Linux 下载页面位于http://bit.ly/getas3—该页面应该能够检测到您正在使用的操作系统,并将为您显示合适的安装程序。您将被要求同意一些条款和条件,然后才能继续下载。阅读它,理解它,并同意它,这样你就可以继续下去。之后,AS3 安装程序将以压缩文件的形式下载。

对于 macOS,您需要执行以下操作:

  1. 解压缩安装程序的压缩文件。

  2. 将应用文件拖到应用文件夹中。

  3. 推出 AS3。

  4. AS3 会提示你导入一些设置,如果你有以前的安装。您可以导入它,这是默认选项。

注意

如果您已经安装了 Android Studio,您可以继续使用该版本,并安装预览版。AS3 可以和你现有版本的 Android Studio 共存;其设置将保存在不同的目录中。

对于 Windows,您需要执行以下操作:

  1. 解压缩安装程序文件。

  2. 将解压后的目录移动到您选择的位置,例如C:\ Users \ my name \ AndroidStudio

  3. 向下钻取到 AndroidStudio 文件夹;在里面,你会发现 studio64.exe 的。这是您需要启动的文件。为这个文件创建一个快捷方式是个好主意——如果你右击 studio64.exe 并选择“固定到开始菜单”,你可以从 Windows 开始菜单中使用 AS3。或者,你也可以把它钉在任务栏上。

Linux 安装需要做的工作比简单地双击并遵循安装程序提示要多一些。在 Ubuntu 及其衍生产品的未来版本中,这可能会发生变化,变得像 Windows 和 macOS 一样简单和无摩擦,但现在,我们需要做一些调整。Linux 上的额外活动主要是因为 AS3 需要一些 32 位库和硬件加速。

注意

本节中的安装说明适用于 64 位 Ubuntu 和其他 Ubuntu 衍生产品(例如,Linux Mint、Lubuntu、Xubuntu、Ubuntu MATE 等)。).我选择这个发行版是因为我认为它是一个非常常见的 Linux 版本;因此,本书的读者将会使用该发行版。

如果你运行的是 64 位版本的 Ubuntu,你需要安装一些 32 位的库,这样 AS 才能正常工作。

要开始获取 Linux 的 32 位库,请在终端窗口上运行以下命令:

sudo apt-get update && sudo apt-get upgrade -y
sudo dpkg --add-architecture i386
sudo apt-get install libncurses5:i386 libstdc++6:i386 zlib1g:i386

所有准备工作完成后,您需要执行以下操作:

  1. 解压下载的安装文件。您可以使用命令行工具或 GUI 工具来解包文件。例如,您可以右键单击该文件,然后选择“在此解包”选项,如果您的文件管理器有该选项的话。

  2. 解压文件后,将文件夹重命名为 AndroidStudio。

  3. 将文件夹移动到您拥有读取、写入和执行权限的位置。或者,你也可以把它移到 /usr/local/AndroidStudio。

  4. 打开一个终端窗口,进入 AndroidStudio /bin 文件夹,然后运行。/studio.sh.

  5. 第一次启动时,AS3 会问你是否要导入一些设置。如果您已经安装了以前版本的 Android Studio,您可能需要导入这些设置。

Android Studio 配置

如果这是您第一次安装 AS3,您可能希望在开始编码工作之前先配置一些东西。在这一部分,我将带您了解以下内容:

  • 获得更多我们需要的软件,以便创建针对特定版本 Android 的程序

  • 确保我们拥有所有需要的 SDK 工具;并且可选地

  • 改变我们获取 AS3 更新的方式

如果你还没有启动 AS3,那么点击“配置”,如图 8-2 所示。从下拉列表中选择“首选项”。

img/463887_1_En_8_Fig2_HTML.jpg

图 8-2

从 AS3 开始屏幕转到首选项

您将看到“首选项”窗口,如图 8-3 所示。在窗口的左侧,单击“Android SDK”

img/463887_1_En_8_Fig3_HTML.jpg

图 8-3

SDK 平台

当您进入 SDK 窗口时,启用“显示包细节”选项,这样您就可以看到每个 API 级别的更详细的视图。我们不需要下载 SDK 窗口中的所有内容。我们将只得到我们需要的物品。

SDK 级别或平台号是 Android 的特定版本。Android 8 或“奥利奥”是 API 等级 26 和 27,牛轧糖是 API 等级 24 和 25。你不需要记住平台号,至少不再需要,因为 AS3 用相应的 Android 昵称显示平台号。

如果你愿意,你可以下载“牛轧糖”和“奥利奥”;这些是 API 等级 24、25、26 和 27。出于我们的目的,请下载“棉花糖”——它是 API 等级 23。这是我们在整本书中最常使用的版本。请确保在下载平台的同时,您还将下载“Google APIs 英特尔 x86 Atom_64 系统映像”当我们测试运行我们的应用时,我们将需要这些。

选择一个 API 级别现在可能没什么大不了的,因为在这一点上,我们只是在练习应用。当您计划向公众发布您的应用时,您可能不会轻易做出这个选择。为你的应用选择一个最低的 SDK 或 API 级别将决定有多少人能够使用你的应用。在撰写本文时,所有安卓设备中有 25%在使用“棉花糖”,22%在使用“牛轧糖”,4%在使用“奥利奥”。这些统计数据来自开发者的仪表板页面。安卓。com 。不时检查这些统计数据是个好主意;你可以在这里找到 http://bit.ly/droiddashboard

回到我们的配置,当您对您的选择满意时,启用您想要下载的 API 和图像的复选框,然后单击“SDK 工具”——它就在“SDK 平台”按钮的旁边,如图 8-4 所示。

img/463887_1_En_8_Fig4_HTML.jpg

图 8-4

SDK 工具

你通常不需要改变这个窗口上的任何东西,但是检查一下你是否有工具也无妨,如表 8-2 所示,标记为“已安装”

表 8-2

SDK 工具

|

工具

|

描述

|
| --- | --- |
| Android SDK 构建工具 | 这包含了像 adb 这样的重要工具,它将帮助我们进行诊断和调试;sqlite3,我们在创建使用数据库的应用时可以使用它;加上一些其他工具。 |
| Android SDK 平台工具 | 这包含了像 adb 这样的重要工具,它将帮助我们进行诊断和调试;sqlite3,我们在创建使用数据库的应用时可以使用它;加上一些其他工具。 |
| Android SDK 工具 | 这包括基本的 Android 工具,如 ProGuard。您不需要深入研究这些工具的细节(目前)。只要确保这个框被选中,我们就可以开始了。 |
| 安卓模拟器 | 你肯定会用这个。这是一个设备仿真工具。我们将使用它在虚拟设备中测试我们的应用。 |
| 支持知识库 | 如果你想写针对 Android Wear,Android TV,或者 Google Cast 的代码,你要下载这个。它还包含本地 Maven 资源库以支持库。支持库还允许您在旧版本的 Android 上使用新功能。 |
| HAXM 安装程序 | 如果你使用的是 macOS,或者是搭载英特尔处理器的 PC,你可以使用这个。它是 Android 模拟器的加速器。 |

注意

如果你在 Linux 平台上,你不能使用 HAXM,即使你有一个 Intel 处理器。KVM 将用于 Linux,而不是 HAXM。

一旦你对你的选择满意,点击“确定”按钮开始下载软件包。

我们要做的最后一项配置检查是“更新通道”它在同一个“偏好设置”窗口中。点击右侧的“更新”项,显示“更新”设置,如图 8-5 所示。

img/463887_1_En_8_Fig5_HTML.jpg

图 8-5

更新

AS3,就像任何 Android Studio 安装一样,默认配置为从您最初下载安装程序的渠道获取更新。因为我们从稳定的渠道下载了安装程序,所以默认情况下它会从那个渠道获得更新。您可以将频道切换到以下四个频道之一:

  • 金丝雀频道:这是最新版本,每周都会更新。您不希望将它用于生产代码。

  • 开发频道:就像金丝雀频道,但更稳定一些。你还是不想用这个做生产。

  • Beta 通道:这包含发布候选。在反馈到稳定的渠道之前,开发人员基本上都在等待反馈。

  • 稳定渠道:这是官方稳定发布,适合生产工作。

硬件加速

在你编写应用的时候,不时地测试和运行它是很有用的,这样可以得到即时的反馈,并发现它是否像预期的那样运行,或者它是否在运行。为此,您将使用物理或虚拟设备。每个选项都有其利弊,你不必选择一个而不是另一个。事实上,你最终将不得不使用这两个选项。

Android 虚拟设备或 AVD 是一个仿真器,你可以在其中运行你的应用。在模拟器上运行有时会很慢——这就是谷歌和英特尔开发 HAXM 的原因。这是一个模拟器加速工具,让测试你的应用变得更容易忍受。这对开发者来说绝对是福音。也就是说,如果您使用的是支持虚拟化的英特尔处理器,并且您没有使用 Linux。但是,如果你不够幸运,没有落到馅饼的那一部分,也不用担心;在 Linux 中有很多方法可以实现模拟器加速,我们将在后面看到。

macOS 用户可能最容易拥有,因为 HAXM 是自动随 AS3 安装的。他们不需要做任何事情就可以得到它 AS3 安装程序会为他们做好准备。

Windows 用户可以通过以下方式获得 HAXM:

  • https://software.intel.com/en-us/android 下载。像安装其他 Windows 软件一样安装它,双击它,然后按照提示进行操作。

  • 或者,你可以通过 AS3 的 SDK 管理器获得 HAXM 这是推荐的方法。

对于 Linux 用户,推荐的软件是 KVM。KVM(基于内核的虚拟机)是一个用于 Linux 的虚拟化解决方案。它包含虚拟化扩展(英特尔 VT 或 AMD-V)。

要获得 KVM,我们需要从回购中提取一些软件。但是在做其他事情之前,你需要做两件事:

  1. 确保在 BIOS 或 UEFI 设置中启用了虚拟化。关于如何获得这些设置,请查阅您的硬件手册。它通常包括关闭电脑,重新启动电脑,并在听到系统扬声器的声音时按下中断键,如 F2 或 DEL,但正如我所说的,请查阅您的硬件手册。

  2. 一旦您完成了更改,并重新启动到 Linux,请检查您的系统是否可以运行虚拟化。这可以通过从终端egrep –c '(vmx|svm)' /proc/cpuinfo运行以下命令来完成。如果结果是一个大于零的数字,这意味着您可以继续安装。

要安装 KVM,在终端窗口上键入命令,如清单 8-1 所示。

sudo apt-get install qemu-kvm libvirt-bin ubuntu-vm-builder bridge-utils
sudo adduser your_user_name kvm
sudo adduser your_user_name libvirtd

Listing 8-1Commands to Install KVM

您可能需要重新启动系统才能完成安装。

章节总结

  • Android 是完整的开发平台。它包括操作系统、应用框架、应用、软件开发套件、预构建的应用和参考设计

  • Android 的发布周期大约是 12 个月;我们每年都有新版本。

  • AS3 自动包含对 Kotlin 的支持。

  • 仿真器的硬件加速是你可能想要研究的东西。它将在开发和测试过程中节省大量的等待时间。

下面是下一章的内容:

  • 一个安卓应用里面有什么?我们将探索应用的组成部分;Android 称之为组件,有好几个。我们将逐一查看。

  • 我们将创建我们的第一个项目。我们将逐步介绍如何在 Android Studio 中启动并运行一个简单的项目。

  • 我们将构建一个仿真器——它是用来测试应用的。Android devs 称之为 AVD,是 Android 虚拟设备的简称。

  • 我们将看看 Android Studio IDE 的某些部分。了解你的工具的各个角落总是好的。

九、入门指南

我们将介绍的内容:

  • Android 组件

  • 创建项目

  • 创建 android 虚拟设备

  • Android Studio IDE

android 中的应用与为桌面编写的应用不太一样。就外表而言,它们可能有一些惊人的相似之处,但在结构上却有很大的不同。EXE 文件包含应用需要的所有例程和子例程。有时它可能依赖于一些动态加载的库,但是可执行文件几乎是自包含的。Android 应用不完全是这样,它们由松散耦合的组件组成,这些组件使用 Android 平台特有的消息传递机制相互通信。

在这一章中,我们将仔细看看 Android 应用的内部。我们还将通过创建和运行一个示例应用来熟悉 Android Studio 3。最后,我们将简单浏览一下 Android Studio 3 IDE。

应用中有什么

android 应用不像 Windows 中的 EXE 文件那样是一个单一的包。它是一个松散组装的组件和其他资源的捆绑包,它们一起保存在一个 Android 包文件或 APK 中。图 9-1 显示了一个假想应用的逻辑结构。

img/463887_1_En_9_Fig1_HTML.png

图 9-1

什么构成了一个应用

图 9-1 中描述的应用是一个大型应用——它拥有一切,包括厨房水槽。你的应用不需要包括所有这些东西,就像我们这里假设的应用;但你的肯定会包括其中一些。

活动、服务、广播接收者和内容提供者被称为 Android 组件。它们是应用的关键组成部分。它们是对有用事物的高级抽象,如向用户显示屏幕、在后台运行任务、广播事件以便感兴趣的应用可以响应它们等。组件是具有非常特定行为的预编码或预构建的类,我们通过扩展它们在应用中使用它们,以便我们可以添加我们的应用特有的行为。

构建一个 Android 应用很像建造一座房子。有些人用传统方式建造房屋——他们组装横梁、支柱、地板等。他们像工匠一样,用原材质手工制作门和其他配件。如果我们以这种方式构建 android 应用,可能会花费我们很长时间,而且可能会非常困难。对于一些程序员来说,从头构建应用所需的技能可能遥不可及。在 Android 中,应用是使用组件构建的。把它想象成房子的预制构件。零件是预先制造好的,只需要组装就可以了。

一个活动是我们把用户可以看到的东西放在一起的地方。是用户可以做的专注的事情。例如,可以有目的地使用户能够查看单封电子邮件或填写表单。它是用户界面元素粘合在一起的地方。如图 9-1 所示,活动内部有视图片段。视图是用于将内容绘制到屏幕中的类;视图对象的一些例子是按钮和文本视图。片段类似于活动,因为它也是一个组合单元,但是更小。像活动一样,它们也可以持有视图对象。大多数现代应用使用片段来解决在多种外形上部署应用的问题。片段可以根据可用的屏幕空间和/或方向打开或关闭。

服务是允许我们在不冻结用户界面的情况下运行程序逻辑的类。服务是在后台运行的代码;当你的应用需要从网上下载文件或者播放音乐时,它们会非常有用。

BroadcastReceivers 允许我们的应用监听来自 Android 系统或其他应用的特定消息——是的,我们的应用可以发送消息并在系统范围内广播。例如,如果您希望在电池电量下降到 10%以下时显示警告消息,您可能希望使用 BroadcastReceivers。

ContentProviders 允许我们创建能够与其他应用共享数据的应用。它管理对某种中央数据存储库的访问。有些内容提供商有自己的用户界面,但有些没有。您使用该组件的主要原因是允许其他应用访问您的应用的数据,而无需通过一些 SQL 技巧。数据库访问的细节对他们是完全隐藏的(客户端应用)。Android 中的“联系人”应用就是一个作为内容提供者的预建应用的例子。

您的应用可能需要一些视觉或听觉素材;这些就是我们在图 9-1 中所说的“资源”。

AndroidManifest 顾名思义——它是一个清单,并且是 XML 格式的。它声明了关于应用的一些事情,比如

  • 应用的名称

  • 当用户启动应用时,哪个活动将首先显示

  • app 里有什么样的组件。如果它有活动,清单会声明它们——类名和所有的名称。如果应用有服务,它们的类名也将在 manifest 中声明。

  • 这个应用可以做哪些事情?它的权限是什么?允许上网还是相机?它能记录 GPS 位置吗?等等。

  • 它使用外部库吗?

  • 此应用将在哪个(些)版本的 Android 上运行?

如你所见,舱单是一个繁忙的地方,有很多事情要留意。不过这个文件不用太担心。AS3 的创建向导会自动处理这里的大部分条目。为数不多的几个与它交互的场合之一可能是当你需要给你的应用添加权限的时候。

组件激活

Android 热衷于松散耦合。应用只是由清单文件保存在一起的组件的集合。这些组件中的每一个都可以通过向其发送消息来激活。这种程序交互性的方法非常独特,因为它非常以用户为中心。它赋予用户很大的权力来选择如何操作和创建数据。

让我们举一个 Android 设备的常见使用场景。例如,用户打开“联系人”应用并选择 John Doe 的联系人详细资料。比方说,这个联系人可能有一个电子邮件地址、一部手机和一个 twitter 用户名。用户可以点击 John 的每一个联系点,每次,Android 都会启动一个不同的应用;默认的电子邮件客户端、拨号器和下载的 Twitter 应用。用户可能不关心启动了哪个应用或者当前打开了多少个应用;他只是想传达一个信息。如果这位用户不喜欢电子邮件应用或默认的 twitter 客户端,他可以删除这些应用,并用其他东西替换它们,他应该会回到业务中。

为了实现这种程序交互,Android 需要设计平台,重点关注松散耦合和可插拔性。像联系人应用这样的组件不应该知道当电子邮件地址或手机号码被窃听时应该使用什么应用的任何具体细节。用于特定类型数据的应用的解决方案不应硬连线到联系人应用中;否则,用户将无法在发送电子邮件或推文时选择使用哪个应用。

这就是意图的来源。当一个组件拥有超出其服务能力的数据或信息时,它可以使用 Intents 访问 Android 平台,并询问是否有任何应用能够(或想要)满足请求。有两种意图:隐含的和明确的。我们在电子邮件和 twitter 示例中讨论的意图被称为显式意图。我们将在后面的章节中更深入地探讨这一点。

Android Intents 是一种组件激活机制。它们是一种消息传递机制,如果您想要激活任何 Android 组件,无论是活动、服务、内容提供者还是广播接收者,都可以使用。要激活任何组件,您需要创建一个意图,并将其传递给想要激活的组件。在具有多个活动的应用中,意图用于将控制或焦点从一个活动切换到另一个活动。

创建项目

现在,我们对 Android 应用内部有了一些工作思路,让我们尝试创建一个示例项目并试用 ide。如果 AS3 还没有打开,就启动它。图 9-2 为 Android Studio 3 的欢迎画面。

点击“开始一个新的 Android Studio 项目”,如图 9-2 所示。检查一下你是否有互联网连接可能是个好主意。AS3 使用 Gradle 构建工具;当创建向导完成时,Gradle 将从 internet 存储库中提取几个文件。图 9-3 显示下一个屏幕。

img/463887_1_En_9_Fig2_HTML.jpg

图 9-2

AS3 欢迎屏幕

如图 9-3 所示,您需要填写一些关于项目的信息(例如,应用名称、公司域名和项目位置)。应用名称的默认值是“我的应用”;您可以保留默认值。

img/463887_1_En_9_Fig3_HTML.jpg

图 9-3

创建新项目

我填了公司域名;如果你愿意,你也可以。一般是你公司的网站。该信息将在项目中使用,并将成为反向 DNS 表示法中的包名。因此,我们的类将存储在名为 com.example.ted 的包中。

项目位置是 AS3 将存储项目的文件夹的位置。您也可以保留默认值。

启用“包含 Kotlin 支持”复选框很重要,因为我们将使用 Kotlin 作为编程语言。点击“下一步”

图 9-4 显示了下一个屏幕。在这里,您将被要求选择您的应用预期运行的 Android 版本。只勾选“手机和平板”,选择 API 23。

img/463887_1_En_9_Fig4_HTML.jpg

图 9-4

目标 Android 设备

图 9-5 显示下一个画面;可能会出现一个小弹出窗口,提醒您需要安装“即时应用”现在单击“否”。“即时应用”是 Google Play 的一项功能,允许用户在不安装应用的情况下使用或试用应用。如果用户喜欢这个应用,那么如果有必要,他们可以从应用商店购买。我们暂时完全忽略这一点。点击“下一步”

img/463887_1_En_9_Fig5_HTML.jpg

图 9-5

即时应用

在下一个屏幕上,如图 9-6 所示,我们被要求向应用添加一个活动。您有几个选择,但是对于我们的目的,选择“空活动”点击“下一步”

img/463887_1_En_9_Fig6_HTML.jpg

图 9-6

选择一项活动

项目创建向导的最后一个屏幕如图 9-7 所示。我们被要求填写活动名称和布局名称。我们将保留所有内容的默认值。点击“下一步”

img/463887_1_En_9_Fig7_HTML.jpg

图 9-7

配置活动

图 9-8 显示了我们在 AS3 主窗口中新创建的项目。在点击图 9-7 中的“下一步”按钮后,需要一段时间事情才会尘埃落定,因为 Gradle 工具会构建项目,当它试图这么做时,它会从存储库中提取相当多的文件。

img/463887_1_En_9_Fig8_HTML.jpg

图 9-8

带有打开项目的主 AS3

我们现在不会试图改变这个项目中的任何东西。我们的目标是简单地试用 AS3,熟悉项目创建的各个步骤。项目创建向导已经生成了一个带有几个视图的活动。我们测试的下一步是在模拟器中运行项目。为此,单击工具栏中的运行图标(图 9-8 中的圆圈)。

当您点击运行图标时,将出现“选择部署目标”屏幕,如图 9-9 所示。该屏幕显示所有运行的 Android 虚拟设备(avd)。它还显示了所有连接的物理 Android 设备,如果你插入了任何设备。

img/463887_1_En_9_Fig9_HTML.jpg

图 9-9

选择部署目标

如您所见,我已经创建了几个虚拟设备。在您的情况下,您可能看不到“可用虚拟设备”下的任何内容,因为您有一个全新的安装。单击“创建新的虚拟设备”

在图 9-10 中,您可以选择虚拟设备的外形。我选择了 Nexus 5x。点击“下一步”

img/463887_1_En_9_Fig10_HTML.jpg

图 9-10

选择硬件

图 9-11 显示了系统映像的选项。系统映像是我们可以在模拟器上运行的 Android 操作系统的副本。我们的项目是用 API 23(“棉花糖”)的目标 SDK 值创建的。选择高于 API 23 的系统映像是可以的,但是出于我们的目的,让我们实际下载 API 23 系统映像。

img/463887_1_En_9_Fig11_HTML.jpg

图 9-11

系统映像

点击中间标签“x86 Images”,如图 9-11 所示,使用 Google APIs 查找 API level 23,x86_64。单击“下载”链接。

图 9-12 显示了组件安装程序窗口,它显示了下载的进度。完成后,单击“完成”关闭窗口。

img/463887_1_En_9_Fig12_HTML.jpg

图 9-12

组件安装程序

我们再次回到系统图像窗口,如图 9-13 所示。您会注意到“棉花糖”标签旁边的“下载”链接不再可见,该行现在是可选的。当棉花糖行被选中时,点击“下一步”

img/463887_1_En_9_Fig13_HTML.jpg

图 9-13

系统映像

图 9-14 显示了创建 AVD 的最终配置屏幕。我将保留所有的默认值,包括 AVD 名称。点击“完成”

img/463887_1_En_9_Fig14_HTML.jpg

图 9-14

Android 虚拟设备

我们回到了“选择部署目标”屏幕(图 9-15 ),但这一次,我们新创建的 AVD (Nexus 5X API 23)显示在“可用虚拟设备”中选择我们刚刚创建的 AVD,然后单击“OK”

img/463887_1_En_9_Fig15_HTML.jpg

图 9-15

选择部署目标

AS3 可能会提示您安装“即时运行”,如图 9-16 所示。我们想安装这个,因为它将加快我们的开发时间。即时运行允许我们将代码更改推送到 AVD,而无需构建新的 APK。那会节省我们的时间。点按“安装并继续”

img/463887_1_En_9_Fig16_HTML.jpg

图 9-16

瞬间奔跑

AS3 将为应用创建 APK,并在之后立即将其推送到 AVD。完成后,您应该能够看到应用在 AVD 中运行,如图 9-17 所示。

img/463887_1_En_9_Fig17_HTML.jpg

图 9-17

Android 虚拟设备

IDE

让我们花点时间熟悉一下 IDE。在深入研究编码之前,最好先了解一些情况。Android Studio 基于 IntelliJ,我们在前面的章节中使用 IntelliJ 进行 Kotlin 研究,所以 AS3 应该看起来很熟悉。图 9-18 显示了一个打开项目的 AS3 IDE。

img/463887_1_En_9_Fig18_HTML.jpg

图 9-18

带有已打开项目的 AS3 IDE

编辑器窗口是最突出的窗口,拥有最多的屏幕空间。在编辑器窗口中,您可以创建和修改项目文件。它会根据您正在编辑的内容改变外观。如果您正在处理程序源文件,此窗口将只显示源文件。编辑布局文件时,您可能会看到原始 XML 文件或布局的可视化渲染。

Android Studio 中的每个项目都包含一个或多个带有源代码文件和资源文件的模块。模块的类型包括 Android 应用模块、库模块,有时还包括 Google 应用模块。AS3 默认在 Android 视图中显示项目文件,如图 9-18 所示。Android 视图由模块组成,提供对项目最相关文件的快速访问。您可以通过点击项目窗口顶部的向下箭头来改变查看项目文件的方式,如图 9-19 所示。

img/463887_1_En_9_Fig19_HTML.jpg

图 9-19

如何在项目窗口中切换视图

导航栏允许您浏览项目文件。这只是“项目文件”窗口的一个更紧凑的视图。这是一个水平排列的人字形集合,类似于一些网站上可以找到的面包屑导航。您可以通过导航栏或项目工具窗口打开项目文件。

工具栏允许您执行各种操作(例如,保存文件、运行应用、打开 AVD 管理器、打开 SDK 管理器、撤销、重做操作等)。).

工具窗口让您可以访问非常具体的任务(例如,查看项目文件、查看所有待办事项注释、查看 logcat 窗口、访问 profiler 等。).每个工具窗口都是可展开和可折叠的。你可以在需要的时候把它们打开,用完后把它们藏起来。

工具窗口栏沿着 IDE 窗口的周边运行。它包含激活特定工具窗口所需的各个按钮。

状态栏是 IDE 的一部分,显示你的项目和 AS3 本身的进展。它显示上下文相关的消息,如错误消息、正在运行的流程、存储库消息等。

炙单

Android Studio 提供了许多导航 IDE 的方式,但是主要的导航方式是主菜单。图 9-20 显示 AS3 主菜单;它位于 IDE 的顶部,提供了最完整的导航方式。它包含用于打开、创建项目、重构代码、运行和调试应用、将文件置于版本控制之下等等的命令。

img/463887_1_En_9_Fig20_HTML.jpg

图 9-20

Android Studio 的主菜单

快捷键

随着应用的增长,您可能想尝试一种更快的方式来导航 AS3。这里有一些键盘快捷键让你开始。

表 9-1

一些键盘快捷键

|

工作

|

Linux 和 Windows

|

苹果

|
| --- | --- | --- |
| 在文件中搜索 | CTRL + F | ≤??] |
| 到处搜索 | CTRL + Shift + F | + F``+ F |
| 全部保存 | CTLR + S | ≤??] |
| 覆盖方法 | CTRL + O | CTRL + O |
| 实现方法 | CTRL + I | CTRL + I |
| 基本代码完成 | CTRL + Space | CTRL + Space |
| 建设 | CTRL + F9 | ≤??] |
| 构建并运行 | Shift + F10 | CTRL + R |
| 应用更改(通过即时运行) | CTRL + F10 | + R``+ R |

表 9-1 中显示的快捷键列表明显不完整。Android 开发者网站维护着一个页面,上面有 Android Studio 键盘快捷键的完整列表;你可以在这里找到 http://bit.ly/androidstudiokbshortcuts

AS3 主菜单中的某些操作或选项没有默认的键盘映射(例如,进入全屏视图)。在这种情况下,您可以将自己选择的键盘快捷键映射到菜单操作。您可以在 AS3 的键图设置中完成此操作。

要打开键图设置,从主菜单中选择文件设置(在 macOS 上, Android Studio首选项,导航到键图窗格,如图 9-21 所示。

img/463887_1_En_9_Fig21_HTML.png

图 9-21

设置键盘映射

  • 按键映射下拉菜单让您选择所需的按键映射,它在预设的按键映射之间切换。

  • 动作列表。右键单击一个操作来修改它。您可以为动作添加附加的键盘快捷键,添加鼠标快捷键以将动作与鼠标单击相关联,或者移除当前快捷键。如果您正在使用预置的键映射,修改动作的快捷键将自动创建键映射的副本,并将您的修改添加到副本中。

  • 您可以使用搜索框来搜索使用动作名称的快捷键。

  • 通过快捷方式搜索。您可以在此搜索窗口中键入键盘快捷键来查找操作名称。

自定义代码样式

在同一个设置(MAC OS 中的首选项)窗口中,您还可以自定义编码风格和许多其他设置,如编辑器字体和配色方案等。

若要自定编码样式,请打开“偏好设置”窗口(如果它尚未打开)。在主菜单上点击文件设置(在 macOS 上, Android Studio首选项)。代码样式窗口位于首选项窗口右侧的编辑器菜单下,如图 9-22 所示。

img/463887_1_En_9_Fig22_HTML.jpg

图 9-22

代码风格

现在,您可以随意调整编辑器。这些设置是不言自明的,只需根据你的喜好进行调整即可——或者,如果你在一个团队中工作,根据发布的编码风格指南来调整设置。

章节总结

  • Android 应用由松散组装的组件组成,这些组件由 AndroidManifest.xml 组合在一起。

  • 您可以在 Android 清单文件中设置应用权限。

  • 一个应用可能包含活动、服务、广播接收者和内容提供者等组件的组合。

  • 组件使用意图相互通信。

在下一章,我们将开始研究如何用活动和布局来构建用户界面。我们将了解 Android 如何使用 XML 作为布局资源,以及这些 XML 资源如何在运行时使用名为 inflation 的过程转换并呈现为对象,等等。

十、活动和布局

我们将介绍的内容:

  • 活动和布局

  • 查看和查看组对象

  • 活动生命周期

  • Kotlin Android 扩展

大多数程序需要一个入口点或一个开始例程,所有的执行都从那里开始。即使是前面例子中简单的“Hello World”也需要一个主函数作为入口点。安卓程序也一样,它也需要自己版本的“功能主”但是 Android 程序的入口点不仅仅是一个名为“main”的函数——它比这个函数要复杂一些。在这一章中,我们将探索一个基本应用的结构。我们将看看如何建立一个用户界面,并发现是什么让他们滴答作响。

应用入口点

一个简单的向用户显示屏幕的应用至少需要三样东西。它需要(1)一个充当主程序文件的活动类;(2)包含所有 UI 定义的布局文件;以及(3)清单文件,它将项目的所有内容联系在一起。如果你还记得使用 JavaBean 的 manifest 文件,Android manifest 有点像。它描述了项目的内容。

当应用启动时,Android 运行时会创建一个 Intent 对象并检查清单文件。它在寻找intent-filter节点的特定值;运行时试图查看应用是否有定义的入口点,类似于“主函数”清单 10-1 显示了清单文件的摘录。

<activity android:name=".MainActivity">
 <intent-filter>
   <action android:name="android.intent.action.MAIN" />
   <category android:name="android.intent.category.LAUNCHER" />
 </intent-filter>
</activity>

Listing 10-1Excerpt from AndroidManifest.xml

清单 10-1 显示了一个活动的声明。如果应用有多个活动,你会看到几个定义,如清单 10-1——每个活动一个定义。定义的第一行有一个名为android:name的属性。该属性指向活动的类名。在这个例子中,类的名称是“MainActivity”

第二行声明了意图过滤器;当您在 intent-filter 节点上看到类似于android.intent.action.MAIN的内容时,这意味着该活动是应用的入口点。当应用启动时,这是将与用户交互的活动。

活动类别

主活动类负责与用户的初始交互。这是一个 Kotlin 类,在这个类中,我们可以并且经常做以下事情:

  • 选择要使用的 UI 文件。当我们从活动内部调用setLayout(xml:file)函数时,它会将活动绑定到 xml:file。这被称为“布局绑定”当活动绑定到布局时,屏幕上将充满用户可以触摸或滑动的用户界面元素。

  • 获取视图对象的引用。视图对象也称为小部件或控件。当我们有一个对视图对象的编程引用时,我们可以操作它们,改变它们的属性,或者将它们与一个事件相关联。这被称为视图绑定。

Activity 类以某种方式继承了 android.app.Activity。在我们的例子中,它们继承自 AppCompatActivity 这是 FragmentActivity 的子元素,而后者又是 android.app.Activity 的子元素。我们使用 appcompactivity 类,这样我们就可以将工具栏等现代 UI 元素放在我们的项目中,并且仍然可以在旧版本的 android 上运行它们,否则工具栏将不受支持——因此,appcompactivity 的名称中有“Compat”。

当运行时启动一个最终会启动一个活动的应用时,它会创建并跟踪该活动发生了什么。每个活动都有一个非常完整的生命周期,每个生命周期事件都有一个关联的函数,我们可以用它来定制应用的行为。

图 10-1 显示了活动生命周期的各个阶段。每个方框显示了特定存在阶段的活动状态。函数调用的名称嵌入在连接阶段的方向箭头中。

img/463887_1_En_10_Fig1_HTML.jpg

图 10-1

活动生命周期

当运行时启动应用时,它调用主活动的onCreate()函数,这将使活动的状态变为“已创建”。您可以使用此函数执行初始化例程,如准备事件处理代码等。

活动将进行到下一个状态,即“已开始”;此时,用户可以看到活动,但是还不能进行交互。下一个状态是“恢复”;这是应用与用户交互的状态。

如果用户单击任何可能启动另一个活动的东西,运行时将暂停当前活动,并进入“暂停”状态。从那里,如果用户返回到活动,调用onResume()函数,活动再次运行。另一方面,如果用户决定打开一个不同的应用,运行时可能会“停止”并最终“破坏”该应用。

布局文件

布局文件包含以 XML 层次结构排列的视图对象。像按钮或文本字段这样的用户界面元素是在 XML 文件中编写的。一些人可能会害怕只使用 XML 编辑器手工编写 UI。但是你不必担心,因为 AS3 使得用户界面的构建变得很容易。我们可以在文本模式(手工编辑 XML)下使用布局文件,也可以在设计模式(WYSIWYG)下使用它。

图 10-2 显示了以两种可能模式显示的布局文件:文本模式和设计模式。您可以通过点击主编辑器窗口左下方的“文本”或“设计”选项卡来切换模式。当您通过编辑 XML 来更改元素时,AS3 会自动更新设计视图的呈现。同样,当您在设计视图中进行更改时,XML 文件也会更新。

img/463887_1_En_10_Fig2_HTML.jpg

图 10-2

以文本和设计模式显示的布局文件

清单 10-2 显示了一个典型的布局文件。如果您选择创建一个“空”活动,那么这就是项目创建向导将产生的结果。

<?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">

  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World!"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

Listing 10-2activity_main.xml

一个简单的布局文件通常有两个部分:一个容器的声明和其中每个 UI 元素的声明。在清单 10-2 中,第二行(也是 XML 文档的根)是容器的声明。TextView 元素被声明为容器的子节点。这就是容器和 UI 元素在布局文件中的排列方式。

查看和查看组对象

视图对象是一个组合单元。通过将一个或多个视图对象并排排列,或者有时相互嵌入,可以构建一个 UI。Android 库定义了两种视图,一个“视图”和一个“视图组”。按钮或文本字段就是视图对象的一个例子。这些对象旨在与其他视图一起组成,但它们并不包含子视图,而是独立存在的。另一方面,视图组可以包含子视图——这就是它们有时被称为容器的原因。

图 10-3 显示了一些更常见的 UI 元素的类层次结构。用户界面中的每一项都是 android.view.View 类的子类。我们可以使用 Android SDK 中预先构建的用户界面元素,如 TextView、Button、ProgressBars 等。或者,如果需要,我们可以构造自定义控件(窗口小部件或视图有时被称为“控件”),方法是:( 1)对现有元素(如 TextViews)进行子类化;(2)子类化视图类本身,完全从零开始绘制一个自定义小部件;或者(3)细分视图组并在其中嵌入其他小部件——这就是所谓的复合视图(图 10-3 中的单选按钮组就是一个例子)。

img/463887_1_En_10_Fig3_HTML.jpg

图 10-3

视图组类层次结构

每个视图对象最终都会在运行时变成一个 Java 对象,但是我们在设计时将它们作为 XML 元素来处理。我们不必担心 Android 如何将 XML 膨胀成 Java 对象,因为这个过程对我们来说是不可见的——它发生在幕后。图 10-4 显示了 Android 编译过程的逻辑表示。

img/463887_1_En_10_Fig4_HTML.jpg

图 10-4

Android 编译过程

Kotlin 编译器将程序源文件转换成 Java 字节码。产生的字节码与 Kotlin 标准库结合形成一个 DEX 文件。DEX 文件是 Dalvik 可执行文件——它是 Android 运行时(ART)理解的可执行文件格式。在 dex 文件和其他资源被打包到 Android 包(APK)之前,它还产生了一个名为“R.class”的特殊文件。我们使用 R.class 来获取对我们在布局文件中定义的 UI 元素的程序引用。

容器

除了创建复合视图,ViewGroup 类还有另一个用途。它们是布局管理器的基础。布局管理器是一个容器,负责控制子视图在屏幕上相对于容器和彼此的位置。Android 自带了几个预置的布局管理器。表 10-1 向我们展示了其中的一些。

表 10-1

布局经理

|

布局管理器

|

描述

|
| --- | --- |
| 线形布局 | 根据选定的方向,将小工具定位在单行或单列中。可以为每个微件分配一个权重值,该权重值决定了该微件相对于其他微件所占用的空间量。 |
| 表格布局 | 以行和列的网格格式排列小部件 |
| 框架布局 | 将子视图堆叠在一起。XML 布局文件的最后一个条目是堆栈顶部的条目。 |
| 相对布局 | 通过在每个视图上指定对齐方式和边距,可以相对于其他视图和容器定位视图。 |
| 约束布局 | ConstraintLayout 是最新布局。它还相对于彼此和容器定位小部件(如 RelativeLayout)。但是它不仅仅使用对齐和边距来完成布局管理。它引入了“约束”对象的概念,将小部件锚定到目标上。这个目标可以是另一个小部件或容器;或者另一个定位点。这是我们将在本书中的大多数例子中使用的布局。 |

现在我们已经有了一些关于活动和布局的工作知识,让我们在下一节中在代码级别探索它们。

你好世界

让我们创建一个包含空活动的新应用。如果你想继续进行代码示例,项目信息如表 10-2 所示。

表 10-2

Hello 应用的项目信息

|

项目明细

|

|
| --- | --- |
| 应用名称 | 你好 |
| 公司域 | 使用您的网站名称 |
| Kotlin 支架 | 是 |
| 波形因数 | 仅限手机和平板电脑 |
| 最低 SDK | API 23 棉花糖 |
| 活动类型 | 空的 |
| 活动名称 | 主要活动 |
| 布局名称 | 活动 _ 主要 |

创建项目时,您会在项目窗口中看到一堆文件,但我们只对三个感兴趣。图 10-5 显示了(1)主程序文件的位置;(二)载货清单;以及(3)项目文件窗口中的主布局文件。

img/463887_1_En_10_Fig5_HTML.jpg

图 10-5

CH10Hello 项目

名为 activity_main.xml 的主布局文件位于 app ➤ res ➤布局文件夹中。所有用户界面元素都写在布局文件中。

主程序文件 MainActivity.kt 位于 app ➤ java ➤包名文件夹中。这是 Kotlin 文件,包含扩展 Android 活动的类。如果你想对用户生成的事件做出反应,这里就是我们编写程序逻辑的地方。不要被“java”文件夹所迷惑,所有的源文件,不管是 java 还是 Kotlin,都存储在“Java”文件夹中。没有“Kotlin”文件夹。

清单文件向 Android 构建工具描述了关于应用的基本信息:Android OS 和 Google play。看图 10-5 ,好像清单文件在 app ➤清单➤ AndroidManifest.xml 里。你需要记住,我们看到的是项目窗口的“Android 视图”。它是项目文件的逻辑表示,而不是文件相对于项目根文件夹的文字排列。如果想查看项目文件的实际位置,切换到“项目视图”,如图 10-6 所示。

img/463887_1_En_10_Fig6_HTML.png

图 10-6

CH10Hello,在项目视图中

项目视图显示了所有项目文件的实际位置。它看起来比“Android 视图”要忙得多,但是如果你需要定位项目下的任何文件,这个视图会很有用。现在我们可以回到“Android 视图”,这是我们将在本书的大部分内容中使用的。

让我们仔细看看生成的布局和 MainActivity 文件。代码分别显示在清单 10-3 和 10-4 中。

| -什么 | 布局文件的根节点,它也声明哪种布局管理器是有效的。在这种情况下,我们使用约束布局管理器 | | ➋ | TextView 对象的声明。它是布局管理器的子节点。 | | ➌ | 定义 TextView 对象的约束之一。它说,在文本视图的底部有一个锚点,它锚定在容器的底部。 |
<?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">

  <TextView                          ➋
    android:id="@+id/hello"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World!"
    app:layout_constraintBottom_toBottomOf="parent" ➌
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

Listing 10-3activity_main.xml

package com.example.ted.ch10hello

import android.support.v7.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) { ➊
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main) ➋
  }

Listing 10-4MainActivity.Kt

}

| -什么 | 第一个活动生命周期方法。运行时可能会也可能不会将一个`Bundle`对象传递给函数。bundle 对象通常包含以前活动状态的数据(例如,当您从用户处收集数据时,您可能希望在活动达到“暂停”状态时将它们保存在 Bundle 中,这样,如果用户被打断(通常是被另一个活动打断),您就不必要求用户再次输入数据,因为数据已经在 Bundle 中了)。 | | ➋ | `setContent()`函数将这个活动绑定到一个特定的布局文件。“R”类是在 Android 构建过程中由 *aapt* 工具生成的;它包含了对我们在 *app ➤ res* 文件夹中声明的所有内容的编程引用。在这个声明中,我们将主活动。带`R.layout.activity_main.`的 Kt |

既然我们知道了项目向导给了我们什么,让我们对应用进行更改。

修改 Hello World

我们将对布局文件和活动进行一些小的修改。我们将执行以下操作:

  1. 更改当前 TextView 控件中的文本。

  2. 在屏幕上添加一个按钮,我们将把按钮放在文本视图的正下方。

  3. 向活动添加一个功能。该函数将递增 TextView 的当前值。

  4. 我们将把我们的新函数与按钮关联起来,这样我们每次单击按钮,TextView 的值都会增加 1。

图 10-7 显示了我们项目在 AS3 内部的总体布局。目前,我们正在设计模式下查看 activity_main.xml。在这种模式下,我们可以看到视图面板、设计图面和蓝图面。

img/463887_1_En_10_Fig7_HTML.png

图 10-7

设计视图中显示的 CH10Hello

要添加一个按钮控件,请将按钮从视图面板拖放到设计图面,如图 10-8 所示——您也可以将其拖放到蓝图图面中,这样也可以。

img/463887_1_En_10_Fig8_HTML.png

图 10-8

从视图选项板拖放控件

按钮控件还没有任何约束,因为我们没有把任何。向设计图面添加控件时,不会自动添加约束。TextView 有约束,因为它是在我们创建项目时由向导生成的。图 10-9 显示了我们当前项目的运行时和设计时再现。

img/463887_1_En_10_Fig9_HTML.png

图 10-9

无约束按钮

Hello TextView 很好地位于屏幕中央,因为它有四个锚点(约束)。该按钮在设计时出现在 Hello 文本的正下方,但在运行时,它位于屏幕的位置 0,0(左上角),这是控件在运行时没有约束时的定位方式。

让我们重新开始。移除设计图面中所有现有的约束。您可以通过选择所有控件并单击“清除约束”按钮来完成此操作,如图 10-10 所示。

img/463887_1_En_10_Fig10_HTML.png

图 10-10

清除约束

移除所有约束后,按照您希望控件在运行时出现的方式在设计图面上重新定位控件。接下来,再次选择所有控件,方法是在控件周围单击并拖动鼠标。

要“神奇地”为我们的控件添加所有约束,单击“推断约束”,如图 10-11 所示。AS3 将尽力猜测控件所需的约束,这些约束将与您在设计图面中的排列相匹配。

img/463887_1_En_10_Fig11_HTML.png

图 10-11

推断约束

可以在“属性”窗口中设置控件的属性。我们需要改变文本视图和按钮控件的一些属性。当在设计界面中选择一个对象时,该对象的属性会出现在属性窗口中,如图 10-12 所示。

img/463887_1_En_10_Fig12_HTML.png

图 10-12

属性窗口

“属性”窗口包含选定视图对象的所有属性,但默认情况下不会显示所有属性。它只显示我们常用的属性。要查看所有属性,点击“查看所有属性”按钮,如图 10-12 所示。

将 TextView 的“ID”属性改为“textHello”,如图 10-12 所示。接下来,将“textApperance”更改为“Material”。大”——您必须在属性窗口中向下滚动一点,这样才能看到“textApperance”属性。

视图对象的 ID 属性很重要,因为它使视图对象可以从我们的代码(Activity 类)中访问。

我们需要更改的下一个属性是按钮的 onClick 属性。选择按钮,然后找到“onClick”属性。您可能需要显示按钮的所有属性并向下滚动,直到看到 onClick 属性。

img/463887_1_En_10_Fig13_HTML.jpg

图 10-13

按钮的 onClick 属性

在按钮的 onClick 属性中键入“addNumber”,如图 10-13 所示。这个动作会将按钮的 click 事件关联到 MainActivity 类中的addNumber()函数。当然,我们还没有写函数,但是没关系,因为我们很快就会实现它。

我们已经完成了布局文件中的工作。现在我们可以在 MainActivity 类上工作了。打开主活动。Kt 并进行如下修改,如清单 10-5 所示。

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    findViewById<TextView>(R.id.textHello).text = "1"

  }
}

Listing 10-5MainActivity.Kt

这里没有惊喜。onCreate()函数中的最后一条语句获取对 textHello 对象的引用,并将 text 属性设置为“1”这已经是很大的进步了。请记住,在 Java 中,这条语句看起来像清单 10-6 。

TextView helloText = (TextView) findViewById(R.id.textHello);
helloText.setText("1")

Listing 10-6How to Set a Property During Runtime, in Java

在 Kotlin 中,我们得到了很好的 getter 和 setter 语法糖。但是我们仍然可以削减一些锅炉板代码。AS3 自动带有 Kotlin Android Extensions 插件,每当创建一个新项目时,它已经在模块级的“build.gradle”文件中声明。图 10-14 显示了 build.gradle 文件及其内容。

img/463887_1_En_10_Fig14_HTML.png

图 10-14

build.gradle,模块级

Gradle 已经取代 Apache Ant 成为构建工具。你通常不需要改变 gradle 文件中的任何内容,因为默认内容在大多数情况下都很好。

回到代码,清单 10-7 显示了 MainActivity 的完整程序。Kt,它实现了每当单击按钮时递增 textHello 值的逻辑。

| -什么 | 该语句导入了 Kotlin Android 扩展。您可能不需要自己输入——AS3 会在尝试使用视图对象的 ID 进行视图绑定时自动添加它。 | | ➋ | 我们不用再用`findViewById()`了;我们甚至不必使用 R.class 来限定视图对象的 ID。Android Kotlin 扩展将视图暴露给我们的代码,少了很多仪式。这将产生更加清晰的代码。还要注意,我们得到了 Kotlin 添加的良好的 getter 和 setter 语法。 | | ➌ | `addNumber()`功能与按钮控件的 *onClick* 事件相关联。该函数是一个*事件处理程序*——当点击按钮时,该函数将被调用。它需要接受一个`View`对象作为参数,因为这是事件处理程序的要求。该函数需要能够访问引发事件的视图对象。 | | -你好 | `textHello.text`以 CharSequence 类型返回 textHello 的当前值。`toString()`将其转换为字符串类型,我们可以使用`toInt()`函数将其转换为 Int 类型。我们需要 Int 形式的值,因为我们将在数学运算中使用它。 | | ➎ | 该语句将 textHello 的 text 属性设置为一个新值。 |
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.TextView
import kotlinx.android.synthetic.main.activity_main.* ➊

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    textHello.text = "1" ➋

  }

  fun addNumber(v: View) {  ➌
    val currVal = textHello.text.toString().toInt() ➍
    val nextVal = currVal + 1
    textHello.text = nextVal.toString() ➎
  }
}

Listing 10-7MainActivity.Kt

完成编辑后,在 AVD 上运行应用。图 10-15 显示了在仿真器上运行的项目。

img/463887_1_En_10_Fig15_HTML.jpg

图 10-15

运行在模拟器上的 CH10Hello

章节总结

  • Android 应用的入口点需要三个文件:清单文件、布局文件和活动类

  • AndroidManifest 文件声明了 Android 项目的所有内容。清单可以指定一个活动类作为应用的入口点。

  • 布局文件描述了屏幕的 UI 结构。每个元素都被描述为一个 XML 节点,但是 XML 文件在运行时是膨胀的。膨胀过程产生 UI 元素的 Java 对象表示。

  • 所有 UI 元素都继承自 android.view.View 类。

  • 复合视图可以通过从 ViewGroup 类继承来构造。

  • 布局管理器提供了在屏幕上排列 UI 元素的方法。Android SDK 有大量预置的管理器,我们可以开箱即用。

  • Kotlin Android 扩展允许我们通过公开视图元素的属性和功能来简化视图绑定代码。我们不再需要使用 findViewById。

在下一章中,我们将学习如何:

  • 使用一些基本的视图元素,如按钮和吐司

  • 使用 Kotlin 的 Android 扩展获取视图对象的引用;它取代了黄油刀

  • 处理点击和长点击;我们将使用对象表达式的完整语法来完成长格式,并使用 lambda 表达式来完成捷径

十一、事件处理

我们将介绍的内容:

  • 监听器对象

  • 匿名内部对象

  • 在事件处理程序中使用 lambdas

在上一章中,我们已经做了一些事件处理。我们编写了一个函数,它会在每次单击按钮时增加文本视图的值,这部分练习是关于声明性事件处理的。要将函数名绑定到点击事件,我们只需将视图的 android:onClick 属性设置为函数名。这是一种简单的处理事件的方式,但是仅限于“click”事件。当你需要处理像长点击或手势这样的事件时,你需要使用事件监听器——这是本章的主题。

事件处理简介

用户通过触摸、点击、滑动或输入东西来与你的应用进行交互。Android 框架捕获、存储、处理这些动作,并将其作为事件对象发送给你的应用。我们可以通过编写专门用于处理这些事件的函数来响应这些事件。处理事件的函数写在监听器对象里面——而且有相当多的这样的函数。图 11-1 显示了 Android 框架和你的应用如何处理用户动作的简化模型。

img/463887_1_En_11_Fig1_HTML.png

图 11-1

简化的事件处理模型

当用户用你的应用做一些事情时,比如点击一个按钮,Android 框架捕捉这个动作,并把它变成一个事件对象。事件对象包含关于用户动作的数据(例如,点击了哪个按钮,按钮被点击时的位置,等等)。)Android 将此事件对象发送到您的应用,并调用与用户动作相对应的特定函数。如果用户点击了按钮,Android 将调用按钮对象上的onClick()函数,如果用户点击了同一个按钮,但按住时间稍长,则调用onLongClick()函数。像按钮一样,视图对象可以响应一系列事件,如点击、按键、触摸或滑动等。表 11-1 列出了一些常见事件及其对应的事件处理程序。

表 11-1

通用监听器对象

|

连接

|

功能

|

描述

|
| --- | --- | --- |
| View.OnClickListener | onClick() | 当用户触摸并按住控件时(在触摸模式下),或者用导航键聚焦在项目上然后按 ENTER 键时,调用此函数 |
| View.OnLongClickListener | onLongClick() | 几乎和点击一样,但时间更长 |
| View.OnFocusChangeListener | onFocusChange() | 当用户导航到控件上或离开控件时 |
| View.OnTouchListener | onTouch() | 几乎和点击动作一样,但是这个处理程序让你发现用户是向上还是向下滑动。你可以用这个来回应手势 |
| View.OnCreateContextMenuListener | onCreateContextMenu() | 当一个上下文菜单被构建时,Android 调用这个,作为一个持续长时间点击的结果 |

为了设置一个监听器,View 对象可以设置或者更恰当地说,注册一个监听器对象。注册一个监听器意味着当用户与视图对象交互时,你告诉 Android 框架调用哪个函数。图 11-2 显示了注册处理程序的注释代码。

img/463887_1_En_11_Fig2_HTML.png

图 11-2

带注释的事件注册和处理代码

setOnClickListener 是 android.view.view 类的成员函数,这意味着 View 的每个子类都有它。这个函数需要一个 OnClickListener 对象作为参数——这个对象成为按钮控件的监听器。点击按钮时,运行 onClick 函数内的代码。

我们通过创建一个从视图继承的对象表达式来创建监听器对象。OnClickListener 。该类型在视图类中被声明为嵌套接口。对象表达式是 Java 的匿名内部类的 Kotlin 等价物。在 Java 中,我们编写了类似清单 11-1 中 thseat 的代码。

button.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View view) {
    System.out.println("Hello click");
  }
}});

Listing 11-1onClick Listener in Java

在 Kotlin 中,使用对象表达式创建一个匿名内部类,如清单 11-2 所示。

button.setOnClickListener(object: View.OnClickListener {
  override fun onClick(v: View?) {
    println("Hello click")
  }
})

Listing 11-2onClick Listener in Kotlin

清单 11-2 实际上是一种编写对象表达式的冗长方式。Kotlin 对 lambdas 的支持可以简化我们现有的代码,如清单 11-3 所示。

button.setOnClickListener {
  println("Hello")
}

Listing 11-3onClick Listener Using lambdas

现在我们已经有了足够的关于事件的工作知识,让我们通过创建一个新项目来进一步探索它们。表 11-2 显示了项目详情。

表 11-2

CH11EventAnonymousclass 类的项目信息

|

项目详细信息

|

价值

|
| --- | --- |
| 应用名称 | ch11 事件匿名类 |
| 公司域 | 使用您的网站名称 |
| Kotlin 支架 | 是 |
| 波形因数 | 仅限手机和平板电脑 |
| 最低 SDK | API 23 棉花糖 |
| 活动类型 | 空的 |
| 活动名称 | 主要活动 |
| 布局名称 | 活动 _ 主要 |
| 向后兼容性 | 是的。应用兼容性 |

这个项目将只包含两个控件:一个是当我们使用向导时项目附带的 TextView,另一个是我们还没有添加的 Button view。这个按钮将使用一个匿名的内部对象拦截点击和长时间点击事件。

在主编辑器中打开 activity_main.xml 文件,如果它还没有打开的话。你可以在 app > res > layout 文件夹下的项目浏览器窗口中找到。

向设计图面添加一个按钮,并向其添加一些约束。您可以通过将按钮控件从组件面板中拖动到设计图面上来将它添加到布局中,如图 11-3 所示。

img/463887_1_En_11_Fig3_HTML.png

图 11-3

向设计图面添加一个 button 控件

当按钮控件被选中时,点击约束工具栏上的“推断约束”(如图 11-3 )。

您可能会注意到在布局编辑器的右上角有一个黄色的警告三角形(如图 11-4 所示)。单击警告框。

img/463887_1_En_11_Fig4_HTML.jpg

图 11-4

显示警告和错误按钮

图 11-5 显示消息工具窗口。它包含了一些关于为什么我们会得到警告的解释,以及一个建议修复的按钮提示。

img/463887_1_En_11_Fig5_HTML.jpg

图 11-5

建议的修复

AS3 抱怨是因为新添加的按钮在其文本属性中有一个硬编码的值。清单 11-4 显示了“修复”之前的 activity_main.xml(的一个片段)现在, android:text 属性有一个值“Button”,一个字符串文字。

<Button
  android:id="@+id/button"
  android:text="Button"
/>

Listing 11-4activity_main.xml, Button Element, Before the Fix

Androids 更喜欢我们在资源文件中编写属性值,比如按钮的文本属性,而不是硬编码。单击“修复”按钮,这样 AS3 可以自动提取字符串资源。该操作打开提取资源窗口(参见图 11-6 )。

img/463887_1_En_11_Fig6_HTML.png

图 11-6

提取资源

我们的项目在app/RES/values/strings . XML中有一个字符串资源文件。它为应用提供文本资源值。Android 希望我们在这个资源文件中存储所有的字符串文字,而不是像你在清单 11-4 中看到的那样硬编码它们。

“资源名称”成为新创建的字符串资源的“名称”属性,“资源值”成为字符串资源的值。该值将显示在按钮的文本中。单击“确定”完成操作。

清单 11-5 显示了修复后 activity_main.xml 的内容。android:text 的值现在设置为“@string/button。”@符号意味着我们不应该直接使用这个字符串的值,而应该在字符串资源文件中查找一个名为“button”的资源。

<Button
  android:id="@+id/button"
  android:text="@string/button"
/>

Listing 11-5activity_main.xml, Button Element, After the Fix

我们需要在布局文件上做的最后一件事是给布局容器分配一个 id 属性。默认情况下,布局容器没有 id 属性。我们需要给它分配一个 id,因为我们将在后面的代码中引用它。切换到设计模式,点击布局容器内的某处(如图 11-7 )。在属性面板中,编辑 id 属性。在这个例子中,布局容器的 id 是“root_layout”

img/463887_1_En_11_Fig7_HTML.png

图 11-7

更改布局容器的 id 属性

清单 11-6 显示了我们布局文件的修改内容。

| -什么 | 布局容器的 android:id 现在被设置为`+@id/root_layout`。在后面的代码中,我们可以将这个控件称为`root_layout .` | | ➋ | android:text 属性现在有一个值`@string/button;`,它不再是硬编码的。它现在从 strings.xml 资源文件中获取其值。 |
<?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:id="@+id/root_layout"    ➊
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity">

  <TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World!"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintVertical_bias="0.353" />

  <Button

    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="8dp"
    android:layout_marginStart="8dp"
    android:layout_marginTop="36dp"
    android:text="@string/button"   ➋
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/textView" />

</android.support.constraint.ConstraintLayout>

Listing 11-6Complete Listing for activity_main.xml

现在我们可以处理程序文件了。打开主活动。Kt 在主编辑器中。您可以通过双击文件 app/Java/com . example…/main activity 来启动它。项目窗口中的 Kt。

我们希望按钮对点击和长时间点击都有反应。为此,我们需要为同一个按钮设置两个独立的侦听器——我们可以创建两个按钮,并为每个按钮分配一个侦听器,但是我觉得如果我们将两个侦听器绑定到同一个按钮,这个练习会更有指导意义。

在我们设置侦听器之前,活动不需要对用户可见;它只需要处于“已创建”状态。这就是为什么我们要在onCreate()回调函数中设置监听器。让我们先处理点击事件,然后我们将处理长时间点击。清单 11-7 显示了 OnClickListener 的代码。

button.setOnClickListener(object : View.OnClickListener {
    override fun onClick(v: View?) {
    }
})

Listing 11-7
OnClickListener

顺便说一下,当您键入这些代码时,您可能会看到一些错误或警告,如图 11-8 所示。

img/463887_1_En_11_Fig8_HTML.png

图 11-8

AS3 提示

在图 11-8 中,AS3 警告了一个未解析的“按钮”引用。要修复这个错误,我们可以手动键入所需的导入语句,或者使用 AS3 的“快速修复”功能。要使用快速修复,单击未解析引用中的任意位置——在我们的例子中是“按钮”标识符——然后按下键 OPTION + ENTER (如果你在 macOS 上);如果你在 Windows 或 Linux 上,按 ALT + ENTER

如果有多种解决问题的方法,AS3 将提供一些选项。您可以滚动选项并选择您想要使用的选项。

图 11-9 显示了如何修复未解决的参考误差的选项。我们将选择最后一个选项—这个导入语句是 Kotlin Android Extensions (KAE)。KAE 的神奇之处在于,它将布局中所有视图元素的 id 作为 Activity 类的扩展属性公开。因此,如果您在 activity_main.xml 中有一个 ID 为“Button”的按钮视图,您可以像使用常规变量一样在 activity 类中简单地使用该 ID——您不再需要使用 findViewById()

img/463887_1_En_11_Fig9_HTML.jpg

图 11-9

AS3 暗示导入

一旦你输入了清单 11-7 和图 11-10 中所示的事件处理程序,你会注意到 AS3 在提示我们将 listener 对象转换成 lambda 表达式。

img/463887_1_En_11_Fig10_HTML.jpg

图 11-10

转换为 lambda 提示

要使用快速修复,单击“OnClickListener”中的任意位置,如图 11-11 所示,按 OPTION + ENTER 或 ALT + ENTER,然后选择“转换为 lambda”

img/463887_1_En_11_Fig11_HTML.jpg

图 11-11

转换为 lambda 快速修复

lambda 简化版删除了我们的一些代码 setOnClickListener 的括号、对象表达式和被覆盖的 onClick 函数都被删除了,留给我们的只有下面的代码:

button.setOnClickListener {  }

接下来要做的事情是在 onClick 处理程序中放置一个 Toast 消息。清单 11-8 显示了点击处理程序中的一个简单的 Toast 消息。祝酒词是一个小的弹出消息,一段时间后会自动消失。您可以使用它向用户发送小的反馈消息。清单 11-8 展示了如何在 OnClickListener 中构造一个 Toast 消息。

button.setOnClickListener {
  Toast.makeText(this, "Hello World", Toast.LENGTH_LONG).show()
}

Listing 11-8
Toast Message

显示祝酒词的过程分为两步。第一步是使用 makeText() 函数创建一条 Toast 消息。它有三个参数:(1)应用的上下文,在我们的例子中是 MainActivity 的实例;(2)消息显示;以及(3)显示消息多长时间。第二步是通过调用。显示()功能。

让我们转到长点击监听器。这个监听器的代码如清单 11-9 所示。

button.setOnLongClickListener(object: View.OnLongClickListener {
  override fun onLongClick(v: View?): Boolean {

    return true
  }
})

Listing 11-9
OnLongClickListener

将清单 11-9 中的代码简化为它的 lambda 版本,得到如下代码:

button.setOnLongClickListener { true }

为了测试长点击处理程序,让我们使用 SnackBar 而不是 Toast。SnackBar 类似于 Toast,但它出现在屏幕的底部。你也可以让它在超时后消失,比如祝酒,或者你可以让用户滑动它。SnackBar 比 Toast 功能更强大,因为您可以在消息中包含一些操作,比如一个小对话框。

在项目中使用 SnackBar 之前,需要修改项目的 build.gradle 文件。请参见清单 11-10 了解您需要做出的更改。

| -什么 | 在使用 SnackBar 之前,您需要将它添加到项目的 **build.gradle** 文件(应用级别)中。 |
dependencies {
    implementation 'com.android.support:design:27.1.1' ➊
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

Listing 11-10/app/build.gradle

之后,您需要“同步”gradle 文件。主编辑器的上部会出现一个黄色条,右上角会有一个“同步”文件的链接。点击它,如图 11-12 所示。

img/463887_1_En_11_Fig12_HTML.jpg

图 11-12

同步 build.gradle 文件

之后,您现在可以使用 SnackBar 元素了。清单 11-11 展示了如何在长点击处理程序中构建一个 SnackBar。

button.setOnLongClickListener {
  Snackbar.make(root_layout, "Long click", Snackbar.LENGTH_LONG).show()
  true
}

Listing 11-11SnackBar Message Inside OnLongClickListener

SnackBar 的 make 函数需要三个参数:(1)一个父视图;root_layout 是我们的布局容器的 ID;(2)要显示的消息;以及(3)显示消息多长时间。

OnLongClickListener 中的最后一行实际上是一个 return 语句,但是我们省略了“return ”,因为处理程序是 lambda 形式的——在这种形式中,返回块上的最后一个表达式。

onLongClick()回调函数有一个布尔签名—它返回 true 或 false。在我们的例子中,我们返回了 true,这告诉 Android 运行时事件已经被消费了,不需要其他事件处理程序(如 onClick)再次处理它。如果我们返回 false,onClick 处理程序会在 onLongClick 之后立即启动。清单 11-12 显示了 MainActivity 的完整代码。

| -什么 | 我们项目的包声明。这来自项目创建期间的“公司域”条目。 | | ➋ | Kotlin Android 扩展(KAE)的导入声明。KAE 将 activity_main.xml 中的所有视图元素转换为扩展属性。因此,我们可以只使用 ID 来引用任何视图元素。 | | ➌ | 我们从 AppCompatActivity 扩展而来,因此我们可以使用 SnackBar 等现代元素,同时仍然可以在早期版本的 Android 上运行该应用。 | | -你好 | 该语句将 MainActivity 绑定到我们的布局文件 activity_main.xml。 |
package com.example.ted.ch11_event_anonymous_class  ➊

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.test.ViewAsserts
import android.view.View
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.* ➋

class MainActivity : AppCompatActivity() {  ➌

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)  ➍

    button.setOnClickListener {
      Toast.makeText(this, "Hello World", Toast.LENGTH_LONG).show()
    }

    button.setOnLongClickListener {
      Snackbar.make(root_layout, "Long click", Snackbar.LENGTH_LONG).show()
      true
    }
  }
}

Listing 11-12MainActivity.Kt, Annotated

如果你在模拟器上运行应用,你会看到类似图 11-13 的东西。

img/463887_1_En_11_Fig13_HTML.png

图 11-13

在模拟器中运行的已完成项目

章节总结

  • 如果想处理简单的点击事件,可以将 android:onClick 属性设置为一个函数名。

  • 如果您想要拦截某些事件,监听器对象必须注册到 Android 运行时。

  • 侦听器对象有很多种,它们在 View 类中被列为嵌套接口。

  • 使用 Kotlin Android 扩展简化了我们的编码。它将布局文件中所有视图的 Id 作为 MainActivity 的扩展属性公开——我们不再需要使用 findViewById() 了。

  • Lambdas 清理我们的事件处理代码。

在下一章,我们将看看 Android 最重要的部分之一:意图。Android 作为一个架构,没有它就无法存在。它是将 Android 中所有松散耦合的组件结合在一起的粘合剂。

十二、意图

我们将介绍的内容:

  • 意向概述

  • 显性和隐性意图

  • 在活动之间传递数据

  • 从意图返回结果

Android 的架构在构建应用的方式上非常独特。它有组件的概念,而不仅仅是简单的对象。而安卓让这些组件交互的方式,是只有安卓平台才有的。Android 使用意图作为其组件通信的方式——它使用意图在组件之间传递消息。在这一章中,我们将看看意图:它们是什么以及我们如何使用它们。

意图是什么

意图是“对要执行的操作的抽象描述”。 1 “这是 Android 独有的概念,因为没有其他平台使用相同的东西作为组件激活的手段。在前几章中,我们看了 Android 应用的内部。你可能记得一个应用只是一堆松散地组合在一起的“组件”(见图 12-1 ),每个组件都在一个清单文件中声明。

img/463887_1_En_12_Fig1_HTML.png

图 12-1

一个 安卓应用的逻辑表示

如果您需要您的组件相互通信(例如,启动另一个活动),该怎么办?你认为我们应该如何处理?如果你有桌面编程的经验,你可以做一些类似清单 12-1 中的代码。

class MainActivity : AppCompatActivity {

  button.setOnClickListener(object: View.OnClickListener {
    override fun onClick(v: View?) {
      SecondActivity(). // Won't work
   }
  })
}

class SecondActivity : AppCompatActivity {}

Listing 12-1Wrong Way to Activate Another Activity

清单 12-1 可能看起来是启动另一个活动的简单而直接的方法,但不幸的是,它是错误的,不会起作用。活动不是一个简单的对象,而是一个组件。不能仅仅通过实例化来激活组件。要启动一个活动,您需要创建一个 Intent 对象,并使用 startActivity()函数启动它。代码如清单 12-2 所示。

| -什么 | **this @ main activity**Intent 构造函数的第一个参数是一个`Context`对象。我们传递了`this@MainActivity`,因为 Activity 类是`Context`的子类,所以我们可以使用它。或者,我们也可以使用`getApplicationContext()` `;`,应用上下文也可以被接受。 | | ➋ | **第二个活动::class.java** 。第二个参数是一个`Class`对象。它是我们想要向其传递消息的组件的类。这是一个*反射*语法。你可能已经知道,*反射*允许我们在运行时检查程序的结构。如果 SecondActivity 是一个 Kotlin 类(KClass ),它将引用 second activity 的运行时引用,但它不是。SecondActivity 是一个 Java 类(Android 库仍然是 Java 的),因此我们称它为`SecondActivity::class.java` | | ➌ | 我们通过调用`startActivity()`并向其传递 intent 对象来启动活动。 |
button.setOnClickListener {
  val intent = Intent(this@MainActivity, SecondActivity::class.java) ➊ ➋
  startActivity(intent) ➌
}

Listing 12-2How to Activate Another Activity

Android 平台对松散耦合非常狂热,组件激活在它的架构中受到了影响。应用只是由清单文件保存在一起的组件的集合,每个组件都可以通过向其发送消息来激活。基本思想是,组件之间不直接对话。如果一个组件,比如一个活动,想要与另一个组件对话,它需要向 Android 运行时发送一个请求,并让运行时解析这个请求。您可以将意图视为 Android 中的消息传递机制:它将组件粘合在一起。

松耦合

你可能会认为 Android 的架构过于复杂,因为为什么要花这么大力气去推出另一个屏幕呢?为什么我们不能只创建一个对象的实例就完事了呢——这已经是众所周知的编程习惯了。为什么我们要用组件激活来代替它呢?

嗯,Android 的程序交互性方法非常独特,因为它非常以用户为中心。它赋予用户很大的权力来选择如何操作和创建数据。移动用户一般以任务为中心,而不是以 app 为中心;他们并不真正关心哪个应用做什么,只要它能完成。

让我们举一个 Android 设备的常见使用场景。例如,用户打开“联系人”应用并选择 Ted Hagos 的联系人详细资料。比方说,这个联系人可能有一个电子邮件地址、一部手机和一个 twitter 用户名。用户可以点击 Ted 的每个联系点,每一次,Android 都会启动一个不同的应用;默认的电子邮件客户端、拨号器和下载的 Twitter 应用。用户可能不关心启动了哪个应用或者当前打开了多少个应用;他只是想传达一个信息。如果这位用户不喜欢电子邮件应用或默认的 twitter 客户端,他可以删除这些应用,并用其他东西替换它们,他应该会回到业务中。图 12-2 显示了一个使用联系人应用的简单故事板。

img/463887_1_En_12_Fig2_HTML.png

图 12-2

用户如何与“通讯录”应用交互

为了实现这种程序交互,Android 需要设计平台,重点关注松散耦合和可插拔性。像联系人应用这样的组件,不应该知道当电子邮件地址或手机号码被点击时应该使用什么应用的任何具体细节。用于特定类型数据的应用的解决方案不应硬连线到联系人应用中;否则,用户将无法在发送电子邮件或推文时选择使用哪个应用。

这就是意图的来源。当一个组件需要完成一项超出其服务能力的任务时,它可以使用 Intents 访问 Android 平台,并询问是否有任何应用可以(或想要)满足该请求。

两种意图

有两种意图:隐含的和明确的。打个比方可能有助于说明这两种意图之间的区别。比方说,我们将请人买些糖。如果我们给出一个指令,比如“你能买些糖吗”,没有进一步的细节,这就相当于一个隐含的意图,因为那个人可以在任何地方买糖。另一方面,如果我们给出类似“请你去第三街的 ABC 商店买些糖”的指示,这就相当于一个明确的意图。清单 12-2 中的代码示例是一个显式意图的例子。

隐式意图非常强大,因为它们允许您的应用利用其他应用。你的应用可以获得不是你自己编写的功能。例如,您可以创建一个打开相机、拍摄和保存照片的意图,而无需编写任何特定于相机的代码。

意图可以携带数据

意图可以做的远不止发起其他活动;你也可以用它发送和接收数据。假设我们有两个名为 MainActivitySecondActivity 的活动,当在 MainActivity 中单击一个按钮视图对象时,我们希望启动并向 SecondActivity 发送一些数据。要向 SecondActivity 发送数据,您需要:

  1. 创建一个意图——在我们的例子中,它是一个明确的意图。

  2. 使用 putExtra 方法将数据添加到意图中。

  3. 通过调用 startActivity 方法启动另一个活动;此时,Android 运行时将启动 SecondActivity。

  4. SecondActivityonCreate 方法中,我们可以通过使用 getExtra 方法从意图中提取数据。

图 12-3 显示了这一切是如何工作的简单序列图。

img/463887_1_En_12_Fig3_HTML.png

图 12-3

如何向另一个活动发送数据

注意

Android 中的大多数函数调用像 startActivityonCreate 等。是异步的——这就是为什么序列图中使用的箭头是半杆箭头。图 12-3 (以及其他序列图中)所示的调用顺序只是近似值,它们实际上可能不会按照那个顺序发生。

为了用代码表示这些步骤,它可能看起来像清单 12-3 。

button.setOnClickListener {
  val intent = Intent(this@MainActivity, SecondActivity::class.java)
  intent.putExtra("main_activity_data", editText.text.toString())
  startActivity(intent)
}

Listing 12-3Code Snippet from MainActivity

putExtra 方法的参数是一个键值对;第一个参数是键或名称,第二个参数是值。name 参数将总是字符串类型,但第二个参数(值)可能不总是字符串类型。 putExtra 方法是重载的,它可以接受第二个参数的类型范围。如果你在 Android Studio 中输入得足够慢,你可能会在输入 putExtra 方法时看到代码提示中显示的选项;见图 12-4 。

img/463887_1_En_12_Fig4_HTML.jpg

图 12-4

AS3 中的代码提示显示了重载的 putExtra()

在清单 12-3 中,我们在 putExtra 的第二个参数中放了一个字符串;我们也可以使用其他类型(例如, Int、Byte、Char、Float、Short、等基本类型。).我们也可以使用包、Parcelables、序列化

在调用了意图上的 putExtra 方法之后,下一步是调用 startActivity 。这将触发 Android 运行时的意图解析机制,并最终启动第二个活动

现在我们继续进行第二项活动。很自然,你想从 MainActivity 中提取我们发送的数据。要做到这一点,你需要做两件事。您需要:

  1. 获取对意图对象的引用;和

  2. 从意图中调用 getExtra 函数。该代码可能如下所示:

val myintent = getIntent()
val data = myintent.getStringExtra("main_activity_data")

但是由于 Kotlin 的 getters 和 setters 的魔力,getIntent()函数变成了 intent 属性。所以,我们可以这样重写:

val data = intent.getStringExtra("main_activity_data")

从另一个活动中获取结果

在上一节中,我们设法启动了第二个活动并向其发送数据。在这一节中,我们将基于前面的例子,但是这一次,我们也将一些数据发送回 MainActivity 。为此,我们需要:

  1. 创建一个明确的意图。

  2. 使用 putExtra 方法将数据添加到意图中。

  3. 通过调用 startActivityForResult 方法启动另一个活动。像 startActivity 方法一样,我们需要将一个 Intent 对象作为参数传递给这个方法。此外,我们还需要传递一个请求代码给它。一个请求代码充当某种令牌。当我们开始一个活动并期望返回一些结果时,任何其他活动都可以返回任何结果。如果我们在一个项目中有几个活动,当我们得到结果时,可能会感到困惑。我们需要一种方法来跟踪谁发送回这些结果,而请求代码将帮助我们做到这一点。一旦我们调用 startActivityForResult,第二个活动就会启动。

  4. 在 SecondActivity 的 onCreate 方法中,我们可以通过使用 getExtra 方法从意图中提取数据。

  5. 我们可以在 SecondActivity 中做一些计算。当我们准备好发回数据时,我们将执行以下操作:

    1. 获取对意图对象的引用。

    2. 使用 putExtra 方法将数据添加到意图中。

    3. 调用 SecondActivity 的 setResult 方法。在这里我们需要做两件事情:(1)设置意向呼叫的状态,如果没有错误,您可以将其设置为活动。结果 _ OK;以及(2)将包含额外内容的意图对象作为第二个参数传递。

    4. SecondActivity 内部调用 finish() 。这将停止 SecondActivity,并有效地将意图发送给名为 SecondActivity 的组件,即 MainActivity

  6. 回到 MainActivity,无论我们期望从 second Activity——或者任何其他活动——返回什么结果,都可以从 onActivityResult 回调中接收。这个方法的参数中有三样东西:它有请求代码、结果代码和由 SecondActivity 发回的 Intent 对象。

图 12-5 显示了如何从另一个活动发送和获取结果的序列图。

img/463887_1_En_12_Fig5_HTML.png

图 12-5

从另一个活动获取结果的序列图

当您向另一个活动发送数据并希望获得一些数据时,您需要使用 startActivityForResult 而不是 startActivity 。这样做的代码如下所示:

startActivityForResult(intent, SECOND_ACTIVITY)

和 startActivity 一样,你将 Intent 对象传递给 startActivityForResult ,除了 Intent 对象,你还需要传递一个请求代码 ( SECOND_ACTIVITY)。这个请求代码对于 MainActivity 非常重要,因为我们将使用它来跟踪从谁那里获取数据。请求代码是一个您需要定义的 Int 。只要您有多个请求代码,每个代码都是不同的,那么您将使用什么号码并不重要。如果您发送数据并期望从几个活动返回数据,您将使用请求代码来跟踪哪些其他活动正在向您发送数据。这样,当结果出来的时候,我们就可以知道我们最初想做什么。

在 SecondActivity 中,当我们准备好发回数据时,我们需要创建另一个 Intent 对象,并使用 putExtra 方法加载数据。之后,我们调用 SecondActivity 的 setResult 方法。setResult 方法有两个参数:一个结果代码和 Intent 对象。如果应用一切正常,使用活动。结果 _ OK;否则使用活动。结果 _ 取消。RESULT_OK 实际上是-1,RESULT_CANCELLED 是 0,但是请不要使用 Int 文字,总是使用提供的类常量。

当您在 SecondActivity 上调用 finish 方法时,它将进入停止状态,MainActivity 将再次出现在前台—因此,它将重启并且恢复。无论 SecondActivity 发送回什么数据,我们都应该能够在 MainActivity 的 onActivityResult 回调中获得它。清单 12-4 显示了一个典型的被覆盖的 onActivityResult 回调。

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  super.onActivityResult(requestCode, resultCode, data)

  if((requestCode == SECOND_ACTIVITY) and (resultCode == Activity.RESULT_OK))  {
    // extract data here
  }
}

Listing 12-4
onActivityResult

注意

您如何知道何时应该覆盖 onActivityResult 回调?如果您使用 startActivityForResult 启动另一个活动,您应该覆盖 onActivityResult 回调——在这里您可以获得发送给您的任何数据。

隐含的意图

我们在前面几节中看到的都是明确意图的例子。明确的意图告诉 Android 运行时精确地激活哪个组件。回到我们的类比,这就像告诉某人去 3 rd 街的杂货店买些糖。另一方面,一个隐含的意图只是简单地给出了“得到一些糖”的指令——在哪里或如何得到糖并不重要。隐含的意图只规定了行动。

当你使用一个隐含的意图时,一般的想法是你想使用一个不存在于你的应用中的功能——如果它存在于你的应用中,你会首先使用一个明确的意图;—因此,您要求 Android 运行时在设备上的某个地方找到一个应用来满足您的请求。

从前面的例子中我们知道,意图可以携带数据;我们用临时演员做了这件事。额外的东西是一个意图可以拥有的四样东西之一;另外三个是动作数据、类别。动作是您想要做的操作(例如,查看、拨号、应答、呼叫等。).数据与操作必须处理的信息类型有关(是 URI、电话号码、图片等。),而类别与哪些组件有资格处理此意图有关。有时,运行时需要类别来过滤或选择那些能够响应我们意图的组件。您可以向活动、广播接收者和服务发送意图,但是在本章中,我们将只处理活动。

通常你需要做四件事来获得一个隐含的意图。您需要:

  1. 创建意图对象

  2. 设置其动作(例如,“查看地图”、“拨打电话”、“拍照”等)。)

  3. 设置其数据;和

  4. 启动意图

清单 12-5 向我们展示了这一切在代码中的样子。

| -什么 | 使用无参数构造函数创建意图。 | | ➋ | 设置意图动作。在这个例子中,我们想要查看一些东西;它可以是联系人、网页、地图、某处的图片等。此时,Android 运行时还不知道您想要查看什么。ACTION_VIEW 是您可以使用的许多意图操作之一。你可以在安卓官方网站找到其他种类的动作([`bit . ly/androidcommonint ents`](http://bit.ly/androidcommonintents))。 | | ➌ | 设置其数据。在这一点上,Android 运行时已经很清楚你在做什么了。在这个例子中,Uri 是一个网页。Android 很聪明地判断出我们想要浏览网页。 | | -你好 | Android 将搜索设备上最符合这一要求的每个应用。如果它发现不止一个应用,它会让用户选择哪一个。如果只找到一个,它将简单地启动该应用。 |
val m_intent = Intent() ➊
m_intent = setAction(Intent.ACTION_VIEW)   ➋
m_intent = setData(Uri.parse("https://workingdev.net")) ➌
startActivity (m_intent) ➍

Listing 12-5Example Intent to Launch a Web Browser

我们可以将清单 12-16 中的代码简化成这样

m_intent = Intent(Intent.ACTION_VIEW,  Uri.parse("https://workingdev.net"))
startActivity(m_intent)

动作和数据可以作为参数传递给 Intent 的构造函数。

任何符合我们意图的组件都不需要为了接收意图而运行。请记住,所有应用都需要一个清单文件。每个应用在清单文件中声明它的能力,特别是通过<intent-filter>部分。Android 的包管理器拥有设备上安装的所有应用的所有信息。Android 的运行时只需要清单文件上的信息来查看哪些应用有能力和/或有资格响应意图。

在接下来的部分中,我们将更详细地探讨隐含和明确的意图。我们将设置示例项目,以便您可以在上面进行练习。

演示 1:发起一项活动

我们不会在这个项目上做任何花哨的东西。我们将简单地创建两个活动:MainActivity 和 SecondActivity。当单击按钮时,我们将从 MainActivity 启动 SecondActivity。项目详情见表 12-1 。

表 12-1

演示应用的项目详情

|

项目详细信息

|

价值

|
| --- | --- |
| 应用名称 | ch12 launchanothersactivity |
| 公司域 | 您的网站名称 |
| Kotlin 支架 | 是 |
| 波形因数 | 仅限手机和平板电脑 |
| 最低 SDK | API 23 棉花糖 |
| 活动类型 | 空的 |
| 活动名称 | 主要活动 |
| 布局名称 | 活动 _ 主要 |
| 向后兼容性 | 是的。应用兼容性 |

当项目在主窗口中打开时,创建第二个活动。方法之一是选择“app”项目工具窗口,如图 12-6 所示,然后从主工具栏点击文件新建活动空活动

img/463887_1_En_12_Fig6_HTML.jpg

图 12-6

项目工具窗口中选择“app”

让我们把它命名为“SecondActivity”,如图 12-7 所示。

img/463887_1_En_12_Fig7_HTML.jpg

图 12-7

新的安卓活动

接下来,转到 activity_main.xml(设计视图)。移除 TextView 元素并用按钮视图替换它。将按钮定位在布局的中心附近,然后使用“推断约束”按钮,如图 12-8 所示

接下来,同样在设计视图中打开 activity_second.xml,然后添加一个按钮视图并将其置于布局的中心,就像您在 activity_main 中所做的那样..

img/463887_1_En_12_Fig8_HTML.png

图 12-8

在布局上居中按钮视图

此时,您应该可以使用以下视图元素和类:

  • 主要活动。Kt 及其关联的 activity_main.xml ,这来自项目创建向导

  • SecondActivity.Kt .及其关联的 activity_second.xml ,这来自活动创建向导

  • activity_main 中的按钮视图对象,其 id 为“Button”——这是项目中第一个按钮元素的默认 id

  • activity_second 中的另一个按钮视图对象,其 id 为“Button 2”——这是项目中第二个按钮元素的默认 id

清单 12-6 和 12-7 分别显示 activity_main 和 activity_second 的代码;如果您尝试自己构建项目,您可以将它们用作参考或比较。

<?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=".SecondActivity">

  <Button
    android:id="@+id/button2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="88dp"
    android:text="Button"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

Listing 12-7/app/res/layout/-->activity_second.xml

<?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">

  <Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="80dp"
    android:text="Button"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

Listing 12-6/app/res/layout/activity_main.xml

清单 12-8 和 12-9 显示了主活动的注释代码。Kt 和 SecondActivity。Kt。

| -什么 | 我们正在定义一个简单的日志对象。我们本可以使用 **android.util.Log** 类,但是我认为大多数阅读本书的人都有 Java 背景,所以这看起来应该很熟悉。参数`MainActivity::` [`class.name`](http://class.name) 大致相当于 Java 的`getClass().getName()`。或者,您也可以将任何字符串传递给`getLogger()`方法——例如`getLogger("My Project")`——但是通常的做法是使用 Logger 对象的类名。 | | ➋ | 我们只是创建一个日志条目,说明我们正在进行 MainActivity 的“onCreate”回调。 | | ➌ | 这是按钮的点击监听器的基本设置;你已经做过了。 | | -你好 | 这条线创建一个意图对象。意图对象的第一个参数是上下文对象;您可以在这里使用应用上下文,但是在我们的例子中,我们使用了活动上下文。`this@MainActivity`是对 MainActivity 的上下文的引用。第二个参数是意图的目标对象。这是给 Android 运行时的一个特定指令,我们想要激活这个对象。第二个参数的类型应该是**类**。MainActivity 的类对象的符号是`MainActivity::class.java`。 | | ➎ | 我们启动意图。 |
import android.content.Intent
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
import java.util.logging.Logger

class MainActivity : AppCompatActivity() {

  val Log = Logger.getLogger(MainActivity::class.java.name)  ➊

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    Log.info("onCreate")  ➋

    button.setOnClickListener { ➌
      val m_intent = Intent(this@MainActivity, SecondActivity::class.java) ➍
      startActivity(m_intent) ➎
    }
  }

  override fun onPause() {
    super.onPause()
    Log.info("onPause")
  }

  override fun onRestart() {
    super.onRestart()
    Log.info("onRestart")
  }

  override fun onResume() {
    super.onResume()
    Log.info("onResume")
  }
}

Listing 12-8Full Listing and Annotated Code of -->MainActivity.Kt

| -什么 | 当我们调用它时,SecondActivity 将处于“停止”状态。 | | ➋ | 当 SecondActivity 进入 **onStart** 回调时,它将对用户可见。无论什么活动在前台,现在都将被移到后台;主活动将进入“暂停”状态。 |
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_second.*
import java.util.logging.Logger

class SecondActivity : AppCompatActivity() {

  val Log = Logger.getLogger(SecondActivity::class.java.name)

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_second)

    Log.info("onCreate")

    button2.setOnClickListener {
      finish()  ➊
    }
  }

  override fun onStart() {
    super.onStart()   ➋
    Log.info("onStart")
  }

  override fun onStop() {
    super.onStop()
    Log.info("onStop")
  }
}

Listing 12-9
-->SecondActivity.Kt

当您从 MainActivity 调用 startActivity 时,运行时将激活 SecondActivity。当 SecondActivity 对用户可见时,这应该发生在 SecondActivity 的 onStart 期间,MainActivity 将进入“暂停”状态。

当你从 SecondActivity 调用finish()时,它会进入“停止”状态。MainActivity 将被带到前台,因此它将重新进入“resume”和“restart”状态。这种相互作用如图 12-9 所示。

img/463887_1_En_12_Fig9_HTML.png

图 12-9

MainActivity、SecondActivity 和运行时的序列图

我已经覆盖了 MainActivity 和 SecondActivity 的一些生命周期回调。您可以检查日志来查看调用生命周期方法的时间和顺序。您可以使用 Logcat 工具窗口来检查应用和系统日志,如图 12-10 所示。

img/463887_1_En_12_Fig10_HTML.jpg

图 12-10

Logcat 工具窗口

演示 2:向活动发送数据

在这个项目中,我们将继续探索显式意图的基本机制。然而,我们并不只是启动另一个活动,我们还会向它发送一些数据。我们将详细讨论如何在意图中加入“额外”以及如何提取它。同样,如果你想编码,项目的细节如表 12-2 所示。

表 12-2

项目详情

|

项目详细信息

|

价值

|
| --- | --- |
| 应用名称 | ch12senddatatoanothersctivity |
| 公司域 | 您的网站名称 |
| Kotlin 支架 | 是 |
| 波形因数 | 仅限手机和平板电脑 |
| 最低 SDK | API 23 棉花糖 |
| 活动类型 | 空的 |
| 活动名称 | 主要活动 |
| 布局名称 | 活动 _ 主要 |
| 向后兼容性 | 是的。应用兼容性 |

与上一节一样,我们还需要创建另一个活动。创建另一个活动,并将其命名为“SecondActivity”

返回 activity_main 并在设计视图中打开它。从布局中删除“Hello”TextView,然后添加一个 EditText 和一个 Button 视图,如图 12-11 所示。对齐元素,在布局中居中,并使用“推断约束”,就像我们在之前的演示项目中所做的那样。

img/463887_1_En_12_Fig11_HTML.jpg

图 12-11

activity_main.xml,设计视图

接下来,在设计视图中打开 activity_second,然后向其中添加一个 TextView 元素。使用“推断约束”(像往常一样)并调整一些属性,如文本大小和文本对齐,如图 12-12 所示。

img/463887_1_En_12_Fig12_HTML.jpg

图 12-12

activity_second.xml,设计模式

到目前为止,您应该已经拥有了以下视图元素和类:

  • 主要活动。Kt 及其关联的activity _ main . XML;这来自项目创建向导。

  • SecondActivity.Kt .及其关联的activity _ second . XML;这来自活动创建向导。

  • activity_main 中的 editText 和 button 视图对象,它们的 id 分别是“EditText”和“Button”。editText 是项目中第一个明文元素的默认 id。

  • activity_second 中的 textView 对象,其 id 为“TextView”,这是项目中第一个 TextView 元素的默认 id。

清单 12-10 和 12-11 分别显示了 activity_main.xml 和 activity_two.xml 的代码。

<?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=".SecondActivity">

  <TextView
    android:id="@+id/textView"
    android:layout_width="324dp"
    android:layout_height="wrap_content"
    android:text="TextView"
    android:textAlignment="center"
    android:textSize="36sp"
    tools:layout_editor_absoluteX="35dp"
    tools:layout_editor_absoluteY="78dp" />
</android.support.constraint.ConstraintLayout>

Listing 12-11/app/res/layout/activity_second.xml

<?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">

  <Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="31dp"
    android:text="Button"
    app:layout_constraintEnd_toEndOf="@+id/editText"
    app:layout_constraintStart_toStartOf="@+id/editText"
    app:layout_constraintTop_toBottomOf="@+id/editText" />

  <EditText
    android:id="@+id/editText"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="49dp"
    android:ems="10"
    android:inputType="textPersonName"
    android:text="Name"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

Listing 12-10/app/res/layout/activity_main.xml

清单 12-12 和 12-13 分别显示了 MainActivity 和 SecondActivity 的注释代码。

| -什么 | 我们正在获取用户在 EditText 对象中输入的任何内容的值。这样做的语法实际上是`editText.getText().toString()`但是 Kotlin 用 getters 和 setters 的语法糖使我们的生活变得更容易。我们可以使用属性“ **text** ”来设置或获取 EditText 视图的运行时值。我们必须调用`toString()`函数,因为`EditText.getText()`的返回类型是**可编辑的**或**字符序列**。我需要它是字符串类型的,因为 **putExtra** 既不接受 Editable 也不接受 CharSequence 它收绳子。 | | ➋ | 我们正在创建一个明确的意图,其目标是 **SecondActivity。** | | ➌ | 现在我们要放一些数据在意图上。putExtra 的两个参数看起来像一个*键-值*对;他们确实是。*键*是第一个参数,“main_activity_data”,而*值*是 EditText 的运行时内容——当然是转换成字符串。 | | -你好 | 我们正在发送意向对象。 |
import android.content.Intent
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    button.setOnClickListener {
      val m_data = editText.text.toString() ➊
      val m_intent = Intent(this@MainActivity, SecondActivity::class.java) ➋
      m_intent.putExtra("main_activity_data", m_data) ➌
      startActivity(m_intent) ➍
    }
  }
}

Listing 12-12
MainActivity

| -什么 | 我们正在获取对与 SecondActivity 相关联的 Intent 对象的引用,我们在这里没有创建新的 Intent 对象。语法实际上是`getIntent()`,但是因为 Kotlin 的魔法酱,我们把它简单地称为**意图** | | ➋ | Intent 对象的 **getStringExtra** 方法正在做您认为它在做的事情。它使用地图习惯用法从意图对象中提取一些数据;你给它一个键,你会得到一个值。在本例中,我们给它指定了键“main _ activity _ data——这是我们在 MainActivity 中使用的同一个键。我们使用了 **getStringExtra** 方法,因为我们知道它包含一个字符串。*的取放器*应与*的放放器*对应。如果你放入*字节、数组或包*,那么你应该分别得到 **getByteExtra** 、 **getArrayExtra、**和 **getBundleExtra** 。 | | ➌ | 我们正在改变文本视图的运行时值。我们把它设置成我们从意图号外得到的任何东西。 |
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_second.*

class SecondActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_second)

    val m_data = intent.getStringExtra("main_activity_data") ➊ ➋
    textView.setText(m_data) ➌
  }
}

Listing 12-13
SecondActivity

运行程序并尝试在编辑文本上键入。当您单击按钮时,SecondActivity 上的 TextView 应该显示您键入的任何内容。

演示 3:向活动发送数据并从中获取数据

在这个项目中,我们将要求用户输入他的体重和身高,然后我们将计算他的身体质量指数(身体质量指数)。该项目有两个活动:主活动和次活动。

我们将要求用户在 MainActivity 上输入他的身高和体重。我们将通过一个意向把数据发送给 SecondActivity。在 SecondActivity 中,我们将从 MainActivity 发送给我们的意图中提取数据。我们将使用身高和体重数据来计算身体质量指数,然后将其发送回 MainActivity。

如果你想继续,我已经在表 12-3 中列出了项目细节。

表 12-3

演示应用的项目详情

|

项目详细信息

|

价值

|
| --- | --- |
| 应用名称 | ch12 sendanddgetdatabackfrom activity |
| 公司域 | 您的网站名称 |
| Kotlin 支架 | 是 |
| 波形因数 | 仅限手机和平板电脑 |
| 最低 SDK | API 23 棉花糖 |
| 活动类型 | 空的 |
| 活动名称 | 主要活动 |
| 布局名称 | 活动 _ 主要 |
| 向后兼容性 | 是的。应用兼容性 |

和前面的演示一样,这个项目也有两个活动,但是它有更多的视图元素。像在前面的演示中那样创建两个活动。

MainActivity 有两个视图元素:两个用于用户输入的 EditTexts、一个按钮和一个 TextView,我们将使用它来显示身体质量指数。您可以在清单 12-14 中找到视图对象的详细信息,如 id、文本大小;这是 activity_main.xml 的完整代码。

我给了视图一个非常简单的安排——我简单地将它们垂直打包并居中。我也没有太在意布局的限制。在目测了一个我认为不那么令人反感的布局后,我使用了“推断约束”按钮来自动修复所有的布局约束,就像我们在之前的演示中所做的那样。图 12-13 说明了如何管理 activity_main 的布局。

img/463887_1_En_12_Fig13_HTML.png

图 12-13

【activity _ main 的基本布局

示例代码不会特意以编程方式验证输入,所以我们将在编辑文本上放置一些验证机制。体重和身高输入字段应该只接受数字,特别是浮点数;我们可以通过设置 EditText 视图的 inputType 属性来实现这一点。以下是如何做到这一点:

  1. 在设计视图上编辑 activity_main 时,选择一个编辑文本视图。

  2. 在属性工具窗口中,单击“输入类型”

  3. 选择“数字十进制”

  4. 对其他编辑文本重复步骤 1-3。

图 12-14 说明了这一过程。

img/463887_1_En_12_Fig14_HTML.png

图 12-14

对编辑文本施加验证约束

这应该可以处理 MainActivity 的 UI。清单 12-14 显示了 activity_main.xml 的完整代码

<?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">

  <EditText
    android:id="@+id/input_weight"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="68dp"
    android:ems="10"
    android:inputType="numberDecimal"
    android:text="Name"
    app:layout_constraintEnd_toEndOf="@+id/input_height"
    app:layout_constraintStart_toStartOf="@+id/input_height"
    app:layout_constraintTop_toTopOf="parent" />

  <EditText
    android:id="@+id/input_height"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="23dp"
    android:ems="10"
    android:inputType="textPersonName"
    android:text="Name"
    app:layout_constraintEnd_toEndOf="@+id/btn_send_data"
    app:layout_constraintStart_toStartOf="@+id/btn_send_data"
    app:layout_constraintTop_toBottomOf="@+id/input_weight" />

  <Button

    android:id="@+id/btn_send_data"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="21dp"
    android:text="calculate BMI"
    app:layout_constraintEnd_toEndOf="@+id/txt_bmi"
    app:layout_constraintStart_toStartOf="@+id/txt_bmi"
    app:layout_constraintTop_toBottomOf="@+id/input_height" />

  <TextView
    android:id="@+id/txt_bmi"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="33dp"
    android:text="TextView"
    android:textSize="36sp"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/btn_send_data" />
</android.support.constraint.ConstraintLayout>

Listing 12-14/app/res/layout/activity_main.xml

您可以使用 AS3 中的上下文菜单来创建 SecondActivity。右键点击项目文件夹中的“app”,然后新建活动清空活动,如图 12-15 所示。

img/463887_1_En_12_Fig15_HTML.jpg

图 12-15

创建一个新的空活动

填写新活动的详细信息,如图 12-16 所示。确保新活动的名称是 SecondActivity ,并且您正在与 MainActivity 相同的包中创建它。

img/463887_1_En_12_Fig16_HTML.jpg

图 12-16

创建第二个活动

SecondActivity 有两个视图元素:一个显示传递给它的意图内容的 TextView 和一个触发身体质量指数计算的按钮。图 12-17 显示了 SecondActivity 的用户界面。将布局中的元素居中,并使用“推断约束”将元素锚定到位。你也可以根据自己的喜好调整文本视图的文本对齐文本大小属性。

img/463887_1_En_12_Fig17_HTML.jpg

图 12-17

activity_second.xml

清单 12-15 显示了 activity_second.xml 的完整代码

<?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=".SecondActivity">

  <TextView
    android:id="@+id/txt_intentdata"
    android:layout_width="346dp"
    android:layout_height="wrap_content"
    android:layout_marginTop="109dp"
    android:text="TextView"
    android:textAlignment="center"
    android:textSize="24sp"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

  <Button
    android:id="@+id/btn_calculate"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="29dp"
    android:text="calc bmi"
    app:layout_constraintEnd_toEndOf="@+id/txt_intentdata"
    app:layout_constraintStart_toStartOf="@+id/txt_intentdata"

    app:layout_constraintTop_toBottomOf="@+id/txt_intentdata" />
</android.support.constraint.ConstraintLayout>

Listing 12-15/app/res/layout/activity_second.xml

让我们放大一下 MainActivity 的 onCreate 方法。应用一打开,EditText 就会等待用户输入。一旦用户点击按钮,我们的应用将收集输入并有目的地发送出去。

清单 12-16 显示了 MainActivity 的注释片段,其中包含了单击按钮时的事件处理代码。

| -什么 | 我们声明并定义了一个属性,它将作为某种常量。这就是我们稍后在代码中用作*请求代码*的内容。 | | ➋ | 我们正在设置纯文本视图的*提示*属性。提示显示为文本的灰色占位符。如果您在 HTML 5 中使用了占位符属性,提示属性与此类似。您可以使用提示来代替标签。 | | ➌ | 我们正在定义一个显式意图,`this@MainActivity`是上下文,意图目标是一个类对象(`SecondActivity::class.java`)。 | | -你好 | 我们需要向 SecondActivity 发送两个数据点,当需要发送多对键值对时,最好使用 Bundles。 | | ➎ | 像 Intent 一样, **Bundle** 对象也允许我们以几种方式向它添加数据。我在这个例子中使用了`putFloat()`,因为我想处理浮点数。如果需要处理字符串,字节,字符,整型等。,只需使用适当的 **putXXX** 方法即可。 | | ➏ | 我们正在加载以捆绑到意图对象。使用有意图的包允许我们处理更复杂的数据结构。 | | -好的 | 我们正在发送活动,但是这一次,我们告诉运行时我们期望一些数据返回——这就是为什么我们使用 **startActivityForResult** 。每当其他活动调用 MainActivity 的`finish()`方法时,这就通知运行时调用 main activity 的 **onActivityResult** 回调。startActivityForResult 的第二个参数是请求代码。当我们收到返回的结果时,请求代码将帮助我们路由程序逻辑。在这个调用中,我们使用了类常量 **SECOND_ACTIVITY** 作为启动 SecondActivity 的请求代码,这意味着当 SecondActivity 调用它的`finish()`方法时,这个请求代码也会被发送回 MainActivity。 |
val SECOND_ACTIVITY = 1000 ➊

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)

  input_weight.setHint("weight (lbs)")     ➋
  input_height.setHint("height (inches)")

  btn_send_data.setOnClickListener {

    val m_intent = Intent(this@MainActivity, SecondActivity::class.java) ➌
    val m_bundle = Bundle()   ➍

    m_bundle.putFloat("weight", input_weight.text.toString().toFloat()) ➎
    m_bundle.putFloat("height", input_height.text.toString().toFloat())
    m_intent.putExtra("main_activity_data", m_bundle)  ➏

    startActivityForResult(m_intent, SECOND_ACTIVITY) ➐
  }
}

Listing 12-16onCreate Method of MainActivity

练习的下一个阶段发生在 SecondActivity 的 onCreate 回调上。在我们将身高和体重数据发送给接收活动之后,我们必须提取并处理这些数据。清单 12-17 显示了该代码的注释片段。

| -什么 | 我们需要获得对与 SecondActivity 关联的 Intent 对象的引用。这是我们从 MainActivity 中启动的同一个 Intent 对象。这也是激活 SecondActivity 的相同意图。为了获得关联的 Intent 对象,我们应该调用`getIntent()`,但是因为我们使用的是 Kotlin,而不是方法`getIntent()`,我们简单地称它为**Intent**——属性而不是方法。请记住,我们在这里并不是创建一个新的意图,我们只是获得一个与 SecondActivity 相关的意图的引用。我们在 MainActivity 中发送了一个包,所以我们应该使用 **getBundleExtra** 来获取数据。 | | ➋ | 既然我们已经获得了包,我们需要开始从包中获取更多的数据。我们使用 **putFloat** 将数据放入包中,因此,我们需要使用 **getFloat** 将数据取出。 | | ➌ | 我们将文本视图的*文本*属性设置为串联的*高度*和*重量*字符串。 | | -你好 | 在这一行中,我们正在创建一个新的意图对象。这个活动将把一些数据发送回 MainActivity。我们需要一个新的意图来做这件事。 | | ➎ | 这是一种计算身体质量指数的简单方法,但应该可行。 | | ➏ | 现在我们已经计算了身体质量指数,让我们加载到我们新创建的意图对象。 | | -好的 | **setResult** 方法接受两个参数:a.**结果代码**。这不是 0 就是-1。一般来说,如果出错了,你应该返回-1,或者如果一切顺利,你应该返回 0。但是在 Activity 类中使用类常量是个好主意。**活动。结果 _OK** 为-1,**活动。RESULT_CANCELLED** 为 0。b.**意图**。这是包含计算身体质量指数的目的对象。 | | -好的 | 最后,为了将计算结果返回给 MainActivity,我们需要调用`finish().` |
override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_second)

  val bundle = intent.getBundleExtra("main_activity_data") ➊
  val height = bundle.getFloat("height") ➋
  val weight = bundle.getFloat("weight")

  txt_intentdata.text = "Height: $height | Weight: $weight" ➌

  btn_calculate.setOnClickListener {
    val m_intent = Intent()  ➍
    val m_bmi = 703 * (weight / (height * height)) ➎
    m_intent.putExtra("second_activity_data", m_bmi) ➏
    setResult(Activity.RESULT_OK, m_intent) ➐
    finish() ➑
  }
}

Listing 12-17onCreate Method of SecondActivity

意图之旅的下一部分是回到主活动。在 SecondActivity 调用完成后,运行时将调用 MainActivity 上的 onActivityResult 回调——在这个回调中,我们有机会处理 SecondActivity 发送给我们的任何数据。清单 12-18 向我们展示了 MainActivity 的 onActivityResult 的注释片段。

| -什么 | 该表达式中有两个测试:1.`requestCode == SECOND_ACTIVITY`。我们在问数据是否来自 SecondActivity。2.`Activity.RESULT_OK`。我们正在尝试查看 SecondActivity 是否调用了 setResult 并实际调用了 finish。 | | ➋ | 既然我们知道数据来自 SecondActivity,并且一切顺利,我们就可以从意图中提取数据了。我们使用了 **getFloatExtra** ,因为我们知道它包含了一个浮点——毕竟我们把它放在了那里。我们不得不在`data?.getFloatExtra()`中使用*安全调用*(问号),因为传递给 onActivityResult 的 Intent 对象的签名是可空类型。 | | ➌ | 我们可以显示计算出的身体质量指数值。 |
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  super.onActivityResult(requestCode, resultCode, data)

  if((requestCode == SECOND_ACTIVITY) and (resultCode == Activity.RESULT_OK))  { ➊
    val bmi = data?.getFloatExtra("second_activity_data", 1.0F) ➋
    txt_bmi.setText(bmi.toString()) ➌
  }
}

Listing 12-18Annotated onActivityResult of MainActivity

如果您正在编写代码,那么现在您应该能够将整个应用拼凑起来。

清单 12-19 显示了 MainActivity 的完整代码。您可能会注意到这个完整列表与列表 12-16 和 12-18 之间的一些差异。为了简洁明了,我省略了清单 12-16 和 12-18 中的一些其他细节。在清单 12-19 中,我放回了所有省略的部分,并且对它们进行了注释,这样您可以更容易地发现它们。

| -什么 | 让我们清除输入字段。我们将这个调用放在 **onResume** 回调中,这样每次活动对用户可见时,输入字段都是清晰的。您可能还记得,在活动的生命周期中,可以多次调用 **onResume** 生命周期方法。当应用启动时,它将被第一次调用。当 SecondActivity 调用 **finish** 时会被第二次调用,MainActivity 会从后台堆栈中弹出,以此类推。 | | ➋ | 我没有使用返回可空类型的`data?.getExtra(),`,而是使用了返回不可空类型的`data!!.getExtra(),`。我这样做是为了简化我们在 **gerBMIDescription** 函数中的代码,它需要一个不可空的类型。我们本可以在 **getBMIDescription** 中处理可空类型,但是我选择使用更简单的方法处理不可空类型。 | | ➌ | 该函数接受一个身体质量指数浮点值,并返回一个权重描述。 | | -你好 | `initializeInputs()`的实现。我们只是将 EditTexts 的文本属性*设置为一个空字符串。* |
import android.app.Activity
import android.content.Intent
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

  val SECOND_ACTIVITY = 1000

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    input_weight.setHint("weight (lbs)")
    input_height.setHint("height (inches)")

    btn_send_data.setOnClickListener {

      val m_intent = Intent(this@MainActivity, SecondActivity::class.java)
      val m_bundle = Bundle()

      m_bundle.putFloat("weight", input_weight.text.toString().toFloat())
      m_bundle.putFloat("height", input_height.text.toString().toFloat())
      m_intent.putExtra("main_activity_data", m_bundle)

      startActivityForResult(m_intent, SECOND_ACTIVITY)
    }
  }

  override fun onResume() {
    super.onResume()
    clearInputs()  ➊
  }

  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    if((requestCode == SECOND_ACTIVITY) and (resultCode == Activity.RESULT_OK))  {
      val bmi = data!!.getFloatExtra("second_activity_data", 1.0F) ➋
      val bmiString = "%.2f".format(bmi)
      input_height.setText("")
      input_weight.setText("")
      txt_bmi.setText("BMI : $bmiString ${getBMIDescription(bmi)}")
    }
  }

  private fun getBMIDescription(bmi: Float) : String { ➌

    return when (bmi) {
      in 1.0..18.5 -> "Underweight"
      in 18.6..24.9 -> "Normal weight"
      in 25.0..29.9 -> "Overweight"
      else -> "Obese"
    }
  }

  private fun clearInputs() { //  ➍
    input_weight.setText("")
    input_height.setText("")
  }
}

Listing 12-19Full Code Listing for MainActivity

演示 4:隐含的意图

我们最后一个演示应用的特点是隐含的意图。在本节中,我们将处理三种类型的数据:web URI、地理坐标和电话号码。希望这三个例子能给你足够的洞察力和立足点,让你继续探索隐含的意图。像往常一样,如果你想编码,项目细节如表 12-4 所示。

表 12-4

演示应用的项目详情

|

项目详细信息

|

价值

|
| --- | --- |
| 应用名称 | CH12ImplicitIntents |
| 公司域 | 使用您的网站名称 |
| Kotlin 支架 | 是 |
| 波形因数 | 仅限手机和平板电脑 |
| 最低 SDK | API 23 棉花糖 |
| 活动类型 | 空的 |
| 活动名称 | 主要活动 |
| 布局名称 | 活动 _ 主要 |
| 向后兼容性 | 是的。应用兼容性 |

该应用有一个简单的设置,我在 activity_main.xml 上做的唯一一件事就是删除“Hello World”TextView。我使用 Options 菜单来帮助用户选择启动三个示例意图。选项菜单在动作栏上,如图 12-18 所示。

img/463887_1_En_12_Fig18_HTML.jpg

图 12-18

MainActivity 菜单

在 UI 部分没有什么要做的,所以没有必要显示 activity_main 的 XML 列表。我们需要做的一切都在 MainActivity 内部完成。

在前面的章节中,我们使用 XML 资源构建了菜单;在这个例子中,我构建的菜单有点不同。我没有使用 XML 资源,而是动态构建了所有的菜单项。清单 12-20 显示了 MainActivity 的完整和带注释的代码。

| -什么 | 在调用了 **onCreate** 方法之后,将会调用 **onCreateOptionsMenu** 回调。在 API 11 (Honeycomb)之前, **onCreateOptionsMenu** 只有在用户点击手机的*选项*按钮时才会被调用,但从 Honeycomb 开始,现在被称为 onCreate。这种行为变化的主要原因是因为 ActionBar 是从 API 11 开始引入的。因为我们使用的是 API 23,所以我们可以利用这个行为来构建一个简单的菜单。 | | ➋ | 我们将动态添加一个菜单项。 | | ➌ | 每当用户点击其中一个菜单项,就会调用 **onOptionsItemSelected** 。这是我们处理菜单点击的地方。 | | -你好 | **项**参数可以告诉我们点击了哪个菜单项。我们将它转换成字符串,这样我们就可以用它在 **when** 表达式中路由我们的程序逻辑。 | | ➎ | 这是创建意图的简化版本。 |
import android.content.Intent
import android.net.Uri
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
  }

  override fun onCreateOptionsMenu(menu: Menu?): Boolean { ➊
    menu?.add("Web") ➋
    menu?.add("Map")
    menu?.add("Phone number")
    return super.onCreateOptionsMenu(menu)
  }

  override fun onOptionsItemSelected(item: MenuItem?): Boolean { ➌

    var m_uri: Uri

    var m_intent: Intent = Intent()

    when (item?.toString()) { ➍
      "Web" -> {
        m_uri = Uri.parse("https://www.apress.com")
        m_intent = Intent(Intent.ACTION_VIEW, m_uri) ➎
      }
      "Map" -> {
        m_uri = Uri.parse("geo:40.7113399,-74.0263469")

//      This would have worked as well
//      m_uri = Uri.parse("https://maps.google.com/maps?q=40.7113399,-74.0263469")
        m_intent = Intent(Intent.ACTION_VIEW, m_uri)

      }
      "Phone number" -> {
        m_uri = Uri.parse("tel:639285083333")
        m_intent = Intent(Intent.ACTION_DIAL, m_uri)
      }

    startActivity(m_intent)
    return true
  }
}

Listing 12-20
MainActivity

图 12-19 显示了我们的应用的运行时快照。

img/463887_1_En_12_Fig19_HTML.jpg

图 12-19

隐含意图,运行

章节总结

  • 意图用于组件激活。

  • 有两种意图:隐性的和显性的。

  • 明确的意图让我们可以处理多种活动。您可以使用明确的意图来激活特定的活动。

  • 隐式意图扩展了应用的功能。它让你的应用做一些超出应用功能的事情。

  • 你可以通过意图发送和接收数据。

在下一章中,我们将:

  • 查看并简要了解材质设计(不多)。

  • 了解如何在我们的应用中创建和应用样式和主题。

  • 了解如何在动作栏中添加菜单。

十三、主题和菜单

我们将介绍的内容:

  • 主题和颜色

  • 菜单

谷歌 Play 商店有将近 350 万个应用。有很多应用可供选择,这对用户来说是好事,但对开发者来说,竞争非常激烈。

如果你要发布一个应用,你需要润色它——即使只是修饰一下——这样才不会显得寒酸。即使你有一个杀手级应用,你也应该考虑它对用户来说是什么样子(和感觉)。记住,不管你的代码有多棒,用户看到的不是代码,而是用户界面。

谷歌发布了一套用户界面指南。他们称之为材质设计,你可以在 http://material.io 了解更多信息。材质设计是一个很大的主题,它本身就可以填满整本书,我们不打算涵盖所有内容,但在这一章中,我们将看看主题以及如何将 AppBar 添加到您的应用中。

样式和主题

Android 平台有类似“风格”和“主题”的概念。样式是属性的集合,您可以在其中控制视图的外观、背景和前景色、字体大小等等。另一方面,主题是应用于整个应用的风格,而不仅仅是单个视图。当您将样式应用为主题时,应用中的每个视图都会遵循该主题。Android Manifest 的应用节点中的应用应用了一个主题,如以下代码片段所示:

android:theme="@style/AppTheme"

在本例中,“AppTheme”是样式的名称。样式在 app ➤ res ➤ styles.xml 中被写成 XML 文件——文件名通常是 style.xml,但它可以改变,这不是硬性要求。清单 13-1 显示了当前的 styles.xml 这是我们在项目创建向导之后得到的结果。

<resources>
 <!-- Base application theme. -->
  <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
  </style>
</resources>

Listing 13-1app/res/values/styles.xml

styles (styles.xml)的根节点是“resources”,您可以在这个节点下定义任意多的样式。样式节点具有属性“名称”和“父节点”name 属性是您选择的东西,比如变量、类或函数的名称。父属性是您需要从一组现有主题中选择的东西。AS3 会用提示帮你解决,如图 13-1 所示。

img/463887_1_En_13_Fig1_HTML.jpg

图 13-1

编辑 styles.xml 时的代码提示

一旦定义了样式节点,就可以开始为应用定制颜色了。颜色被定义为“style”元素中的“item”条目。

谷歌的材质设计通过使用贯穿整个应用的原色和强调色,赋予你的品牌身份以生命。这些颜色定义如下:

  • colorPrimary: 应用栏的颜色

    colorPrimaryDark: 状态栏和上下文应用栏的颜色;这是彩色原色的黑暗版本

  • colorAccent: 复选框、单选按钮和编辑文本框等视图的颜色

  • 窗口背景:屏幕背景的颜色

  • textColorPrimary: 应用栏中 UI 文本的颜色

  • 状态栏颜色:状态栏的颜色

  • 导航条颜色:导航条的颜色

您不必在 styles.xml 中定义所有这些,但是如果您愿意,您可以这样做。您可能已经注意到,颜色项的值本身并没有在 styles.xml 文件中定义,而是被重定向到另一个资源文件。在 styles.xml 中,当您看到这样的条目时

<item name="colorPrimary">@color/colorPrimary</item>

这意味着“colorPrimary”的实际值可以在 colors.xml 文件中找到,该文件位于 app ➤研究➤值文件夹中。清单 13-2 显示了 colors.xml 的当前内容

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="colorPrimary">#3F51B5</color>
  <color name="colorPrimaryDark">#303F9F</color>
  <color name="colorAccent">#FF4081</color>
</resources>

Listing 13-2app/res/values/colors.xml

自定义主题

您可以通过两种方式编辑颜色。您可以直接编辑 colors.xml 文件,也可以使用 AS3 的主题编辑器修改颜色。要使用主题编辑器,在主编辑器中打开 styles.xml 文件,然后点击右上角的“打开编辑器”链接,如图 13-2 所示。

img/463887_1_En_13_Fig2_HTML.jpg

图 13-2

启动“打开编辑器”

主题编辑器允许您更改应用的颜色值。它还向您展示了应用在给定的配色方案下的外观。图 13-3 显示了主题编辑器的各个部分。

img/463887_1_En_13_Fig3_HTML.png

图 13-3

主题编辑器

要改变颜色,单击材质颜色旁边的样本(如图 13-3 )。这将启动拾色器(如图 13-4 )。

img/463887_1_En_13_Fig4_HTML.jpg

图 13-4

颜色选择器

Google 在 http://bit.ly/materialdesigndox 发布了关于材质设计的文档;在修改配色方案之前,最好先阅读一下。你可以使用的另一个网络资源是 materialPalette.com;它面向 Android 材质设计。图 13-5 显示了他们网站的截图。

img/463887_1_En_13_Fig5_HTML.jpg

图 13-5

截图自 https://www.materialpalette.com

基本的想法是选择两种颜色,网站为你建立一个调色板。现在你可以简单地复制原色、深原色、强调色、浅原色和其他颜色的十六进制值。

菜单

菜单在 UI 设计中非常重要。它们允许用户使用应用的功能。传统上,菜单系统是分层次地组织在介绍组中的,这意味着在用户到达他的目标动作之前,他需要遍历菜单的层次结构。Android 的菜单系统,在某个时间点上,已经完全像那样表现了——分组和分级。但那是过去的事了。Android 的菜单方式在其生命周期中发生了巨大的变化。

Android Honeycomb 之前的菜单依赖于硬件按钮,如图 13-6 所示。

img/463887_1_En_13_Fig6_HTML.png

图 13-6

旧 Android 硬件上的菜单

那时候,我们总是可以相信“home”和“option”按钮会出现在任何 Android 手机上。我们基于这些假设开发我们的应用,因为它们在当时是合理的。

时代变了,安卓硬件也变了。屏幕分辨率大幅提高,硬件按键消失。幸运的是,Android 的菜单方式也发生了变化,并跟上了硬件功能的发展。

Honeycomb 问世时,Android 中加入了一种新的菜单系统。最低目标 SDK 是 API 11 的应用现在可以使用“ActionBar”

如图 13-7 所示,ActionBar 是屏幕顶部的一个专用区域,在整个应用中持续存在。仔细想想很像 AS3 的主菜单栏。

img/463887_1_En_13_Fig7_HTML.jpg

图 13-7

带有动作栏的应用

您可以使用操作栏来显示应用最重要的功能,并以可预测的方式访问它们(例如,在顶部放置一个永久的搜索小部件,等等)。).它通过消除菜单中的混乱来创建一个更整洁的外观,并且在菜单中的所有项目不能适合屏幕的情况下,动作栏会显示一个溢出图标。溢出图标是一个垂直省略号,由三个点垂直排列而成,通常位于工具栏的最右侧。它还显示应用的名称,因此它强化了应用的品牌身份。

如今,ActionBar 已经有点过时了,已经被工具栏盖过了。工具栏更加通用,因为它不是永久地夹在屏幕的顶部,你可以把它放在任何你想放的地方,而且它有更多的功能。然而,对于简单的菜单系统,ActionBar 仍然是一个可行的解决方案;事实上,没有什么可以阻止你在应用中同时使用动作栏和工具栏。使用你拥有的最好的工具。

在 Android API level 10 或更低版本中,当用户按下硬件菜单按钮时,菜单选项会出现在屏幕底部。在 Android API 11 及更高版本中,选项菜单中的项目在应用栏中可用。默认情况下,系统会将所有项目放置在动作溢出中,用户可以通过应用栏右侧的动作溢出图标来显示。

要将菜单添加到应用,您需要执行以下操作:

  1. 创建一个菜单资源文件。我们将在 app/res 文件夹中创建一个菜单文件夹。然后,我们将在其中创建一个菜单资源文件。

  2. 在主程序中展开菜单资源。我们将覆盖 MainActivity 的onCreateOptionsMenu并调用菜单对象的膨胀函数。

  3. 添加事件处理程序到菜单项。我们将覆盖 MainActivity 的 onOptionsItemSelected 函数,并根据单击了哪个菜单项来路由用户操作。

  4. 或者,将矢量图像添加到菜单中。

让我们创建一个演示应用,这样我们就可以探索菜单。项目详情见表 13-1 。

表 13-1

演示应用的项目详情

|

项目明细

|

|
| --- | --- |
| 应用名称 | CH13AppBar(消歧义) |
| 公司域 | 使用您的网站名称 |
| Kotlin 支架 | 是 |
| 波形因数 | 仅限手机和平板电脑 |
| 最低 SDK | API 23 棉花糖 |
| 活动类型 | 空的 |
| 活动名称 | 主要活动 |
| 布局名称 | 活动 _ 主要 |
| 向后兼容性 | 是的。应用兼容性 |

我们不会在这个应用中添加任何额外的视图元素,因为它们不会被需要,但我们会将 and android:id 添加到我们的布局容器中。注意清单 13-3 中的第六行:ID 属性在默认情况下是不存在的,你需要把它放进去。每个视图元素的 id 现在对我们来说更重要了,因为 Kotlin Android 扩展依赖于它。如果没有视图 id,扩展将无法合成视图 id。

<?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:id="@+id/root_layout"
  tools:context=".MainActivity">
</android.support.constraint.ConstraintLayout>

Listing 13-3excerpt from activity_main.xml

我们还需要编辑模块级的 build.gradle 文件。为了使用 Snackbar 小部件,我们需要在 gradle 文件中包含“com.android.support:design”依赖项。图 13-8 显示了 gradle 文件在项目窗口中的位置。

img/463887_1_En_13_Fig8_HTML.jpg

图 13-8

模块级 build.gradle

您需要添加“com.android.support:design”行,如清单 13-4 所示。

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:27.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    implementation 'com.android.support:design:27.1.0'
}

Listing 13-4excerpt from build.gradle

AS3 会感觉到在构建文件中有什么改变,它会要求你“同步”gradle 文件。该提示将在主编辑器的上部显示为黄色条。单击“同步”以便继续。

现在我们准备创建菜单文件,但在此之前,让我们创建一个菜单文件夹。在项目窗口中右键点击 appres 文件夹,如图 13-9 所示。选择新建安卓资源目录

img/463887_1_En_13_Fig9_HTML.jpg

图 13-9

创建新的 Android 资源目录

给新创建的文件夹命名,如图 13-10 所示。

img/463887_1_En_13_Fig10_HTML.jpg

图 13-10

新菜单文件夹

现在我们有了一个菜单文件夹,右击它并创建一个新的菜单资源文件,如图 13-11 所示。

img/463887_1_En_13_Fig11_HTML.jpg

图 13-11

新菜单资源文件

让我们给新创建的菜单文件命名,如图 13-12 所示。

img/463887_1_En_13_Fig12_HTML.jpg

图 13-12

主菜单资源文件

让我们在菜单文件中添加一些项目。在主编辑器中打开文件app/RES/menu/main _ menu . XML,添加如清单 13-5 所示的菜单项。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

  <item android:id="@+id/menuFile"
    android:title="@string/menuFile"
    />
  <item android:id="@+id/menuEdit"
    android:title="@string/menuEdit"
    />
  <item android:id="@+id/menuHelp"
    android:title="@string/menuHelp"
    />
  <item android:id="@+id/menuExit"
    android:title="@string/menuExit"
    />
</menu>

Listing 13-5app/res/menu/main_menu.xml

清单 13-5 中的每个项目元素代表一个菜单项。每个元素由两个属性组成:一个 android:id 和一个 android:title 。标题是您将在菜单上看到的内容,id 是对菜单项的编程引用。当我们想从程序中引用一个菜单项时,我们将使用这个 id。

android:id 是用 @+id 符号写的,这样在它还不存在的情况下会被创建。 android:title 是用 @string 符号编写的,所以标题的值在 app/res/values/strings.xml 文件中被解析。我们可以像这样对菜单标题进行硬编码:

<item android:id="@+id/menuFile"
  android:title="File" />

但这是一种糟糕的做法。Android 编程的惯例是在 strings.xml 资源文件中存储和检索字符串。将您的字符串存储在/app/res/values/strings.xml 中还可以让您更容易发布非英语版本的应用。想象一下,如果你创建了一个法语或意大利语版本的应用。您必须手动替换所有这些硬编码的字符串。但是如果您将字符串存储在 xml 文件中,那么您只需要在一个文件中替换它,这使得本地化和国际化变得更加容易。

一旦输入完菜单文件,您会注意到 AS3 抱怨新创建的菜单项。android:title 条目无法解析或者无法在 strings.xml 中找到。当然 AS3 找不到它——我们还没有创建它。

我们既可以手动将新条目添加到 strings.xml,也可以使用 AS3 的快速修复来解决错误。让我们使用快速修复。当 main_menu.xml 仍然在编辑器上时,点击@string/menuExit,如图 13-13 所示,然后按 OPTION + ENTERALT + ENTER

img/463887_1_En_13_Fig13_HTML.jpg

图 13-13

将菜单标题添加到 strings.xml

键入该项目的资源值,并对每个 android:title 属性重复这些步骤。资源值将存储在 app ➤ res ➤值➤ strings.xml 中——strings . XML 的内容如清单 13-6 所示。

<resources>
  <string name="app_name">CH13AppBar</string>
  <string name="menuExit">Exit</string>
  <string name="menuHelp">Help</string>
  <string name="menuEdit">Edit</string>
  <string name="menuFile">File</string>
</resources>

Listing 13-6app/res/values/strings.xml

下一步是将菜单与主活动关联起来。为此,我们需要通过覆盖 MainActivity 中的 onCreateOptionsMenu 来扩展菜单文件。

打开主活动。Kt,并开始添加一个顶级函数。一旦你开始输入 onCreateOptionsMenu 的前几个字符,AS3 会通过给出代码提示来帮助你。使用如图 13-14 所示的自动完成功能来完成功能的框架。

img/463887_1_En_13_Fig14_HTML.jpg

图 13-14

自动完成 onCreateOptionsMenu

复制清单 13-7 中的代码来完成 onCreateOptionsMenu。

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
  menuInflater.inflate(R.menu.main_menu, menu)
  return super.onCreateOptionsMenu(menu)
}

Listing 13-7onCreateOptionsMenu

inflate() 函数使用我们之前创建的菜单 XML 文件(第一个参数)创建菜单项,并将其附加到菜单对象(inflate 函数的第二个参数)。Android 运行时在调用 onCreateOptionsMenu 回调函数时会把菜单传给我们。

图 13-15 显示运行时的菜单;左边的图片显示了溢出图标——它是三个白点,像垂直省略号一样排列。通过点击或触摸溢出图标显示菜单项。右图显示了我们的应用,显示了所有菜单项。

img/463887_1_En_13_Fig15_HTML.jpg

图 13-15

查帕尔菜单

现在,菜单项出现了,但是它们还没有做任何事情。为了处理每个菜单项的事件,我们将在 MainActivity 中覆盖 onOptionsItemSelected() 函数。

清单 13-8 显示了被覆盖的 onOptionsItemSelected 的代码。每次用户单击菜单项时,Android 运行时都会调用该方法。运行库将 MenuItem 对象传递给表示所单击的菜单项的函数。

override fun onOptionsItemSelected(item: MenuItem?): Boolean {
  return true
}

Listing 13-8onOptionsItemSelected

我们可以使用 MenuItem 来路由我们的程序逻辑,方法是将它的 itemId 属性与我们在 main_menu.xml 中定义的四个菜单项进行比较。

override fun onOptionsItemSelected(item: MenuItem?): Boolean {
  if(item?.itemId == R.id.menuFile) {
    showMessage(“File Menu “) // user defined function
    return true
  }

}

Listing 13-9comparing itemId with R.id.menuFile

注意我们如何使用安全调用操作符(?。)测试期间。我们需要使用安全调用,因为 MenuItem 在 onOptionsItemSelected 中被声明为可空,而且该函数应该返回一个布尔值。在我们的例子中,我们返回了 true,,这告诉 Android 运行时我们已经消费了这个事件,并且不需要其他侦听器进一步处理这个事件。我们可以继续使用 if-else 构造来路由程序逻辑,但是在这种情况下, when 构造可能更合适。清单 13-10 展示了如何在处理程序逻辑时使用。你可能还记得第三章的和中提到 Kotlin 没有一个 switch 语句——这个 when 结构相当于 Java 的 switch。

override fun onOptionsItemSelected(item: MenuItem?): Boolean {

  when(item?.itemId) {
    R.id.menuFile -> {
      showMessage("File menu")
      return true
    }
    R.id.menuEdit -> {
      showMessage("Edit menu")
      return true
    }
    R.id.menuHelp -> {
      showMessage("Help menu")
      return true
    }
    R.id.menuExit -> {
      showMessage("Exit")
      return true
    }
  }

Listing 13-10using when to route program logic

清单 13-11 、 13-12 和 13-13 分别显示了 MainActivity、activity_main 和 build.gradle 的完整代码。如果您正在编写代码,您可以使用它作为参考。

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.thelogbox.ch13appbar"
        minSdkVersion 23
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:27.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation 'com.android.support:design:27.1.0'
}

Listing 13-13app/build.gradle

<?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:id="@+id/root_layout"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity">

  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World!"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

Listing 13-12complete code for activity_main.xml

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.view.Menu
import android.view.MenuItem

import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
  }

  override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    menuInflater.inflate(R.menu.main_menu, menu)

    return super.onCreateOptionsMenu(menu)
  }

  override fun onOptionsItemSelected

(item: MenuItem?): Boolean {

    when(item?.itemId) {
      R.id.menuFile -> {
        showMessage("File menu")
        return true
      }
      R.id.menuEdit -> {
        showMessage("Edit menu")
        return true
      }
      R.id.menuHelp -> {
        showMessage("Help menu")
        return true
      }
      R.id.menuExit -> {
        showMessage("Exit")
        return true
      }
    }

    return super.onOptionsItemSelected(item)
  }

  private fun showMessage(msg:String) {
    Snackbar.make(root_layout, msg, Snackbar.LENGTH_LONG).show()

  }
}

Listing 13-11complete code for MainActivity.Kt

章节总结

  • 使用样式和主题可以立刻给你的应用增加活力。升级你的应用是最简单的事情。

  • 动作栏中的菜单可以显示应用最重要的功能。

在下一章中,我们将:

  • 看片段。您可以使用它们来使您的应用适应不同的外形和设备方向(纵向与横向)。

  • 我们还将看看如何让片段相互交流。

十四、片段

我们将介绍的内容:

  • 片段简介

  • 横向和纵向

  • 片段间通信

在 Android 的早期,当它只在手机上运行,没有任何高分辨率的屏幕时,activities 作为一种编写 UI 和与用户交互的方式就足够了。随后出现了平板电脑和高分辨率屏幕,创建可以在手机和平板电脑上良好运行的应用变得越来越困难。开发者面临着艰难的选择。要么选择功能最差的硬件作为目标,并使其成为最小公分母,要么通过根据设备的功能移除和添加 UI 元素来使应用适应一系列外形,这被证明是非常难以手动完成的。API 11(蜂巢)出来的时候,安卓用片段解决了这个问题。

片段简介

片段是一个非常高级的概念,初学者可能会战战兢兢地接近它,但是它背后的基本概念非常简单。如果我们把一个活动看作是 UI 的组成单元,那么就把一个片段看作是一个迷你活动——它是一个更小的组成单元。您通常会在运行时显示(和隐藏)片段,以响应用户的操作(例如,倾斜设备或从纵向切换到横向,从而腾出更多的屏幕空间)。你甚至可以使用片段作为适应设备外形的策略;当应用在较小的屏幕上运行时,您将只显示一些片段。

像活动一样,片段由两部分组成:一个 Java 程序和一个布局文件。想法几乎是一样的——在 XML 文件中定义 UI 元素,然后在程序文件中膨胀 XML 文件,这样 XML 中的所有视图对象都将成为一个对象。之后,我们可以使用 R.class 引用 XML 中的每个视图对象。一旦我们理解了这个概念,就可以将片段想象成一个普通的视图对象,我们可以将它拖放到主布局文件上——当然,片段不是普通的视图,但它们是视图。

为了创建一个片段,我们通常做以下事情:

  1. 创建一个 XML 资源文件,放在 /app/res/layout 文件夹中,就像我们放 activity_main.xml 的地方一样。

  2. 给新的资源文件起一个描述性的名字——比如说,fragment_booktitles.

  3. 创建片段类。我们过去在创建片段时会在两个类之间选择——要么从原生的 android.app.Fragment 继承,要么从Android . support . v4 . app . fragment继承。如果您的目标 SDK 是 API 11 或更高版本,您可以使用前者,而对于任何低于 Android 3 (Honeycomb)的应用,您可以使用后者。你仍然可以使用 android.app.Fragment,但作为一个提醒,你需要知道 Android P(又名 Android 9)已经弃用了原生片段。如果您仍然想使用片段,请使用支持库,这样您就可以在所有 API 级别上获得一致的行为。

  4. 接下来,将片段类与 XML 资源布局联系起来。您可以通过在 Fragment 类的 onCreate 方法中膨胀 XML 资源文件来做到这一点。

  5. 添加新创建的片段。

让我们在 Android Studio 中完成它们。首先,创建一个包含空活动的项目,就像我们已经创建的所有其他项目一样。

现在,创建一个 XML 资源文件,放入 /app/res/layout ,如图 14-1 所示。

使用上下文菜单,右键单击项目工具窗口中的 /app/res/layout 文件夹(图 14-1 )。选择新建布局 资源文件。这个布局资源文件将包含我们片段的所有视图元素。你会看到一个“新资源文件”的对话窗口;输入资源文件的名称—出于练习的目的,我将其命名为“book_titles”

img/463887_1_En_14_Fig1_HTML.jpg

图 14-1

新布局资源文件

您可以放置任何您需要的视图元素。这个片段资源文件与我们之前处理过的任何活动资源文件没有什么不同。您可以将任何内容放入活动资源文件,也可以放入片段资源文件。

接下来,让我们创建片段类。再次使用上下文菜单创建类,如图 14-2 所示。

img/463887_1_En_14_Fig2_HTML.jpg

图 14-2

创建新的 Kotlin 类

如果您在创建新的 Kotlin 类时右键单击了Javanet . working dev . fragments test,那么新创建的类将与您的其余代码属于同一个包。如果您在创建新的 Kotlin 类时右键单击 java 文件夹,该类将在默认包中;当这种情况发生时,您需要自己将 package 语句添加到类中。

您将被要求创建哪种 Kotlin 文件。从下拉菜单中选择类别,如图 14-3 所示。

img/463887_1_En_14_Fig3_HTML.jpg

图 14-3

给 Kotlin 类命名

片段类可以通过膨胀资源文件并从 onCreateView 回调中返回来与 UI 资源文件相关联。清单 14-1 包含 MainActivity 的注释和解释片段;它展示了如何将片段类与 UI 资源文件连接起来。具体来说,项目符号❸是负责将片段类与 UI 资源文件相关联的代码。

| -好的 | 我们使用支持库中的 Fragment 类,因为 Android 9 不赞成使用 **android.app.Fragment** 。即使我们通常以 API 23 为目标,从现在开始最好总是使用受支持的库。 | | ❷ | **onCreateView** 回调类似于活动的 onCreate。但是注意不要在这里引用任何视图元素——它们现在还不可用。如果您试图在这里引用一个 UI 元素(例如,一个按钮或文本字段),它将返回 null。 | | -你好 | 在这个例子中,UI 资源文件的名称是 **book_titles** 。因此,假设您有一个名为**/app/RES/layout/book _ titles . XML**的文件。膨胀 XML 资源文件并返回它,以便 MainActivity 可以在它的末端组成 UI。当你在 **onCreateView** 中时,你不能引用任何 UI 元素的原因是因为你还没有膨胀 XML 资源,所以此时你的 UI 元素都不存在。Android 运行时将**充气器**和**容器**对象传递给 **onCreateView** 方法。我们需要这些对象来扩充 UI 资源。 | | (a) | 当所有 UI 元素就绪时,运行时调用 onViewCreated 方法**。这是您可以开始使用和引用 UI 元素的地方。** |
import android.support.v4.app.Fragment    ❶
...

class BookTitle : Fragment() {

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {            ❷

    val v = inflater.inflate(R.layout.book_titles, container, false) ❸
    return v
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    ❹
  }
}

Listing 14-1BookTitle Fragment

注意

“膨胀”UI 资源文件意味着接受 UI 定义(XML 格式),创建实际的视图和视图组对象,并呈现它们。在展开过程之后,您将能够以编程方式引用视图对象。

最后一步是将片段添加到活动中。您可以通过两种方式向活动添加片段:在运行时或在设计时。现在,我们将在设计时添加片段。

打开 MainActivity 的 UI 资源文件(如果它尚未打开)。在项目工具窗口中,双击/app/RES/layout/activity _ main . XML。在“设计”模式下打开它。在调色板中,到常用,寻找 <片段>,,如图 14-4 。

img/463887_1_En_14_Fig4_HTML.jpg

图 14-4

将片段元素拖到 activity_main 中

<片段> 元素拖放到活动中的任意位置,就像拖放任何视图元素一样。将弹出一个片段对话框;您需要选择想要添加到 activity_main 布局中的片段类。在我们的例子中,只有一个片段类—选择 BookTitle 片段。

就这样,我们现在可以运行我们无趣和无趣的片段样本了。如果你运行它,它看起来像模拟器中的图 14-5 。

img/463887_1_En_14_Fig5_HTML.jpg

图 14-5

片段测试,运行

尽管它很无趣,但它应该让你在片段的基础上打好基础。现在,我们准备好了一些更有趣的东西。在下一节中,我们将创建一个包含两个片段的演示项目。

书名和描述,片段演示

我们想做的是:

  1. 在我们的主活动中使用两个片段。

  2. 其中一个片段包含一个书单;我们将让用户通过单击一个单选按钮来选择一本书。

  3. 另一个片段包含对当前选中的书的描述。

  4. 这些片段将根据用户手持设备的方式(纵向或横向)自行重新排列。

在运行时,当设备垂直放置时,应用看起来如图 14-6 所示。

img/463887_1_En_14_Fig6_HTML.png

图 14-6

图书标题应用,垂直方向(纵向)

当用户在横向模式下手持设备时,它看起来如图 14-7 所示。

img/463887_1_En_14_Fig7_HTML.png

图 14-7

水平方向,横向

我们已经知道了如何创建片段,以及如何将它们添加到活动中,但是为了完成这个演示项目,我们将需要散列更多的细节。

  1. 我们如何使用单选按钮作为选择器,这样当一个按钮被选中时,其他的按钮就会被取消选中?我们将使用一个单选按钮组并收集该组下的所有单选按钮。

  2. 我们将在哪里存储每本书的文本定义?我们将使用一个 XML 文件,然后将其加载到一个数组中。数组的每个元素将包含一本书的定义。

  3. 我们如何同步两个片段之间的信息呢?我们将探索片段间的交流。我们不会让片段直接相互通信(虽然我们可以,但这不是好的做法)。我们将通过活动来管理同步。

  4. 我们如何处理设备方向的变化?我们将在 /app/res 中专门为风景布局创建另一个布局文件夹。它将被命名为/app/RES/layout-land;当设备处于横向时,这是我们放置布局文件的地方。

那我们开始工作吧。我为此演示创建了一个新项目;详情见表 14-1 。

表 14-1

项目详细信息

|

项目详细信息

|

价值

|
| --- | --- |
| 应用名称 | ch14 片段书籍 |
| 公司域 | 使用您的网站名称 |
| Kotlin 支架 | 是 |
| 波形因数 | 仅限手机和平板电脑 |
| 最低 SDK | API 23 棉花糖 |
| 活动类型 | 空的 |
| 活动名称 | 主要活动 |
| 布局名称 | 活动 _ 主要 |

让我们创建 XML 资源文件,它将保存书籍描述的文本。为此,您可以:

img/463887_1_En_14_Fig8_HTML.jpg

图 14-8

创建新的 XML 值文件

  1. 使用上下文菜单,在项目工具窗口中右键单击/ app/res/values ,然后

  2. 点击新建XML值 XML 文件,如图 14-8 所示。

  3. 将其命名为“图书描述”——不要键入。xml 扩展名;Android Studio 会处理好的。

在编辑器中打开 bookdescriptions.xml,将清单 14-2 的内容复制到其中。

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string-array name="bookdescriptions">
    <item>
       How to use Android Studio 3, but also teaches you how basic
Android programming. And hey, in case you're also a beginner in Java, that's covered too.
    </item>
    <item>
      This book is also about how to use Android Studio. Like the first one,
      it also teaches you the basics of the IDE and Android programming; but
      this time around, you'll use Kotlin. The newest kid in the JVM block
    </item>
    <item>
      Minimum Android Programming is the book that got me started. I wrote
      in an age when even the Eclipse ADT doesn't exist yet. So, this means
      you'll use the Android SDK in all the glory of the CLI tools
    </item>
  </string-array>
</resources>

Listing 14-2/app/res/values/bookdescriptions.xml

现在我们可以研究片段了。让我们首先创建 book_titles 片段。创建一个新的布局资源文件,并将其命名为“book_titles”

清单 14-3 显示了/app/RES/layout/book _ titles . XML的内容

| -好的 | 获取单选按钮组视图。 | | ❷ | 添加第一个单选按钮作为 RadioGroup 的子节点。 | | -你好 | 对第二个单选按钮执行相同的操作。 | | (a) | 对第三个无线电按钮进行同样的操作 |
<?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:layout_editor_absoluteY="81dp">

  <RadioGroup                                 ❶
    android:id="@+id/radioGroup"
    android:layout_width="354dp"
    android:layout_height="wrap_content"
    tools:layout_editor_absoluteX="16dp"
    tools:layout_editor_absoluteY="75dp">

    <RadioButton                              ❷
      android:id="@+id/rlas3"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_weight="1"
      android:text="Learn Android Studio 3"
      android:textSize="18sp" />

    <RadioButton                              ❸
      android:id="@+id/rlas3kotlin"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_weight="1"
      android:text="Learn Android Studio 3 with Kotlin"
      android:textSize="18sp" />

    <RadioButton                              ❹
      android:id="@+id/rminandroid"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_weight="1"
      android:text="Minimum Android Programming"
      android:textSize="18sp" />
  </RadioGroup>
</android.support.constraint.ConstraintLayout>

Listing 14-3/app/res/layout/book_titles.xml

接下来,让我们为 book_titles UI 创建片段类。使用上下文菜单,右键点击/app/Java/net . working dev . ch 14 fragment books,然后选择新建Kotlin 文件/类。创建一个类,并将其命名为 BookTitle。在本课程中,我们需要做以下工作:

  1. 它是一个片段,所以它需要继承片段类。

  2. 覆盖 onCreateView 回调,膨胀 UI 资源文件,并返回。

  3. 处理单选按钮的单击事件。有几种方法可以做到这一点。一种方法是为 radioGroup 设置一个监听器,另一种方法是为每个单选按钮设置一个点击监听器;我们选择后者。

清单 14-4 中显示了带注释(和解释)的 BookTitle 类。

| -好的 | 我们从支持库中扩展了片段类。我们还实现了**视图。OnClickListener** 接口。我们将使用该类作为三个单选按钮的 **onClick** 监听器对象。 | | ❷ | 运行时调用 **onCreateView** 方法来组成片段的 UI。此时,该片段的 UI 元素都是不可访问的。您不能在这里进行任何 UI 更改或初始化。 | | -你好 | 这将向运行时返回一个视图对象。我们正在膨胀 UI 资源文件。**膨胀**方法有三个参数:1. **UI 资源文件**。片段的 XML 布局,我们将使用 R.layout.book_titles。2.**这是片段的潜在父级,或者根**。为此,我们将只使用**容器**。3.**附着根**。这是一个布尔值。这个值将决定膨胀视图是否应该附加到根参数?如果为 false,root 仅用于为 XML 中的根视图创建 LayoutParams 的正确子类。 | | (a) | 我们说单选按钮的监听器对象是 BookTitle 类的实例, *this* 类。 | | (一) | **onClick** 回调来自**视图。OnClickListener** 接口。当单击其中一个单选按钮时,运行时将调用此方法并传递被单击的实际视图对象。这是我们规划程序逻辑的地方。我们将知道哪个单选按钮实际上被点击了。 | | ❻ | **when** 构造非常适合路由程序逻辑。我们在这里测试 **View.id** 的运行时值; *R.id.rlas3* 、 *R.id.rlas3kotlin* 和 *R.id.rminandroid* 是 book_title.xml 中单选按钮声明的*id*。 | | ❼ | 我们给 **rlas3** 赋值为零,因为 rlas3 的描述在图书描述数组的*0*th元素中找到(我们还没有创建这个数组)。同样,rlas3kotlin 的定义是图书描述数组的第 1 个 st 元素,而 rminandroid 的定义是第 2 个 nd 元素。 |
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

class BookTitle : Fragment(), View.OnClickListener {  ❶

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {    ❷
    val v = inflater.inflate(R.layout.book_titles, container, false) ❸
    return v
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    rlas3.setOnClickListener(this)          ❹
    rlas3kotlin.setOnClickListener(this)
    rminandroid.setOnClickListener(this)
  }

  override fun onClick(v: View?) {  ❺
    var index:Int = 0
    when(v?.id) {
      R.id.rlas3 -> {               ❻
        index = 0                   ❼
      }
      R.id.rlas3kotlin -> {
        index = 1
      }
      R.id.rminandroid -> {
        index = 2
      }
    }
  }
}

Listing 14-4BookTitle Fragment Class

既然已经完成了 book_titles 片段的两个组件,我们就可以处理 book_description 片段了。你已经知道如何创建一个片段,所以我将跳过指令,直接跳到代码。

创建一个新的 UI 资源,命名为 book_description ,确保它在 /app/res/layout 文件夹中。作为片段类的,将其命名为图书描述

清单 14-5 和 14-6 分别显示了 book_description.xml 和 BookDescription 类的代码。

book_description 片段很简单。它只有一个 TextView 元素。注意,我们没有对这个片段使用 ConstraintLayout 我们可以使用,但是使用 LinearLayout 要简单得多。我们希望文本视图的宽度占据整个屏幕的宽度。如果您尝试按照练习进行,您可以简单地复制清单 14-5 并覆盖 book_description.xml 的内容。

| -好的 | 该语句读取文件**/app/RES/values/book descriptions . XML**,并从中创建一个数组。 | | ❷ | 我们创建了一个小函数,它将负责修改描述文本视图中的文本。它接受一个 **Int** 值,我们将用它作为描述的选择器。数组的每个元素包含一本书的描述。 | | -你好 | **arrbookdesc[bookindex]** 从数组中获取一个描述,然后将 TextView 的**文本**属性设置给它。 |
class BookDescription : Fragment() {

  lateinit var arrbookdesc: Array<String>
  var bookindex = 0

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

    val v = inflater.inflate(R.layout.book_description, container, false)
    arrbookdesc = resources.getStringArray(R.array.bookdescriptions) ❶

    return v
  }

  fun changeDescription(index:Int) : Unit { ❷
    bookindex = index
    txtdescription?.setText(arrbookdesc[bookindex]) ❸
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    changeDescription(bookindex)
  }
}

Listing 14-6
BookDescription class

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android=http://schemas.android.com/apk/res/android
  xmlns:tools=http://schemas.android.com/tools
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical">

  <TextView
    android:id="@+id/txtdescription"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="TextView"
    android:textSize="24sp" />
</LinearLayout>

Listing 14-5/app/res/layout/book_description.xml

现在这两个片段已经构建好了,我们可以把注意力放在主活动上了。它需要做三件事:

  1. 把两个片段抱在一起

  2. 充当每个片段的信使。当用户在 book_titles 片段中选择一本书时,我们需要在 bookdescriptions 数组中查找该书的描述,并相应地修改 book_description 片段中的文本描述;和

  3. 根据设备的方向调整两个片段的排列。如果设备垂直定向,两个片段将从上到下堆叠排列。当设备水平放置时,堆叠将从左到右进行。

让我们先努力实现第三个目标。现在,我们只有一个布局文件夹, /app/res/layout 文件夹是 Android 寻找布局资源的默认位置。这就是为什么我们总是把我们的 activity_main.xml 放在这个文件夹中的原因。有一个约定,如果我们创建一个名为 /app/res/layout-land 的文件夹,当设备处于风景模式时,Android 会在这个文件夹中查找布局文件。我们将利用这一惯例来实现我们的目标。

还有,我们需要解决从上到下和从左到右的堆叠顺序。最简单的方法是将 activity_main 的布局从 ConstraintLayout 改为 LinearLayout。想法是为/app/res/layout 和/app/res/layout-land 提供相同的 activity_main xml 文件,但是我们将更改 LinearLayout 方向,以便在默认布局文件夹中,方向是垂直的(默认),而在 layout-land 文件夹中,方向是水平的。我们还会做一些改动,但是我们会在一段时间内完成。

要将 activity_main 的布局转换为 LinearLayout,请执行以下操作:

  1. 在设计视图中打开 activity_main.xml。

  2. In the “Component Tree” tool window, right-click on “ConstraintLayout, as shown in Figure 14-9.

    img/463887_1_En_14_Fig9_HTML.jpg

    图 14-9

    将 activity_main 转换为 LinearLayout

  3. 选择转换视图

  4. A dialog box will appear; choose LinearLayout, as shown in Figure 14-10.

    img/463887_1_En_14_Fig10_HTML.jpg

    图 14-10

    转换为线性布局

清单 14-7 显示了修改后的 activity_main 的代码(转换为 LinearLayout 之后)。

| -好的 | layout _ width:“match _ parent”表示布局将跨越整个屏幕宽度。 | | ❷ | \这意味着布局将跨越屏幕的整个高度。 | | -你好 | 方向:“垂直”意味着我们在布局中放置的任何视图都将从上到下排列。 |
<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"       ❸
  tools:context=".MainActivity">

</LinearLayout>

Listing 14-7Code of activity_main.xml

接下来,将这两个片段添加到 activity_main 中。在设计模式下打开 activity_main,进入调色板常用,然后找到 <片段>,如图 14-11 所示。首先添加 BookTitle 片段。重复该过程并添加图书描述。

img/463887_1_En_14_Fig11_HTML.jpg

图 14-11

将片段元素拖到 activity_main 中

清单 14-8 显示了添加了两个片段的 activity_main.xml。

| -好的 | 我们希望顶部片段横跨整个宽度。 | | ❷ | 高度设置为 0px 即可。我们将让运行时为我们决定高度。反正我们用的是布局权重。 | | -你好 | 假设权重为“1”你在这里用什么数并不重要,只要另一个片段有相同的重量。 | | (a) | 我们还希望底部的片段横跨整个宽度。 | | (一) | 我们让运行时决定高度;把这个也设置成 0px。 | | ❻ | 我们希望顶部和底部的片段有相等的高度。因此,我们也将这里的权重设置为“1”。 |
<?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"
  android:orientation="vertical"
  tools:context=".MainActivity">

  <fragment
    android:id="@+id/fragmentbooktitle"
    android:name="net.workingdev.ch14fragmentsbooks.BookTitle"
    android:layout_width="match_parent"   ❶
    android:layout_height="0px"           ❷
    android:layout_weight="1" />          ❸

  <fragment
    android:id="@+id/fragmentbookdescription"
    android:name="net.workingdev.ch14fragmentsbooks.BookDescription"
    android:layout_width="match_parent"   ❹
    android:layout_height="0px"           ❺
    android:layout_weight="1" />          ❻

</LinearLayout>

Listing 14-8activity_main With book_titles and book_description Fragments

这就是默认的纵向方向。现在,让我们在横向方向上工作。为了在设备水平放置时控制应用的外观和行为,我们需要做四件事。它们概述如下:

  1. 创建文件夹 /app/res/layout-land

  2. layout-land 中创建另一个 UI 资源文件;我们也将命名为 activity_main。

  3. 将/app/res/layout/activity_main 的内容复制到/app/RES/layout-land/activity _ main。

  4. 在/app/RES/layout-land/activity _ main 中进行必要的方向更改。

首先,您需要切换项目工具窗口的视图。现在我们正在使用“Android 视图”,我们需要转到“项目视图”转到项目工具窗口的上部区域,点击向下箭头(如图 14-12 所示),然后选择“项目”

img/463887_1_En_14_Fig12_HTML.jpg

图 14-12

从 Android 视图更改为项目视图

创建文件夹布局-在/app/res 文件夹中着陆。右键点击/app/res 文件夹,然后选择新建安卓资源目录。将新目录命名为“layout-land”,如图 14-13 所示。

img/463887_1_En_14_Fig13_HTML.jpg

图 14-13

新资源目录

右键单击新创建的 layout-land 文件夹,然后选择新建布局资源文件

将文件命名为“activity_main”,并为“根元素”选择 LinearLayout,如图 14-14 所示。

img/463887_1_En_14_Fig14_HTML.jpg

图 14-14

新布局资源文件

将/app/RES/layout/activity_main . XML 的内容复制到 layout-land 中这个新创建的 activity _ main 中,并进行适当的修改,如清单 14-9 所示。

| -好的 | 我们处于横向模式,所以这需要是“水平的”。使用此设置,片段将从左到右排列,而不是从上到下。 | | ❷ | 在纵向模式下,layout_width 设置为“match_parent”,layout_height 设置为“0px”。我们将在横向模式下反转这些设置。所以把 layout_width 设置为“0px”。 | | -你好 | 将 layout_height 设置为“match_parent”。 | | (a) | 和往常一样,我们希望这两个片段具有相等的权重,所以在这里使用“1”。确保另一个片段中的 layout_weight 也是“1” |
<?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"
  android:orientation="horizontal"   ❶

  tools:context=".MainActivity">

  <fragment
    android:id="@+id/fragmentbooktitle"
    android:name="net.workingdev.ch14fragmentsbooks.BookTitle"
    android:layout_width="0px"             ❷
    android:layout_height="match_parent"   ❸
    android:layout_weight="1" />           ❹

  <fragment
    android:id="@+id/fragmentbookdescription"
    android:name="net.workingdev.ch14fragmentsbooks.BookDescription"
    android:layout_width="0px"
    android:layout_height="match_parent"
    android:layout_weight="1" />

</LinearLayout>

Listing 14-9/app/res/layout-land/activity_main.xml

这个项目的最后一部分是同步这两个片段。图 14-15 提醒我们我们的小项目应该做什么。

img/463887_1_En_14_Fig15_HTML.jpg

图 14-15

同步片段

当用户单击 book_titles 片段中的一个单选按钮时,book_description 片段应该会改变并显示当前所选书籍的描述。前面我们在 BookDescription 类中写了 changeDescription 函数;我们可以简单地从 BookTitle 类中调用这个函数,但这不是好的做法。为什么?因为如果我们这样做了,BookTitle 类将会知道很多关于 BookDescription 类的信息——这使得前者依赖于后者。开发人员称之为“紧耦合”,大多数时候应该避免这种情况。

如果我们不直接从 BookTitle 调用 changeDescription,我们要怎么做呢?图 14-16 显示了我们的表演。

img/463887_1_En_14_Fig16_HTML.png

图 14-16

片段之间的通信

这个想法是通过主要活动来引导行动。在序列图中,BookTitle 调用活动中的 onBookChanged 函数,然后活动调用 BookDescription 中的 changeDescription 函数。敏锐的读者可能会注意到,我们只是简单地将依赖性从图书描述转移到主活动,这将使图书标题依赖于(并紧密耦合于)主活动。如果我们把主要活动和书名特别联系起来,你可能是对的。我们不会。我们将使用一个接口来代替;这种方法给了我们某种程度的间接性。它不再是紧密耦合的——至少,没有那么紧密。这是我们要做的。

  1. 创建一个协调器接口——让我们把它命名为协调器,为什么不呢?

  2. 在 MainActivity 中实现协调器接口。

  3. 使用 BookTitle 中的协调员类型。当我们需要调用 BookTitle 中的 coordinator 方法时,我们将针对 Coordinator 类型进行调用,而不是针对 MainActivity。

要创建一个接口,在项目工具窗口中右击你的项目包(如图 14-17 ),然后点击新建Kotlin 文件/类

img/463887_1_En_14_Fig17_HTML.jpg

图 14-17

创建新的 Kotlin 文件/类

如图 14-18 所示,将其命名为“协调器”,然后将“种类”改为“接口”

img/463887_1_En_14_Fig18_HTML.jpg

图 14-18

新界面

清单 14-10 显示了协调器接口的代码。

| -好的 | 声明一个接口。 | | ❷ | 声明一个抽象方法。它接受一个 Int 参数。此参数代表 bookdescriptions 数组中的元素编号。无论我们在这里收到什么值,我们都将使用它来调用 BookDescription 片段中的 **changeDescription** 方法。顺便说一下,我们不必显式地将这个方法声明为*公共*和*抽象*——这是接口中所有方法的默认设置。 |
interface Coordinator {         ❶
  fun onBookChanged(index:Int)  ❷
}

Listing 14-10Coordinator.Kt

接下来,让我们在 MainActivity 中实现这个接口。清单 14-11 显示了带注释的代码。

| -好的 | 让我们实现协调器接口。 | | ❷ | 覆盖 **onBookChanged** 方法。这在协调器接口中被声明为抽象的;我们必须在 MainActivity 中覆盖它,这样我们才能提供具体的行为。 | | -你好 | 我们来获取一个对 BookDescription 片段的引用; **fragmentbookdescription** 是片段的 **id** 。这个调用返回一个**片段**类;*还没有*图书描述类。如果你以前使用 Java 处理过片段,你可能记得我们需要使用 **findFragmentById** 来做这种事情。我们不必再这样做了。Kotlin Android 扩展让我们可以通过 id 直接引用片段——它已经在 MainActivity 中合成了。 | | (a) | 我们将**片段**(仍然是一个片段类)转换为**图书描述**。Kotlin 中的 **is** 操作器足够聪明,可以为我们自动执行转换。我们不必再执行显式强制转换。这是 Java 和 Kotlin 的又一个区别;在前一种情况下,你必须显式强制转换。在 Kotlin 中, **is** 操作符不仅相当于的**实例,它还为我们执行智能强制转换。** | | (一) | 现在,我们可以调用 BookDescription 类的 **changeDescription** 方法。 |
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity(), Coordinator { ❶

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
  }

  override fun onBookChanged(index:Int) {   ❷
    val frag = fragmentbookdescription      ❸
    if (frag is BookDescription) {          ❹
      frag.changeDescription(index)         ❺
    }
  }
}

Listing 14-11MainActivity, Annotated

剩下要做的是在 BookTitle 类中进行更改。当单击单选按钮时,我们将执行以下操作:

  1. 找出哪个按钮被点击了。

  2. 根据单击时 radiobutton 的值,我们将为一个索引变量赋值;0—“学习 Android Studio 3”;1——跟 Kotlin 学习《Android Studio 3》;以及2–“最少安卓编程。整数 0、1 和 2 对应于 bookdescriptions.xml 的三个数组元素。

  3. 使用协调器类型获取对 MainActivity 的引用;然后

  4. 调用 onBookChanged 方法。

清单 14-12 展示了所有这些在代码中的样子。

| -好的 | 让我们找出哪个按钮被点击了。 | | ❷ | 如果是“学习 Android Studio 3”的按钮,我们会将**索引**的值设置为 0,并相应地为 *rlas3kotlin* 和*terminandroid 设置**索引**的值。***when**构造实质上是将 radiobutton 的运行时值转换成一个 Int,我们可以用它作为数组的索引。 | | -你好 | 让我们获取一个对当前正在运行的活动的引用,它是 **MainActivity** 。注意 **getActivity()** 不返回 MainActivity 的具体实例;它只是返回 main activity(fragmentation activity)的超类型。 | | (a) | 让我们将**活动**转换为**协调者**类型。 | | (一) | 最后,调用 **onBookChanged** 方法。 |
class BookTitle : Fragment(), View.OnClickListener {

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    val v = inflater.inflate(R.layout.book_titles, container, false)
    return v
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    rlas3.setOnClickListener(this)
    rlas3kotlin.setOnClickListener(this)
    rminandroid.setOnClickListener(this)
  }

  override fun onClick(v: View?) {
    var index:Int = 0
    when(v?.id) {                 ❶
      R.id.rlas3 -> {             ❷
        index = 0
      }
      R.id.rlas3kotlin -> {
        index = 1
      }
      R.id.rminandroid -> {
        index = 2
      }
    }

    val activity = getActivity()    ❸
    if (activity is Coordinator) {  ❹
      activity.onBookChanged(index) ❺
    }
  }
}

Listing 14-12BookTitle, Annotated

我们已经把所有的点联系起来了。现在我们可以:

  • 在一个片段中使用单选按钮显示书籍;

  • 在另一个片段中显示当前所选书籍的描述;和

  • 根据设备方向的变化调整片段的布局。

尝试在模拟器中运行应用。点按几个按钮,然后尝试将方向从纵向更改为横向。尝试在从纵向模式到横向模式之间切换单选按钮。如果您想从横向切换到纵向,请使用模拟器上的旋转按钮(如图 14-19 所示),反之亦然。

img/463887_1_En_14_Fig19_HTML.png

图 14-19

设备旋转按钮,模拟器

您可能已经注意到,当您更改设备方向时,这两个片段不同步。 book_description 片段总是回到“学习 Android Studio 3”的描述(bookdescription 数组上的第一个元素)。

只要您不改变设备的方向,这两个片段就会保持同步。当你改变方向时,片段会发生一些变化。

随着设备方向的改变,MainActivity 及其片段会发生一些变化。记住一个活动有一个生命周期?片段也有生命周期——它们与活动的生命周期相似,但也有显著的不同。我们不会进入片段生命周期的细节,也不会讨论活动生命周期如何影响片段的生命周期。我只想指出,当你改变装置的方向时,这个活动,连同片段,将会被拆除并重新构建。活动可能会进入以下状态并在其中转换(回调):

  1. activity . onSaveInstanceState .将调用片段的 onSaveInstanceState。

  2. Activity.onPause 。将调用片段 onPause。

  3. 活动停止。Fragment 的 onStop 也将被调用。

  4. Activity.onCreate 。片段的 oncreate➤oncreate 视图➤ onViewCreated 将被调用。

  5. Activity.onStart 。将调用片段 onStart。

  6. activity . onrestoreinstancestate

  7. Activity.onResume 。将调用片段的 onRestoreInstance。

这里需要注意的是,当你改变方向时,片段会失去它们当前的状态。我们需要找到一种方法来保存数组索引的值(在 BookDescription 类中),在它被拆除并重新构建之前。幸运的是,我们知道运行时会调用活动的 onSaveInstanceState ,推而广之,也会调用片段的onSaveInstanceState;这个方法让我们将值保存在一个包中,所以当设备旋转时,我们将使用它来保存数组索引的值。清单 14-13 显示了 BookDescription 类的完整和带注释的代码。

| -好的 | 我们需要检查“bookindex”键是否不为空。我们第一次启动应用时,它将为空,因为应用还没有调用 **onSaveInstanceState** 。如果是 null,我们就默认**book index = 0**;我们使用数组中的第一个描述。 | | ❷ | 如果它不为空,我们已经在“bookindex”键中保存了一个值;因此,获取“bookindex”的值,并将 **bookindex** 变量的值设置为该值。 | | -你好 | 就在活动和片段被拆分和重建之前,运行时调用 **onSaveInstanceState** 。这个方法让我们可以访问一个 Bundle 对象;这是我们在 **onCreateView** 回调期间得到的同一个 Bundle 对象。使用键“bookindex”将 **bookindex** 的当前值保存到包中。 |
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.book_description.*

class BookDescription : Fragment() {

  lateinit var arrbookdesc: Array<String>
  var bookindex = 0

  override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?): View? {

    val v = inflater.inflate(R.layout.book_description, container, false)
    arrbookdesc = resources.getStringArray(R.array.bookdescriptions)

    bookindex = if(savedInstanceState?.getInt("bookindex") == null) 0   ❶
    else { savedInstanceState.getInt("bookindex")}                      ❷

    return v
  }

  override fun onSaveInstanceState(outState: Bundle) {                  ❸
    outState.putInt("bookindex", bookindex)
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    changeDescription(bookindex)
  }

  fun changeDescription(index:Int) : Unit {
    bookindex = index
    println("BOOK INDEX = $bookindex")
    txtdescription?.setText(arrbookdesc[bookindex])
    println(arrbookdesc[bookindex])
  }
}

Listing 14-13Complete Code for BookDescription, Annotated

片段演示,动态

既然我们知道了如何在设计时处理片段,让我们看看如何动态地处理片段。为了动态地添加片段,我们通常必须做以下事情:

  1. 为片段创建布局资源和相应的 Kotlin 类;就像我们在之前的项目中所做的一样。

  2. 在 MainActivity 中,我们创建了 fragment 类的一个实例。

  3. 创建 FragmentManager 和 FragmentTransaction 对象的实例。

  4. 为活动布局文件中的片段创建占位符。占位符是我们稍后放置片段的地方。

  5. 使用 FragmentTransaction 对象,将片段添加到活动中。

这个项目和上一个几乎一样。唯一的区别是我们添加片段的方式。我认为最好为此创建一个新项目,这样可以保持之前的项目不变,以备将来参考。

用以下细节创建一个新项目(表 14-2 )。

表 14-2

项目详细信息

|

项目详细信息

|

价值

|
| --- | --- |
| 应用名称 | CH14FragmentsBooksDynamic |
| 公司域 | 使用您的网站名称 |
| Kotlin 支架 | 是 |
| 波形因数 | 仅限手机和平板电脑 |
| 最低 SDK | API 23 棉花糖 |
| 活动类型 | 空的 |
| 活动名称 | 主要活动 |
| 布局名称 | 活动 _ 主要 |

在大多数情况下,您只需复制并粘贴上一个项目中的文件。我建议你不要复制整个项目文件夹。创建一个新项目,并在以前的项目中重新创建您的步骤;使用与前一个项目完全相同的文件名创建相同的类、接口、xml 资源和 UI 资源。然后,将前一个项目中的文件内容复制到新项目的相应文件中。

完成后,表 14-3 显示了当前项目中哪个文件保持不变,哪个文件将发生变化。

表 14-3

新项目中的变更摘要

|

文件

|

描述

|
| --- | --- |
| MainActivity.Kt 公司 | 变化=是。我们需要添加 FragmentManager 和 FragmentTransaction 代码。 |
| activity_main.xml | 变化=是。我们将移除元素并用一个占位符替换它。 |
| book_description.xml | 更改=否。保持不变。你可以复制粘贴然后不管它。 |
| 图书描述。滨鹬 | 更改=否。复制、粘贴,然后不去管它。 |
| book_titles.xml | 更改=否。原样复制。 |
| 书名.Kt | 更改=否。原样复制。 |
| bookdescriptions.xml | 更改=否。原样复制。 |
| 协调员。滨鹬 | 更改=否。原样复制。 |

正如您所看到的,这些更改都包含在主活动文件中。清单 14-14 显示了完整的代码,并注释了 activity_main.xml 中的变化。

| -好的 | 我们添加了一个 LinearLayout 容器;和 | | ❷ | 我们将第一个容器命名为 **fragtop** 。这是 BookTitles 片段的占位符。 | | -你好 | 我们添加了另一个 LinearLayout 容器;和 | | (a) | 将这个命名为 **fragbottom** 。这是 BookDescription 片段的占位符。 |
<?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"
  android:orientation="vertical"
  tools:context=".MainActivity">

  <LinearLayout                             ❶
    android:id="@+id/fragtop"               ❷
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_weight="1"
    android:orientation="horizontal">

  </LinearLayout>

  <LinearLayout                             ❸
    android:id="@+id/fragbottom"            ❹
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_weight="1"
    android:orientation="horizontal">

  </LinearLayout>
</LinearLayout>

Listing 14-14/app/res/layout/activity_main.xml

您会注意到 activity_main 不再包含 <片段> 元素。相反,我们放置了两个 LinearLayout 容器作为片段的占位符。当我们调用将片段添加到我们的活动中时,我们将把它们放在这些占位符中。这就是 UI 资源布局的变化程度。大部分的变化实际上将发生在主活动上。

在之前的项目中,我们静态地将片段添加到活动中,就片段而言,我们做得不多;但是现在我们将动态添加片段,我们将需要添加必要的代码来在运行时添加片段。

要动态地处理片段,您需要两个对象:FragmentManager 和 FragmentTransaction。您可以使用 FragmentManager 做很多事情,比如通过 Id 和标签查找片段;但是出于我们的目的,我们将只使用它来获取 FragmentTransaction 对象。

FragmentTransaction 负责在运行时添加、附加、分离和删除片段。出于我们的目的,我们将只使用它来添加片段。

清单 14-15 中显示了 MainActivity 的完整代码。

| -好的 | 创建一个 **BookTitle** 片段的实例。 | | ❷ | 创建一个 **BookDescription** 片段的实例。 | | -你好 | 让我们获取一个 FragmentTransaction 对象。 **supportFragmentManager** 是 Android Studio 和 Kotlin 的一项便利功能。实际的调用是**getSupportFragmentManager()**,但是它已经为我们合成了,所以我们不必使用实际的方法。接下来, **beginTransaction()** 调用是一个工厂方法,它给了我们一个 FragmentTransaction 对象。 | | (a) | 让我们使用 FragmentTransaction 来添加一个片段。 **add** 方法有两个参数:1.视图对象的 id。这是我们在 activity_main.xml (fragtop)中添加的 LinearLayout 占位符的 id。2.片段的实例(fragBookTitle) | | (一) | 同样的,我们再加上图书描述片段。 | | ❻ | 我们必须调用 FragmentTransaction 的 **commit()** 方法来完成 FragmentTransaction 中的所有更改。如果不调用这个方法,什么都不会发生——不会添加片段。 | | ❼ | 您还记得这个方法,当用户单击 **BookTitle** 片段中的一个单选按钮时,该片段将调用 MainActivity 中的 **onBookChanged()** 方法。 | | ❽ | 在之前的项目中,我们必须找到 book_description 片段的 id,然后在调用 **changeDescription** 之前将其转换为 BookDescription 对象。我们不必再这样做了,因为我们可以直接引用 **BookDescription** 片段的实例。 |
import android.support.v7.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity(), Coordinator {

  lateinit var fragBookDescription: BookDescription
  lateinit var fragBookTitle: BookTitle

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    fragBookTitle = BookTitle()                 ❶
    fragBookDescription = BookDescription()     ❷
    val fragTransaction = supportFragmentManager.beginTransaction() ❸
    fragTransaction.add(R.id.fragtop, fragBookTitle)  ❹
    fragTransaction.add(R.id.fragbottom, fragBookDescription) ❺
    fragTransaction.commit() ❻

  }

  override fun onBookChanged(index:Int) {         ❼
    fragBookDescription.changeDescription(index)  ❽
  }
}

Listing 14-15MainActivity, Annotated

练习和本章到此结束。我们仅仅触及了片段的表面——它们比这里呈现的要多得多;但是希望,当你进一步探索它们的时候,这会给你一个好的基础。

章节总结

  • 像活动一样,片段可以包含视图元素。他们也是一个组成单位,但更小。

  • 您可以使用片段来响应不同的设备方向、外形或大小。

  • 片段和活动一样,也有生命周期回调。

  • 活动的生命周期对片段有影响。

  • 当您更改设备的方向时,活动(和片段)会被拆除并重新构建。他们经历了一系列的生命周期回调。

  • Android P 弃用 android.app.Fragments 。所以,如果你想使用片段,使用支持库中的类。

在下一章中,我们将了解 Android 所谓的“jank”以及如何在代码中避免它。

十五、在后台运行

我们将介绍的内容:

  • 用户界面线程

  • 线程和可运行程序

  • 处理程序和消息

  • 异步任务

  • 安科的 doasync

没有人愿意使用缓慢的应用。用户希望他们的应用简洁明快。每个开发人员都希望这样——没有人开始构建他们的应用时会说,“这个应用太快了,也许我应该让它慢一点”;没人会这么做。那么,为什么会有像糖蜜一样移动的应用呢?你可能见过我说的这些应用中的一些——你知道你试图滚动一个回收器视图或列表,然后它开始,停止,并发出劈啪声。呆滞。

我们可以列出一些应用运行缓慢的原因,但我敢打赌 10 大原因之一是主线程上运行的太多了。它可能被一个 I/O 例程或一个复杂的计算所拖累,或者两者兼而有之,这很糟糕。

这是否意味着你不应该在你的应用中进行任何 I/O 调用或任何复杂的计算?一点也不。但是你应该知道把 I/O 调用或者复杂的计算放在哪里;而且不在主线程上。

在这一章中,我们将看看如何让运行缓慢的代码远离主线程,从而让应用能够快速而敏捷地做出响应。

基本概念

应用启动时会创建一个流程。它被分配了一些资源,比如内存和其他一些它需要的东西,这样它才能完成它的工作。它至少有一个线程。

不严格地说,线程是一系列指令。它是真正执行你的代码的东西。在应用运行期间,线程将利用进程的资源。它可以读取或写入数据到内存、磁盘,有时甚至是网络 I/O。当线程与所有这些交互时,它实际上只是在等待。它不能在等待时利用 CPU 周期。我们不能让这些 CPU 周期白白浪费掉。可以吗?我们可以做的是创建其他线程,这样当一个或多个线程在等待时,其他线程可以利用 CPU。多线程应用就是这种情况。

当运行时创建应用的实例时,该进程被赋予一个线程。它被称为主线程。一些开发者称之为 UI 线程。运行时只给了我们一个线程,仅此而已。但好消息是我们可以创造更多。UI 线程被允许产生其他线程。

用户界面线程

在我们深入到生成或创建子线程的细节之前,让我们先讨论一下 UI 线程。它负责启动主活动和扩展布局 xml,以便其中的所有视图元素都变成实际的 Java 对象(例如,按钮、文本视图等)。).简而言之,它是负责 UI 的人。

当你发出类似 setText 或者 setHint 的调用时,会在主线程上完成;如果你认为这些调用会立即执行,那就错了。无论你在应用中写什么,一般都会遵循以下步骤:

  1. 这些语句将被放在一个 MessageQueue 中,并一直放在那里,直到

  2. 一个处理程序把它捡起来执行;最后

  3. 它在主线程上执行。

你可能会说,“知道这些很好,但那又怎么样呢?”。嗯,你应该关心这个,因为主线程不仅仅用于绘制 UI 元素。它也用于应用中发生的所有其他事情。记住,活动还有其他方法,如 onCreateonStoponResumeonCreateOptionsMenuonOptionsItemSelected ,以及其他类似的方法;每当代码在这些块上运行时,Android 运行时无法处理队列中的任何消息。处于阻塞状态;阻塞状态是一个并发术语,开发人员用它来表示应用正在等待某件事情完成,然后才能继续处理它的业务。不要在意行话——只要记住屏蔽可能对用户体验不利。

怎么会这样?答案是“因为我们只有一个线程来做所有这些事情。”这个问题的解决方案是创建一个后台线程或子线程,并在其中执行我们的非 UI 任务——但并不总是这样。如果您认为这个调用在处理资源方面足够便宜,比方说 1 ms 到 15 ms,那么就在主线程上进行吧。如果需要 16 毫秒以上的时间,你应该在后台线程上完成。

16 毫秒阈值是 Android 4.1 (Jellybean)发布时“黄油计划”中的一个指导原则。它旨在提高 Android 应用的性能。当运行时感觉到你在主线程上做了太多的事情,它会开始丢帧。当你不打昂贵的电话时,应用以平滑的 60 FPS(每秒帧数)运行。如果你绑定了主线程,你会开始注意到缓慢的性能,或者 Android 团队所说的“jank”。我没有一个明确的指导方针可以告诉你什么是昂贵的电话,什么是便宜的电话。不过,我能做的是,向你们展示这两个调用的例子;希望你能了解什么是昂贵的电话和便宜的电话。

清单 15-1 是一个廉价的调用,即使它将文本属性设置为一个计算值。计算很简单,UI 线程不会出一点汗。

button.setOnClickListener {
  txtsecondnumber.setText((2 * 2 * 2).toString())
}

Listing 15-1Set Text Attribute to a Calculated Value: A Cheap Call

清单 15-2 可能看起来很复杂,因为它计算 GCF。如果数字很大怎么办——这对主线程来说不是太繁重了吗?不完全是。清单 15-2 使用欧几里德算法寻找 GCF。该算法以常数时间或 O(1)执行;这是开发人员在谈论算法的时间复杂度或代码完成需要多长时间时使用的另一种行话。O(1)或恒定时间意味着无论输入是大还是小,算法都将执行相同的操作;无论我们找到的 GCF 是 12 和 15 还是 16,848,662 和 24,时间复杂度都没有太大变化。所以,把这个放在主线程里还是挺好的。

注意

算法的时间复杂度可以表示为 O(1)、O(N)、O(N 2 )、O(2 N )或 O(log N),其中 N 代表输入的大小。这是一个叫做大 O 的符号。了解它是有好处的——特别是如果你想写性能代码的话。

button.setOnClickListener {

  val numfno = txtfirstnumber.text.toString().toInt()
  val numsno = txtsecondnumber.text.toString().toInt()

  var numbig = if(numfno > numsno) numfno else numsno
  var numsmall = if(numfno < numsno) numfno else numsno

  var rem = numbig % numsmall

  while(rem != 0) {
    numbig = numsmall
    numsmall = rem
    rem = numbig % numsmall
  }
  Toast.makeText(this@MainActivity, "GCF is $numsmall", Toast.LENGTH_LONG).show()
}

Listing 15-2Calculate GCF: Still a Cheap Call

清单 15-3 被认为是昂贵的,因为它调用网络 I/O。事实上,代码甚至根本不会编译,因为它将导致一个NetworkOnMainThreadException。IDE 甚至不让我们完成编译过程。根据经验,如果您的代码将进行 I/O 调用,无论是本地文件还是网络,您都应该在后台线程中进行。

button.setOnClickListener {
  val url = "https://api.github.com/users/tedhagos"
  println("inside doGetHttp")
  val client = OkHttpClient()
  val request = Request.Builder().url(url).build()
  val response = client.newCall(request).execute()

  val bodystr = response.body().string()
}

Listing 15-3Read Something from GitHub: Expensive Call

清单 15-4 不做任何 I/O,但是函数 killSomeTime 模拟一个昂贵的调用。

button.setOnClickListener {
    killSomeTime()
  }
}

private fun killSomeTime() {
  for (i in 1..20) {
    textView.text = i.toString()
    println("i:$i")
    Thread.sleep(2000)
  }
}

Listing 15-4Do Something That Blocks: Expensive Call

清单 15-4 中的 Thread.sleep 调用完全暴露了代码将会阻塞的情况,但是它可以模拟一些需要 2 秒钟才能完成的事情。乍一看,你可能认为文本视图会每 2 秒更新一次来显示 i 的当前值,但这不会发生,因为运行时已经降低了帧速率。UI 线程无法更新 textView,因为它正在等待线程唤醒和恢复。

想象一下,如果您有一个类似于清单 15-5 的代码—它没有任何 I/O 调用或 Thread.sleep ,但是它不会像您所期望的那样更新文本字段(在循环的第二层)—同样,因为主线程正忙于计算笛卡尔积。

button.setOnClickListener {
  for (i in 1..100000) {
    for (j in 1..10000) {
      txtfirstnumber.setText((i*j).toString())
      for (k in 1..10000) {
        println("i: $i | j: $j | k$k | i*j*k = ${i*j*k}")
      }
    }
  }
}

Listing 15-5Deeply Nested Calculation: Expensive Call

注意

笛卡尔积是一个数学集合,它是其他集合相乘的结果。

在 Android 的早期版本中,在 Project Butter 之前,清单 15-3 、 15-4 和 15-5 中显示的代码可能会导致 ANR 错误(Android 没有响应)。如今,他们可能不再画 ANR 了,但是更大的担忧是 jank。为了避免 jank,我们应该将那些昂贵的调用转移到后台线程。在 Android 中有很多方法可以做到这一点。有些解决方案是在框架层面上找到的,比如 Loader API 或 AsyncTaskLoader 然而,这些东西从 API 28 开始就被弃用了,所以最好远离它们。在后台也有一些低级的方法来完成一些任务,它们是:

  • 线程和可运行线程,来自 Java

  • AsyncTask 是 Android 框架的一部分

  • 处理程序和消息,也是 Android 框架的一部分

  • Anko 的 doAsync 是一个用 Kotin 编写的第三方库

线程和可运行程序

让我们使用清单 15-14 作为我们探索的用例。要运行这些代码,你需要一个类似图 15-1 的用户界面;我们的基本 UI 的 xml 代码在清单 15-6 中。

img/463887_1_En_15_Fig1_HTML.png

图 15-1

我们的基本活动 _ 主要布局

<?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">

  <Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="16dp"
    android:layout_marginTop="16dp"
    android:text="Button"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/textView" />

  <TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="16dp"
    android:layout_marginTop="32dp"
    android:text="TextView"
    android:textSize="30sp"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

Listing 15-6/app/res/layout/activity_main.xml

如果你现在试图运行清单 15-4 ,它将会运行;但是不会跑的很好。您将注意到以下内容:

  1. 你期望文本视图每 2 秒刷新一次,以显示 i 的当前值。不会的。这些帧会被删除,所以你看不到任何用户界面活动。

  2. 但是你会看到 I 的值,因为它在 Logcat 窗口中每 2 秒更新一次。这是因为 println 不受帧率降低的影响——输出是在控制台中,而不是在 UI 中。

  3. 您可能会看到来自运行时的编排器:的类似这样的消息

    07-31 15:51:29.646 13403-13403/net.workingdev.ch15scratchasynctask I/Choreographer: Skipped 2402 frames! The application may be doing too much work on its main thread.
    
    

虽然这款应用没有获得 ANR 奖,但速度明显变慢了。你肯定能感觉到一些玩笑。为了解决这个问题,让我们将 janky 代码移到后台线程中。

要创建一个线程并启动它,您需要执行以下操作:

  1. 创建一个实现 Runnable 类型的类。

  2. 任何你想在后台运行的东西,把它放在被覆盖的 run 方法中。

  3. 创建一个线程对象,然后将您刚刚在步骤 1 中创建的 Runnable 对象传递给线程的构造函数。

  4. 调用线程的 start 方法。

  5. 每当变量 i 的值改变时,我们更新 TextView。

在代码中,它看起来像下面这样(参见清单 15-7 )。

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    button.setOnClickListener {
      val runnable = Worker()
      val thread = Thread(runnable)
      thread.start()
    }
  }

  inner class Worker : Runnable {
    override fun run() {
      killSomeTime()
    }
  }

  private fun killSomeTime() {
    for (i in 1..20) {
      Thread.sleep(2000)
      println("i: $i")
    }
  }
}

Listing 15-7
Threads and Runnables

到目前为止,在本书的第十五章中,你已经知道了匿名对象、lambdas 以及如何链接函数调用。我们应该能做出这样的东西:

| -好的 | 使用 Kotlin lambda 表达式创建一个可运行的匿名对象。它被传递给一个线程类的构造函数。 | | ❷ | 我们不必再编写 **run** 方法了。Runnable 是一个 SAM 类(一个只有一个抽象方法的类)。在 lambda 表达式中使用 SAM 类时,不需要显式编写抽象方法的名称。 | | -你好 | 调用 **start** 将线程踢入高速档。 |
 button.setOnClickListener {
  Thread(Runnable {     ❶ ❷
    killSomeTime()
  }).start()            ❸
}

如果我们只想将 ln 打印到控制台,我们的代码现在应该可以正常工作。但是请记住,我们需要将 TextField 的值设置为当前值 i

不允许后台线程更改 UI 中的任何内容。这个责任只属于 UI 线程。因此,我们需要解决的下一个问题是如何回到 UI 线程,以便我们可以更新 TextView。有几种方法可以做到这一点,但最简单的是调用 Activity 类的 runOnUiThread 方法。

runOnUiThread 方法获取一个 Runnable 对象,并在主线程中执行该 Runnable 对象的代码。清单 15-8 显示了 MainActivity 的完整的、带注释的和解释的代码。

| -好的 | 要创建一个后台线程,需要创建一个 Runnable 类型的实例(thread)并 **start** 它。**线程**构造函数采用 Runnable 类型并执行 **run** 方法中的任何内容。我在这行中使用了一个对象表达式来创建一个 Runnable 类型的实例,而没有创建一个名为子类的*——有点像 Java 的匿名类。* | | ❷ | 我们现在在 Runnable 的 **run** 方法中。我们在后台线程中。 | | -你好 | 别忘了在线程对象上调用 **start** 。 | | (a) | 后台线程的限制之一是它**不能**做任何修改 UI 的事情。任何 UI 修改代码都必须从创建 UI 的原始线程运行——也就是 UI 线程。如果需要从后台线程改变 UI(像这样),可以调用**活动**类的 **runOnUiThread** 方法。它采用一个 Runnable 类型(再次),你可以把所有的 UI 修改代码放在这个 Runnable 类型的 **run** 方法上。 |
import android.os.AsyncTask
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    button.setOnClickListener {
      Thread(Runnable {                ❶
        killSomeTime()                 ❷
      }).start()                       ❸
    }
  }

  private fun killSomeTime() {
    for (i in 1..20) {
      runOnUiThread(Runnable{          ❹
        textView.text = i.toString()
      })
      println("i:$i")
      Thread.sleep(2000)
    }
  }
}

Listing 15-8Full Code of MainActivity, With Annotations

当您运行这段代码时,您应该每 2 秒钟看到一次变量 i 的更新值。的编舞也不会再因为丢帧而烦扰我们,因为我们回到了非常流畅的 60 FPS 的速度。

使用处理程序类

与线程不同,Handler 类是 Android 框架的一部分,而不是 Java 的一部分。处理程序对象主要用于管理线程。还记得之前关于将代码放入 MessageQueue 的讨论吗;它在那里等待,直到被拾取和执行——是处理程序进行拾取和执行。

基本思想是获取对主线程处理程序的引用,然后,当我们在后台线程中时(在这里我们不能进行任何 UI 更改),向处理程序对象发送一个消息。使用消息对象在后台线程和主线程之间传递数据。

要使用 Handler 对象,您需要执行以下操作:

  1. 获取与 UI 线程关联的处理程序对象。

  2. 在你代码的某个地方,当你要做一些可能导致 jank 的事情时,在后台线程上运行它。

  3. 当您在后台线程中时,当您需要更改 UI 中的某些内容时,请执行以下操作:

    a.创建一个消息对象,最好的方法是调用消息.获取()

    b.通过调用 sendMessage 方法向 Handler 对象发送消息。消息对象可以携带数据。消息对象的数据属性是一个 Bundle 对象,所以你可以对它使用各种putXXX()方法(例如 putString、putInt、putBundle、putFloat 等)。).

  4. 您可以在 Handler 对象的 handleMessage 回调中更改 UI。

清单 15-9 展示了所有这些是如何在代码中组合在一起的。

| -好的 | 将处理程序对象声明为类的属性。我们需要从我们的两个顶级功能访问它。我们在这里使用 **lateinit** 是因为我们还没有准备好定义对象。 | | ❷ | 我们现在正在定义处理程序对象。我们正在获取与 UI 线程关联的 Handler 对象。 | | -你好 | 在这里修改用户界面是安全的。这是与 UI 线程相关联的处理程序。当我们调用 **sendMessage** 时,运行时将调用 **handleMessage** 回调。此方法的消息参数携带数据。 | | (a) | **kill some**是任何 I/O 或耗时任务的代表。总是在后台线程中运行它,以避免 jank。 | | (一) | 创建一个消息对象。这是我们稍后将发送给处理程序的内容。 | | ❻ | 消息对象的**数据**属性就像一个**包**——你可以把东西放在里面。它就像一本字典,每个条目都是一对——一个键和一个值。我们向 putString()方法传递了两样东西,它们是:1.【计数器】、**键**2.`i.toString(),`**值** | | ❼ | 将消息发送到处理程序对象。 |
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Message
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

  lateinit var mhandler: Handler                        ❶

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    mhandler = object : Handler() {                     ❷
      override fun handleMessage(msg: Message?) {
        textView.text = msg?.data?.getString("counter") ❸
      }
    }

    button.setOnClickListener {
      Thread(Runnable {
        killSomeTime()                                  ❹
      }).start()
    }
  }

  private fun killSomeTime() {
    for (i in 1..20) {
      var msg = Message.obtain()                        ❺
      msg.data.putString("counter", i.toString())       ❻
      mhandler.sendMessage(msg)                         ❼
      Thread.sleep(2000)
    }
  }
}

Listing 15-9Full Listing for MainActivity, Annotated and Explained

当您运行这段代码时,它的性能与我们前面的线程示例一样好。

异步任务

另一种在后台运行代码的方法是使用 AsyncTask 类。AsyncTask 和 Handler 类一样,是 Android 框架的一部分。像处理程序一样,它有一个在后台完成工作的机制,并且它还提供了一个更新 UI 的(更干净的)方法。

要使用 AsyncTask,通常需要执行以下操作:

  1. 扩展 AsyncTask 类。

  2. 覆盖 AsyncTask 的 doInBackground 方法,这样您就可以完成后台工作。

  3. 重写几个 AsyncTask 的生命周期方法,这样就可以更新 UI 并报告后台任务的整体状态。

  4. 创建 AsyncTask 子类的一个实例,并调用execute——这就是如何启动后台操作。

AsyncTask 不如简单线程受欢迎的原因之一是它使用泛型。AsyncTask 类是参数化的。在使用它之前,您必须指定三种类型。清单 15-10 向我们展示了如何创建 AsyncTask 类的子类。

| -好的 | AsyncTask 是一个参数化类。在使用它之前,您必须指定三种类型。这三种类型按出现的顺序如下:**a .**Params。这是您需要传递给 AsyncTask 的信息,以便它可以执行后台任务。它可以是任何东西,比如 URL 列表、视图对象或字符串。为了让它对我们来说更有挑战性,它是一个 *vararg* 参数。通常,开发人员使用此参数来传递视图元素,以便 AsyncTask 可以引用活动的视图对象。但是在我们的例子中,我将使 AsyncTask 成为一个内部类——这样,它可以引用 MainActivity 中的任何视图元素(这就是我使用 **Void** 作为第一个类型参数的原因——我根本不需要它)。**b .**??【进度】??。您希望后台线程传递给 UI 线程的信息类型,以便您可以告诉用户正在发生什么。**c .**结果。您想要指示后台操作结果的种类数据;大多数时候,这不是*真*就是*假*。如果操作成功,则为*真,否则为*假。** | | ❷ | 这是唯一需要覆盖的强制函数。顾名思义,这是您在后台做事情的地方。每当你需要读/写一个文件或一个网络 I/O 时,你会想在这里做。这个函数接受一个 *vararg* **Void** 参数,它对应于我们为类定义的第一个*类型参数*。如果您将第一个类型参数设置为字符串,那么 **doInBackground** 应该接受一个字符串。还要注意,这个方法返回一个布尔值;那是因为我们传递了一个**布尔值**作为第三个参数类型。 | | -你好 | 定期地,你可能想要通知用户你的应用正在进行什么,特别是如果它是一个冗长的操作。 **publishProgress** 方法允许您这样做。当你在 **doInBackground** 里面的时候,你不能对 UI 做任何修改。UI 更改需要发生在 UI 线程上。当您调用 **publishProgress** 时,Android 运行时将调用**onprogress update**——在那里您可以进行 UI 更改。无论您向 **publishProgress** 传递什么参数,onProgressUpdate 都会接收到它。 | | (a) | 当你在这个方法中时,所有的语句都将在 UI 线程上执行。这是您对视图对象进行更改的地方。该方法接受一个字符串参数,因为我们将**字符串**作为 AsyncTask 类的第二个类型参数进行了传递,并且它与该类型参数相对应。在我们从**的背景**方法中调用**的发布进度**后,这个方法将被调用;无论您传递给 **publishProgress** 什么数据,都将由 **onProgressUpdate 接收。** | | (一) | 当 **doInBackground** 完成时,运行时将调用该方法。doInBackground 返回了**结果**参数。 |
AsyncTask<Void, String, Boolean> {                                ❶

  override fun doInBackground(vararg p0: Void?) : Boolean {       ❷
    // statement
    publishProgress("status of anything")                         ❸
  }
  override fun onProgressUpdate(vararg values: String?) {
    // update the UI                                              ❹
  }
  override fun onPostExecute(result: Boolean?) {
    println(result)                                               ❺
  }
}

Listing 15-10Subclassing the AsyncTask

现在我们已经熟悉了 AsyncTask 的结构,让我们看看如何在我们的计数示例中使用它。清单 15-11 显示了在 MainActivity 中使用 AsyncTask 的完整和带注释的代码。

| -好的 | 创建一个**工作者**的实例,然后**执行**它。 | | ❷ | 将 AsyncTask 定义为内部类,这样我们就可以引用封闭 MainActivity 的视图对象。*类型参数*解释如下。**a .作废**。我真的不需要传递任何东西给 AsyncTask,所以,Void。**b .字符串**。方法 **onProgressUpdate** 将更新文本视图。因为我们将使用第二种类型来更新值 TextView,所以 String 似乎是一个不错的选择。**c .布尔**。当我们完成**的后台**时,我们想要设置一个状态来表示成功或失败;布尔似乎是一个很好的选择。 | | -你好 | 我们来告诉用户 *i* 的当前值是多少。onProgressUpdate 采用字符串参数;这就是为什么我们要把 *i* 转换成一个整数。 | | (a) | 这模拟了长度运算。 | | (一) | 既然我们在 UI 线程中,我们可以安全地将 TextView 的*文本*属性设置为当前的 *i* 值。我们只从 **publishProgress** 传递了一个参数,所以如果我们想得到它,它是 **values** 参数的第 0 个元素。 |
import android.os.AsyncTask
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    button.setOnClickListener {
      Worker().execute()                                           ❶
    }
  }

  inner class Worker : AsyncTask<Void, String, Boolean>() {        ❷

    override fun doInBackground(vararg p0: Void?) : Boolean {
      for (i in 1..20) {
        publishProgress(i.toString())                              ❸
        Thread.sleep(2000)                                         ❹
      }
      return true
    }

    override fun onProgressUpdate(vararg values: String?) {
      textView.text = values[0]                                    ❺
    }

    override fun onPostExecute(result: Boolean?) {
      println(result)
    }
  }
}

Listing 15-11Full Code for MainActivity, Annotated and Explained

与处理程序和线程类一样,AsyncTask 将释放 UI 线程。当你运行这个程序时,它会以 60 帧/秒的速度发出咕噜声。

安科的 doasync

Anko 是 JetBrains(创建 Kotlin 的同一家公司)用 Kotlin 编写的 Android 库。您可以将它用于各种各样的任务,但是对于我们的目的,我们只需要 doAsync 部分。顾名思义,Anko 的 doAsync 将让我们异步或在后台运行代码。

在使用 Anko 之前,您需要将它添加到项目的 Gradle 文件的依赖项中,如清单 15-12 所示。

dependencies {
  ....
  implementation 'org.jetbrains.anko:anko-common:0.9'
}

Listing 15-12/app/build.gradle

使用 doAsync 的语法如清单 15-13 所示。

| -好的 | 在这里,您可以读取或写入大文件,从互联网上下载文件,或者执行需要很长时间才能完成的任务。该块将在后台线程中执行。 |
doAsync {
  // do things in the background  ❶
}

Listing 15-13Syntax for doAsync

下一个挑战是如何回到 UI 线程。请记住,后台线程不允许更改 UI 中的任何内容。Anko 的方法可能是我们在前面章节中讨论的所有其他选项中最简单的。清单 15-14 展示了一个样例代码,它展示了 doAsync 如何在后台运行代码,以及它如何返回到 UI 线程。

清单 15-14 。doAsync 和 activityUiThread

| -好的 | 后台处理。 | | ❷ | 现在,您回到了 UI 线程。就这么简单。无论何时你需要返回 UI 线程,你都可以在**activity ithread**块中完成。 |
doAsync {
  // do things in the background  ❶
  activityUiThread {
    // make changes to the UI     ❷
    textView.text = "Hello"
  }
}

清单 15-15 显示了 MainActivity 的完整代码示例。它使用 Anko 的 doAsync 来执行长时间的计算,然后将一些内容写回 UI。

| -好的 | 让我们设置一个基本的 OnClickListener。这将触发后台任务。 | | ❷ | 让我们从 1 数到 15。 | | -你好 | 这模拟了一个长时间运行的任务。我们的循环大约要进行 15 次,所以完成这个任务总共需要 30 秒。 | | (a) | 让我们告诉用户这个应用是怎么回事。用 *i.* 的当前值更新 TextView 对象 |
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.activityUiThread
import org.jetbrains.anko.doAsync

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    button.setOnClickListener {             ❶
      doAsync {
        for(i in 1..15) {                   ❷
          Thread.sleep(2000)                ❸
          activityUiThread {
            textView.text = i.toString()    ❹
          }
        }
      }
    }
  }
}

Listing 15-15Full Code for MainActivity Using doAsync, Annotated and Explained

doAsync 就像之前的 Thread、Handler 和 AsyncTask 示例一样,应该表现得一样好。当您运行这段代码时,应用将以 60 FPS 的速度流畅运行。

您已经看到了在后台执行任务的四种底层技术。希望代码示例给了您足够的想法,让您可以继续自己的工作。

现实世界的例子

在我们结束这一章之前,让我们研究一些你可能会在项目中用到的东西。让我们使用 GitHub 的公共 API 从 GitHub 获取一些用户信息。GitHub 允许任何人访问 https://api.github.com/users/<username> 。如果您有 GitHub 帐户,请尝试使用您的 GitHub 登录来调用此 URL,这样您就可以熟悉它返回的内容。清单 15-16 显示了使用我自己的 GitHub id (tedhagos)的 HTTP 调用的部分输出。

{
  "login": "tedhagos",
  "id": 1287584,
  "node_id": "MDQ6VXNlcjEyODc1ODQ=",
  "avatar_url": "https://avatars1.githubusercontent.com/u/1287584?v=4",
  "gravatar_id": "",
  "url": "https://api.github.com/users/tedhagos",
  "html_url": "https://github.com/tedhagos",
  "followers_url": "https://api.github.com/users/tedhagos/followers",
  "following_url": "https://api.github.com/users/tedhagos/following{/other_user}",
  "gists_url": "https://api.github.com/users/tedhagos/gists{/gist_id}",
  "starred_url": "https://api.github.com/users/tedhagos/starred{/owner}{/repo}",
  "subscriptions_url": "https://api.github.com/users/tedhagos/subscriptions",
  "organizations_url": "https://api.github.com/users/tedhagos/orgs",
  "repos_url": "https://api.github.com/users/tedhagos/repos",
  "events_url": "https://api.github.com/users/tedhagos/events{/privacy}",
  "received_events_url": "https://api.github.com/users/tedhagos/received_events",
  "type": "User",
  "site_admin": false,
  "name": "Ted Hagos",
  "company": null,
  "blog": "https://workingdev.net",
  "location": null,
  "email": null,
  "hireable": null,
  "bio": "Currently CTO and Data Protection Officer of RenditionDigital International. Sometimes a writer and tech trainer."
}

Listing 15-16Sample JSON Response from GitHub API

我们想要做的如下:

  1. 提示用户输入 GitHub 帐户;是登录 id。我们将使用 EditText 的 hint 属性来告诉用户输入什么。

  2. 使用我们从用户那里获得的登录 id 编写 HTTP 请求。我们可以通过使用低级的 java.net 类来 DIY 我们的方法,但是那会分散我们对主题的注意力,所以我们将使用 OkHttp。这是一个第三方库,但它非常易于使用,最重要的是,易于理解。

  3. 对 GitHub API 进行 HTTP 调用,并在后台线程中运行。我们将在这个项目中使用 Anko 的 doAsync。这是最容易使用的。你不觉得吗?

  4. HTTP 调用返回一个 JSON 对象,如清单 15-16 所示。我们将解析 JSON 消息,只获取 name 属性的值。

  5. 我们将通过使用方法activity ithread返回到 UI thread,在那里,我们将使用 name 属性的值(我们从 JSON 对象获得的值)更新 textView。

表 15-1 显示了演示项目的详细信息。

表 15-1

项目详细信息

|

项目详细信息

|

价值

|
| --- | --- |
| 应用名称 | CH15GetGitHubInfo |
| 公司域 | 使用您的网站名称 |
| Kotlin 支架 | 是 |
| 波形因数 | 仅限手机和平板电脑 |
| 最低 SDK | API 23 棉花糖 |
| 活动类型 | 空的 |
| 活动名称 | 主要活动 |
| 布局名称 | 活动 _ 主要 |
| 向后兼容性 | 是的。应用兼容性 |

UI 截图如图 15-2 所示。我们将使用 EditText 获取用户的输入,并使用 TextView 显示返回的 JSON 文件的名称属性。

img/463887_1_En_15_Fig2_HTML.png

图 15-2

CH15GetGitHubInfo 的用户界面

清单 15-17 显示了 activity_main.xml 的完整清单

<?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"
  tools:layout_editor_absoluteY="81dp">

  <Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="8dp"
    android:text="Button"
    app:layout_constraintStart_toStartOf="@+id/txtusername"
    app:layout_constraintTop_toBottomOf="@+id/txtusername" />

  <TextView
    android:id="@+id/txtusername"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="8dp"
    android:text="TextView"
    android:textSize="30sp"
    app:layout_constraintStart_toStartOf="@+id/txtsearchuser"
    app:layout_constraintTop_toBottomOf="@+id/txtsearchuser" />

  <EditText
    android:id="@+id/txtsearchuser"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="31dp"
    android:layout_marginTop="30dp"
    android:ems="10"
    android:inputType="textPersonName"
    android:text="Name"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

Listing 15-17/app/res/layout/activity_main.xml

在使用 OkHttp 和 Anko 库之前,您需要将它们的依赖项添加到项目的模块级 gradle 文件中。清单 15-18 显示了您需要添加到 /app/build.gradle依赖项部分的内容。

| -好的 | 您需要添加此项才能使用 OkHttp。 | | ❷ | 你需要添加这个,这样你才能使用 Anko 的 doAsync。 |
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0-alpha3'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation 'com.squareup.okhttp:okhttp:2.5.0'   ❶
    implementation 'org.jetbrains.anko:anko-common:0.9' ❷
}

Listing 15-18Add OkHttp and Anko to /app/build.gradle

在 gradle 文件中添加了 Anko 和 OkHttp 之后,您必须同步该文件。点击屏幕右上角的“立即同步”链接,如图 15-3 所示。

img/463887_1_En_15_Fig3_HTML.jpg

图 15-3

编辑后同步 gradle 文件

OkHttp 网站有一个展示基本用法的示例代码——如清单 15-19 所示。它是用 Java 写的,但是很容易改编为我们所用。

OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

  Response response = client.newCall(request).execute();
  return response.body().string();
}

Listing 15-19Sample Code from 
http://square.github.io/okhttp/

清单 15-20 展示了我们 Kotlin 版本的 OkHttp 代码示例。

private fun fetchGitHubInfo(login_id: String): String {
  val url = https://api.github.com/users/$login_id
  val client = OkHttpClient()
  val request = Request.Builder().url(url).build()
  val response = client.newCall(request).execute()
  val bodystr =  response.body().string() // this can be consumed only once

  return bodystr
}

Listing 15-20Our Kotlin Version of OkHttp Code

够近了。顺便说一句,我希望你注意到了清单 15-20 的倒数第二行——我甚至注释了它。调用 response.body.string,时只能消费一次,不能这样调用:

println(response.body.string())            // consumes the content
val bodystr =  response.body().string().   // no more JSON file here

response.body.string 调用不是等幂。你不能重复调用它,并期望它每次调用都返回相同的结果。

现在我们已经得到了我们需要的一切,是时候编写 MainActivity 了。清单 15-21 显示了 MainActivity 的完整和带注释的代码。

| -好的 | Anko 的 **doAsync** 块从这里开始。这个块中的所有东西都将在后台线程中运行。 | | ❷ | 让我们将 **txtsearchuser** EditText 的当前值传递给 **fetchGitHubInfo** ,并将结果 JSON 对象赋给 *mgithubinfo* 变量。 | | -你好 | 让我们用内置的 **JSONObject** 解析 *mgithubinfo* 。 | | (a) | 现在我们需要返回 UI 线程,这样我们就可以将 http 调用的结果写入 UI。 | | (一) | **activity ithread**块让我们回到 UI 线程并做一些更改。我们将 **txtusername** 的**文本**属性设置为 JSON 文件的 name 属性。 |
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import com.squareup.okhttp.OkHttpClient
import com.squareup.okhttp.Request
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.activityUiThread
import org.jetbrains.anko.doAsync
import org.json.JSONObject

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    button.setOnClickListener {
      doAsync {                                                           ❶
        val mgithubinfo = fetchGitHubInfo(txtsearchuser.text.toString())  ❷
        val jsonreader = JSONObject(mgithubinfo)                          ❸
        activityUiThread {                                                ❹
          txtusername.text = jsonreader.getString("name")                 ❺
        }
      }
    }
  }

  private fun fetchGitHubInfo(login_id: String): String {
    val url = "https://api.github.com/users/$login_id"
    val client = OkHttpClient()
    val request = Request.Builder().url(url).build()
    val response = client.newCall(request).execute()
    val bodystr =  response.body().string() // this can be consumed only once

    return bodystr
  }

  override fun onResume() {
    super.onResume()

    txtsearchuser.setText("")
    txtsearchuser.setHint("Enter GitHub username")
  }
}

Listing 15-21MainActivity, Annotated and Explained

在运行应用之前,还有一件事要做:我们需要将 INTERNET 权限添加到清单文件中。

| -好的 | 您应该将它添加到项目的 AndroidManifest 文件中。 |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="net.workingdev.ch15getgithubinfo">
  <uses-permission android:name="android.permission.INTERNET"/> ❶
  <application
  ....
  </application>=
</manifest>

Listing 15-22
AndroidManifest.xml

图 15-4 显示了正在运行的应用。

img/463887_1_En_15_Fig4_HTML.jpg

图 15-4

仿真器上的 CH15GetGitHubInfo

章节总结

  • 什么是 jank?当你试图在 UI 线程上做太多事情时,Android 运行时会开始丢帧。当你的应用的 FPS 下降时,用户界面会断断续续,使用起来会很慢,很烦人。这是杰克。

  • 我们如何避免?不要试图在 UI 线程上做太多。不要:

    • 从大文件中读取,或者向文件中写入大量信息。

    • 连接到网络并从中读取(或写入)。

    • 计算一个复杂的例程在后台线程中做这些事情。

  • 什么是 UI 线程?最初的线程负责在应用中创建(和修改)视图元素。一些开发人员将 UI 线程称为“主线程”

  • 什么是后台线程?任何不是 UI 线程的线程。你通常为你的应用创建一个后台线程。

  • 创建后台线程的方法有哪些?Java 线程、处理程序、AsyncTask 和 Anko 的 doAsync

在下一章中:

  • 我们将了解开发人员日常面临的各种错误。

  • 我们也会得到一些如何避免它们的提示。

  • 如果我们深陷在错误中,我们会知道该怎么做。

十六、排除故障

我们将介绍的内容:

  • 你会遇到的错误种类

  • 记录调试语句

  • 使用交互式调试器遍历代码

很快,您将会超越本书中示例代码的简单结构。你的程序会变得越来越复杂,文件越来越多,组件越来越多。随着这种情况的发生,你将面临的错误数量也会增加;到那时它们可能更难被发现。

在这一章中,我们将看看你可能会遇到的三种主要的错误,以及什么样的工具或技术可以帮助你解决。

句法误差

语法错误正如您所想的那样:语法错误。发生这种情况是因为你在代码中写了一些 Kotlin 编译器的规则中不允许的东西。换句话说,编译器不理解它。这可能是良性的,就像忘记了表达式中的右花括号或右圆括号一样。它也可能稍微复杂一些,比如在使用泛型时将错误类型的参数传递给函数或参数化类。在 Android 开发的早期,您所能做的就是使用裸露的 SDK,您只有在尝试编译代码时才能知道是否有语法错误,这就是为什么其他程序员也将这种错误称为“编译时”错误的原因。当然,自那以后,Android 开发已经走过了漫长的道路。我们有一个非常称职的 IDE,它甚至可以在你试图编译你的代码之前就发现并指出语法错误。这就好像 IDE 一直在读取代码并编译它。

图 16-1 显示了一个内部 AsyncTask 子类的片段。IDE 通过用红色曲线突出显示有问题的代码来引起您的注意。

img/463887_1_En_16_Fig1_HTML.jpg

图 16-1

AsyncTask 类缺少构造函数

将鼠标悬停在出现曲线的区域足够长的时间,您应该会看到 AS3 的气球提示。它说 AsyncTask 类有一个必须初始化的类型构造函数。要修复它,将构造函数调用——一对括号——放在类定义的旁边,如图 16-2 所示。

img/463887_1_En_16_Fig2_HTML.jpg

图 16-2

AsyncTask 类缺少强制实现

弯弯曲曲的线条正在一条一条消失。这是一个好迹象,意味着我们正在修复错误,但还没有完成。你注意到图 16-2 中的第 15 行了吗?我们仍然有一个错误。它说我们的类没有实现基类成员。AsyncTask 类是抽象的;它声明了抽象方法 doInBackground 。我们必须重写这个方法并编写我们的实现,除非我们也将 class Worker 变成一个抽象类——这不是我们的意图。利用 Android Studio 的快速修复功能( option + Enter 在 Mac, alt + Enter 在 Windows 和 Linux)解决问题,如图 16-2 。

图 16-3 显示了快速固定的作用。它为我们如何修复它提供了一些建议。第一个选项是我们想要的——实现并覆盖 AsyncTask 的抽象成员。

img/463887_1_En_16_Fig3_HTML.jpg

图 16-3

AsyncTask 类的快速修复

点击确定。接下来是实现成员的对话框,如图 16-4 所示。AsyncTask 只有一个需要由子类重写的抽象成员。选择做背景并点击确定继续。

img/463887_1_En_16_Fig4_HTML.jpg

图 16-4

实现成员

Android Studio 会给你一个 doInBackground 函数的结构骨架。现在,您可以编写您的实现了。

有时,即使有曲线的帮助,错误也不是很明显。图 16-5 显示了这个问题的一个例子。

img/463887_1_En_16_Fig5_HTML.jpg

图 16-5

嵌套块

图 16-5 中第 14 行和第 27 行之间的代码显示了一个深度嵌套的块。当您使用匿名对象时,有时会发生这种情况,您可以从示例代码的结构中看到这一点。

img/463887_1_En_16_Fig6_HTML.jpg

图 16-6

错误代码

如果你尝试制作项目(从主菜单栏➤ 构建制作),IDE 会给你更多的信息,更多的信息,如图 16-6 所示;但可能不会给你更多的感悟。这是你确实需要做繁重工作的情况之一。您必须手动检查代码结构。请注意,曲线出现在类的末尾(图 16-6 中的第 27 行)以及告诉我们缺少花括号的错误消息;从那里开始,手动检查成对的花括号。这个问题与我们如何构建代码有关。你只需要小心那些大括号——Python 程序员现在可能会幸灾乐祸地说,“这就是你使用大括号的结果,缩进石头。”

运行时错误

当你的代码遇到它没有预料到的情况时,运行时错误就会发生;顾名思义,这种错误情况只有在程序运行时才会出现,而不是你或编译器在编译时能看到的。您的代码可以顺利编译,但是当运行时环境中的某些内容与您的代码不一致时,它可能会停止运行。这些事情有很多例子,例如:

  • 该应用从互联网上获取一些东西——一张图片或一个文件等。—所以它假设互联网可用,并且有网络连接。一直都是。经验应该告诉你,情况并不总是这样。网络连接有时会中断,如果您不在代码中考虑这一点,它可能会崩溃。

  • 该应用需要从文件中读取。就像我们前面的第一个案例一样,您的代码假设文件将一直存在。有时,文件会损坏,可能变得不可读。这也应该在代码中考虑。

  • 该应用执行数学计算。它使用用户输入的值,有时也使用从其他计算中导出的值。如果您的代码碰巧执行了除法,并且其中一个除法的除数为零,这也会导致运行时问题。

这里有一些代码示例,乍看起来可能没问题,并且可以编译,但是当它在运行时遇到没有准备好的情况时,就会出现运行时错误。

清单 16-1 显示了打开一个文件并将其内容读入一个字符串变量的基本代码。如果代码试图打开一个已经存在的文件,没有问题——代码会正常工作,如预期的那样。如果它试图打开一个不存在或由于某种原因无法读取的文件,问题就会出现。

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)

  button.setOnClickListener {
      openFile("doesnotexist.txt")
  }
}

private fun openFile(file: String) {
  val strFile = File(file).readText()
}

Listing 16-1Possible FileNotFoundException or Other IOException

清单 16-2 可能看起来不自然,但是想象一下,如果你从一个用户那里得到输入,或者你从其他地方读取输入,除数变成零。您将遇到一个算术异常错误。

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)

  button.setOnClickListener {
      divide(10, 0)
  }
}

private fun divide(a:Int, b:Int) :  {
  return a / b
}

Listing 16-2Possible ArithmeticException

顺便说一下, ArithmeticException 只对整数值抛出。对于浮点型和双精度型不会发生这种情况。如果你试图将一个浮点数除以零,它只会产生一个无穷大的值,但不会抛出异常。

清单 16-3 显示了另一个会遇到运行时问题的代码示例。它现在看起来是人为的,因为你可以明显地看到,数组中没有第五元素。但是想象一下,如果你从一个 API 中读取数组(你没有创建数组,是别人创建的),并且你没有使用整数来访问数组;取而代之的是使用变量。到时候误差就不会那么明显了。

val arr = arrayOf(1,2,3,4)
println(arr[5])

Listing 16-3ArrayIndexOutOfBounds Exception

解决运行时错误的唯一方法是:

  1. 了解你的代码。您需要知道哪些调用可能会遇到运行时异常;和

  2. 在代码中使用适当的异常处理。

像 Java 一样,Kotlin 也使用 try-catch 结构来处理异常;但是与 Java 不同,Kotlin 的所有异常都是未检查的(??)。异常处理在 Kotlin 中实际上是可选的— throws 在 Kotlin 中甚至不是一个关键字,但是throws关键字仍然是。这可能是好事,也可能是坏事,看你怎么看;在流行的编码论坛上有关于这个主题的热烈讨论。Kotlin 团队关于检查异常的意见可以在 Kotlin 在线文档( https://kotlinlang.org/docs/reference/exceptions.html )中找到。

根据 Kotlin 团队的说法,Kotlin 针对的是大型开发项目,几乎没有证据表明使用检查异常有助于开发人员的生产力;恰恰相反,它减轻了它。

Kotlin 中的异常处理在很大程度上与 Java 中的方式完全一样。你可以用试抓试抓最终来做。在 Java 7 中,引入了try-withresources 的概念。Kotlin 没有 try-with-resources,但是它有 use 扩展;这相当于尝试资源。

为了唤起我们的记忆,try-catch 块的基本形式如清单 16-4 所示。

| -什么 | 这是 **try** 块的主体。这是您应该编写可能抛出异常的调用的地方。 | | ➋ | 您必须在 catch 子句中尽可能多地提供确切的异常类型(例如,如果您正在处理 FileNotFoundException,那么这就是您应该在此处写入的内容,以代替 **theException)。** | | ➌ | 这是 **catch** 子句的主体。这是您应该写下当异常发生时您想要做的事情的地方(例如,记录到文件,要求用户重复输入,等等)。). | | -你好 | 有时,您可能不想处理异常。你可以**把**扔给函数的调用者(调用栈的上一层),让它成为他们的问题。 | | ➎ | **finally** 子句的主体是放置代码的地方,无论是否发生异常,都要执行这些代码。finally 子句的主体是保证总是被执行的*。* |
try {
  ... ➊
}
catch(mexception: theException) { ➋
  ... ➌
  throw mexception ➍
}
finally {
  ... ➎
}

Listing 16-4The Try-Catch-Finally Structure

现在,让我们看看如何使用 try-catch 来防止打开文件时崩溃。参见清单 16-5 。

| -什么 | **文件**构造函数实际上可以抛出一个 FileNotFoundException,所以我们将它们放在一个 try-catch 块中。 | | ➋ | 我们知道 **FileNotFoundException** 可以被 **File** 构造函数抛出,所以这就是我们放在 **catch** 子句中的内容。如果你想匹配一个更一般类型的异常,你也可以在这里使用 **IOException** 。IOException 是 **FileNotFoundException 的父类。** |
private fun openFile(file: String) {
  try {                                     ➊
    File(file).useLines {
      println(it)
    }
  }
  catch (fe: FileNotFoundException) {       ➋
    println("do your error handling here")
  }
}

Listing 16-5How to Handle the FileNotFoundException

清单 16-6 展示了如何在处理整数运算时防止崩溃。

  private fun divideInt(a:Int, b:Int): Int {
    var result = 0
    try {
      result = a /b
    }
    catch (ae: ArithmeticException) {
      println("handle your exception here")
    }
    finally {
      return result
    }
  }

Listing 16-6How to Handle the ArithmeticException

逻辑错误

逻辑错误是最难发现的。顾名思义,这是你逻辑上的错误。当你的代码没有做你认为它应该做的事情时,那就是逻辑错误。有许多方法可以解决这个问题,但是在这一节中,我们将研究两种方法:在代码的某些地方打印调试语句和使用断点进行代码遍历。

当您检查代码时,您会发现某些您非常确定发生了什么的区域,然后还有一些您不太确定的区域—您可以在这些区域中放置调试语句。就像留下面包屑让你跟着走。有几种方法可以打印调试语句。你可以使用 Java 中的 printlnLog、或者 Logger 类。

图 16-7 显示了 Logcat 工具窗口中 println 语句的输出。

img/463887_1_En_16_Fig7_HTML.jpg

图 16-7

在 Logcat 工具窗口中显示 println

println 是您可以用来打印调试语句的最简单、最容易的工具,但是请记住,只有当 Logcat 的模式设置为“verbose”、“info”或“debug”时,您才能在 Logcat 中看到这些语句。如果您将模式设置为其他模式,如 warn、error 或 assert,您将看不到 println 语句。

当您将 Logcat 的模式设置为 verbose、info 或 debug 时,您将看到 Android 运行时生成的所有消息。如果您只想看到警告消息或错误,那么您需要使用 Log 或 Logger 类。

Log 类有五个静态方法;用法如下所示。

Log.v(tag, message) // verbose
Log.d(tag, message) // debug
Log.i(tag, message) // info
Log.w(tag, message) // warning
Log.e(tag, message) // error

在每种情况下,标签是一个字符串或变量。您可以使用标记来过滤 Logcat 窗口中的消息。消息也是字符串或变量,它包含您实际想要在日志中看到的内容。清单 16-7 显示了如何使用 Log 类的示例代码。

| -什么 | 您可以在类中的任何地方定义标签,但是在本例中,它被定义为 class property。 | | ➋ | 我们正在打印调试信息。 | | ➌ | 我们正在打印警告信息。 |
  val TAG = this@MainActivity::class.toString() ➊

  private fun divideInt(a:Int, b:Int): Int {
    var result = 0
    try {
      Log.d(TAG, "Inside the try")              ➋
      result = a /b
    }
    catch (ae: ArithmeticException) {
      Log.w(TAG, "Sample log message")          ➌
    }
    finally {
      return result
    }
  }

Listing 16-7How to Use the Log Class

或者,我们也可以使用 Java 中的 Logger 类;如清单 16-8 所示。

val Log = Logger.getLogger(MainActivity::class.java.name)

private fun divideInt(a:Int, b:Int): Int {
  var result = 0
  try {
     Log.info("inside try")
     result = a /b
    }
  catch (ae: ArithmeticException) {
     Log.warning("Sample log message")
     println("handle your exception here")
  }
  finally {
    return result
  }
}

Listing 16-8How to Use the Logger Class

运行应用时,您可以在 Logcat 工具窗口中看到日志消息。你可以在 AS3 窗口底部的菜单栏点击它的标签,或者从主菜单栏查看➤工具窗口日志目录来启动它。图 16-8 显示了 Logcat 工具窗口。

img/463887_1_En_16_Fig8_HTML.jpg

图 16-8

Logcat 工具窗口

遍历代码

AS3 包括一个交互式调试器,允许您在代码运行时遍历和单步调试代码。使用交互式调试器,我们可以检查应用的快照——变量值、正在运行的线程等。—在代码中的特定位置和特定时间点。代码中的这些特定位置被称为断点;你可以选择这些断点。

若要设置断点,请选择包含可执行语句的一行,然后在装订线中点按其行号。设置断点时,槽内会出现一个粉红色的圆圈图标,整行都是粉红色点亮,如图 16-9 所示。

img/463887_1_En_16_Fig9_HTML.png

图 16-9

调试器窗口

设置断点后,您必须在调试模式下运行应用。如果应用当前正在运行,请将其停止,然后从主菜单栏中点击运行调试应用。

注意

在调试模式下运行应用并不是调试应用的唯一方式。您还可以在当前运行的应用中附加调试器进程。在某些情况下,第二种技术非常有用,例如,当您试图解决的错误发生在非常特定的条件下时,您可能希望运行应用一段时间,当您认为您接近错误点时,您可以连接调试器。

照常使用该应用。当执行到您设置断点的一行时,该行将从粉红色变为蓝色。这就是你如何知道代码执行是在你的断点。此时,调试器窗口打开,执行停止,AS3 进入交互式调试模式。当您在这里时,应用的状态显示在调试工具窗口中。在此期间,您可以检查变量值,甚至看到应用中运行的线程。

您甚至可以通过单击带有眼镜图标的加号,在“监视”窗口中添加变量或表达式。将有一个文本字段,您可以在其中输入任何有效的表达式。当你按下输入时,Android Studio 会对表达式进行求值,并向你显示结果。要删除监视表达式,请选择表达式,然后单击“监视”窗口上的减号图标。

要恢复程序执行,您可以单击调试器工具栏顶部的“恢复程序”按钮—它是指向右侧的绿色箭头。或者,您也可以从主菜单栏运行恢复程序中恢复程序。如果你想在程序自然完成之前暂停它,你可以点击调试器工具栏上的“停止应用”按钮——它是一个红色的正方形图标。或者,您也可以从主菜单栏运行停止应用中执行此操作

其他说明

在 Android 开发的早期,那时还没有 ide,开发人员使用一种叫做“adb”的工具,这是 Android Debug Bridge 的缩写。这是一个漂亮的命令行工具,可以让你与 Android 设备(虚拟的或真实的)进行通信。它可以让您做以下事情:

  • 安装应用

  • 调试应用

  • 获取对外壳终端的访问权限;请记住,Android 是基于 Linux 的,访问终端会非常方便(例如,当您在 sqlite 数据库上进行一些白盒测试时,等等)。).

Android Studio 接管了一些过去由 adb 做的事情(例如,显示日志、安装应用、调试应用等。).但是,如果你需要在 linux 命令行级别做事情,你真的必须使用ADB——你可以在ANDROID _ HOME/SDK/platform-tools文件夹中找到这个工具;其中 ANDROID_HOME 是您安装 Android SDK 的文件夹。

本章中我们没有提到的另一个工具是 Android Profiler ,它是 Android Studio 3.0 中的新功能。它取代了一个名为的安卓设备监控工具。你可以使用这个工具来查看你的应用的实时数据。您可以找出您的应用消耗了多少 CPU、内存、网络和 I/O 资源。您可以捕获堆转储、查看内存分配以及检查网络传输文件的详细信息。

章节总结

  • 您可能遇到的三种错误是编译类型或语法错误、运行时错误和逻辑错误。

  • 语法错误是最容易修复的。Android Studio 本身为您竭尽全力,让您可以快速发现语法错误。用 AS3 有各种方法来修复语法错误,但是大多数时候,快速修复应该可以做到。

  • Kotlin 不像 Java 那样有检查异常。Kotlin 团队这样做是有充分理由的。如果你是 Kotlin 的初学者,但对 Java 却很陌生,那么这应该不会影响你——在处理可能的异常时,使用你对旧 Java APIs 的了解。如果你是 Kotlin 和 Java 的新手,你应该多花一点时间学习单元测试;这样,你就可以看到你的应用的“快乐之路”和“不快乐之路”;然后你就可以采取相应的行动。

  • 逻辑错误是最难发现的,但 Android Studio 使这种活动变得更容易忍受,因为我们可以使用工具——你可以在程序运行时遍历代码并检查事情。

在下一章中,我们将了解以下内容:

  • 如何使用 SharedPreferences 保存数据?

  • 我们将使用 Bundle 对象,这样我们可以将一些基本类型保存到一个文件中。

  • 我们还将看看如何在活动之间传递数据。

十七、共享参数

我们将介绍的内容:

  • 共享首选项简介

  • 如何从首选项文件中放置和获取数据

  • 如何在活动之间共享首选项文件

默认情况下,Android 应用不会保存你的数据。在应用的整个生命周期中,保持数据的持久性和弹性是您的责任。假设您正在从用户那里收集数据,然后在工作流程进行到一半时,应用被另一个应用中断了。不能保证当你的应用返回时,用户已经输入的任何数据都会存在。

使数据持久意味着以某种形式存储数据。你可以用几种方法存储数据。它们列举如下:

  • 共享的首选项。这是最简单的选择。这只是一个字典对象,它使用熟悉的键值对机制。如果您的数据足够简单,能够以键值对的形式进行结构化,这将非常有用。Android 将这些文件作为 XML 文件存储在内部。您只能存储简单的数据类型,如字符串和基本类型。这通常用于存储用户的偏好,比如列表中的排序顺序,你在电子书应用上阅读的最后一页,等等。

  • 内部或外部存储。使用设备中的内部或媒体存储器(例如 sdcard)。您可以使用它来存储结构更复杂的数据(例如,音频或视频文件)。如果您以前使用过文件 I/O,这与那没有什么不同。

  • SQLite 数据库。这个使用关系数据库。如果您以前使用过其他数据库,如 MS SQL server、MySQL、PostgreSQL 或任何其他关系数据库,这在本质上是相同的。数据存储在表中,您需要使用 SQL 语句来创建、读取、更新和删除数据。

  • 网络存储。如果您可以假设您的用户将始终可以访问 internet,并且您有一个托管在 Internet 上的数据库服务器,那么您可以使用此选项。这种设置可能会有点复杂,因为您需要在某个地方(Amazon、Google 或任何其他云提供商)托管数据库,为数据提供 REST 接口,并在 Android 应用中使用 HTTP 库作为客户端。本书不涉及这个话题。

  • 内容提供者。内容提供商是 Android 平台上的另一个组件;它就在活动、服务和广播接收器的上面。该组件使数据对应用可用,而不是对其本身。可以把它想象成一个拥有公共 HTTP API 层的数据库。任何通过 HTTP 通信的应用都可以读写数据。顺便说一下,ContentProviders 在内部使用 SQLite 数据库——他们只是在简洁的 HTTP API 中包装和提供数据。如果你开发过 RESTful 应用,这些应用通过 API 公开了一些底层数据,这就有点像。

在本章中,我们将了解 SharedPreferences。

SharedPreferences 对象允许您以键-值对的形式存储和检索数据,就像字典一样。它使用 XML 文件进行存储。使用 SharedPreferences 对象存储基本数据可以通过以下步骤完成:

  1. 获取 SharedPreferences 对象。您可以通过从活动中调用 getPreferences 方法来实现这一点。

  2. 接下来,我们得到一个shared preferences。编辑器对象,方法是使用 SharedPreferences 对象的工厂方法。

  3. 现在我们可以用编辑器对象插入数据。

  4. 最后,为了永久存储数据,我们在编辑器上使用了提交应用方法。

清单 17-1 展示了所有这些在代码中的样子。

| -什么 | **Activity.getPreferences** 方法给了我们一个 **SharedPreferences** 对象,它是活动的私有对象。我们使用`Context.MODE_PRIVATE`是因为我们希望首选项文件只允许我们的应用访问——其他应用禁止访问。 | | ➋ | 我们需要一个共享的参考。Editor 对象,我们可以通过在 SharedPreferences 对象上调用 **edit** 方法来获得它。 | | ➌ | 现在,我们可以使用各种 **putXXX** 方法来存储键值对。第一个参数是键,这应该是一个字符串。第二个参数可以是任何基本类型,如 Int、Float、Double、String 等。 | | -你好 | 如果我们不调用**应用**,我们的 **putString** 调用都不会被永久存储到文件中。或者,你也可以调用**提交**。**应用**方法异步保存数据*,而**提交**同步保存*。*因此,要持久化数据,调用 apply 或 commit。* |
val pref = getPreferences(Context.MODE_PRIVATE) ➊
val editor = pref.edit()  ➋

editor.putString("lastname", "Breslav") ➌
editor.putString("firstname", "Andrey")
editor.apply() ➍

Listing 17-1Basic Steps to Save Data

如果你想知道其他的上下文模式选项,这里有。

  • MODE_PRIVATE :默认模式,创建的文件只能由调用应用访问。这可能是你大多数时候想要的。

  • MODE_WORLD_READABLE :任何应用都可以读取偏好数据。这可能会导致应用出现安全漏洞。除非你有一个非常好的理由,否则远离这个。如果您想让数据对任何应用可用,可以考虑构建一个 ContentProvider。

  • MODE_WORLD_WRITEABLE :任何应用都可以编辑偏好数据。这可能会导致应用出现安全漏洞。还是那句话,除非你有充分的理由,否则远离这个。

  • MODE_APPEND :这将把已经存在的参数追加到新的参数中。

让我们为此做一个小的演示项目。表 17-1 显示了项目的详细情况。

表 17-1

演示项目的详细信息

|

项目详细信息

|

价值

|
| --- | --- |
| 应用名称 | ch17 首选项 |
| 公司域 | 使用您的网站名称 |
| Kotlin 支架 | 是 |
| 波形因数 | 仅限手机和平板电脑 |
| 最低 SDK | API 23 棉花糖 |
| 活动类型 | 空的 |
| 活动名称 | 主要活动 |
| 布局名称 | 活动 _ 主要 |

我们想做的是:

  1. 让用户输入他的姓和名——我们将为此使用两个编辑文本。

  2. 当用户单击“保存”按钮时,我们将把姓氏和名字存储到首选项文件中。

  3. 当用户单击“Load”按钮时,我们将从首选项文件中读取姓氏和名字。

  4. 我们将在 TextView 对象中显示它们。

图 17-1 为正在运行的 app 截图。

img/463887_1_En_17_Fig1_HTML.jpg

图 17-1

我们的项目快照,正在运行

清单 17-2 包含 XML 布局文件的完整代码,因此您可以看到视图对象的属性设置。清单 17-3 显示了 MainActivity 的完整和带注释的代码。

| -什么 | 获取一个 **SharedPreferences** 对象。 | | ➋ | 获取一个 **SharedPreferences。编辑**对象。 | | ➌ | 保存 EditText 的运行时值(txtlastname);让我们使用“姓氏”作为关键字。 | | -你好 | 通过调用 **apply** 而不是 **commit 来异步保存数据。** | | ➎ | 我们现在在“加载”按钮监听器里面。让我们获取“lastname”键的值,并将其保存到一个临时变量中。 | | ➏ | 串联姓氏和名字变量 | | -好的 | 将 TextView (txtoutput)的**文本**属性设置为串联的 lastname 和 firstname | | -好的 | 在 **onResume** 回调中,我们初始化了 txtlastname、txtfirstname 和 txtoutput 的文本属性。我们还设置了文本字段的提示属性。 |
import android.content.Context
import android.content.SharedPreferences
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val pref = getPreferences(Context.MODE_PRIVATE)                 ➊

    btnsave.setOnClickListener {
      val editor = pref.edit()                                      ➋

      editor.putString("lastname", txtlastname.text.toString())     ➌
      editor.putString("firstname", txtfirstname.text.toString())
      editor.apply()                                                ➍

      Toast.makeText(this, "Saved data", Toast.LENGTH_LONG).show()
    }

    btnload.setOnClickListener {
      val mlastname = pref.getString("lastname", "")                ➎
      val mfirstname = pref.getString("firstname", "")
      val moutput = "$mfirstname $mlastname"                        ➏

      txtoutput.text = moutput                                      ➐
    }
  }

  override fun onResume() {                                         ➑
    super.onResume()

    txtfirstname.setText("")

    txtlastname.setText("")
    txtfirstname.setHint("first name")
    txtlastname.setHint("last name")

    txtoutput.setText("")
  }
}

Listing 17-3MainActivity, Annotated

<?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">

  <EditText
    android:id="@+id/txtfirstname"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="16dp"
    android:layout_marginTop="36dp"
    android:ems="10"
    android:inputType="textPersonName"
    android:text="Name"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

  <EditText
    android:id="@+id/txtlastname"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="16dp"
    android:layout_marginTop="16dp"
    android:ems="10"
    android:inputType="textPersonName"
    android:text="Name"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/txtfirstname" />

  <TextView
    android:id="@+id/txtoutput"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="183dp"
    android:layout_marginStart="16dp"
    android:text="TextView"
    android:textSize="36sp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent" />

  <Button
    android:id="@+id/btnsave"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="16dp"
    android:text="save"
    app:layout_constraintBaseline_toBaselineOf="@+id/btnload"
    app:layout_constraintStart_toStartOf="parent" />

  <Button
    android:id="@+id/btnload"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="11dp"
    android:layout_marginTop="27dp"
    android:text="load"
    app:layout_constraintEnd_toEndOf="@+id/txtlastname"
    app:layout_constraintTop_toBottomOf="@+id/txtlastname" />
</android.support.constraint.ConstraintLayout>

Listing 17-2/app/res/layout/activity_main.xml

Android 将创建一个 XML 文件来存储该首选项,它将以创建它的活动命名;在我们的例子中,它是主要的活动。

如果你想检查文件,可以使用设备文件浏览器(以前叫 Android 设备监视器)下载。进入主菜单栏,然后查看工具窗口设备文件浏览器。你应该会看到一个类似图 17-2 的屏幕。

img/463887_1_En_17_Fig2_HTML.jpg

图 17-2

设备文件资源管理器中的 MainActivity.xml 文件

接下来,深入到数据数据fullyqualifiednameofproject(在我的例子中是 net . working dev . ch17 preferences;代入自己的项目名称);然后,进一步向下钻取到shared _ prefsmain activity . XML,如图 17-2 所示。

如果双击 MainActivity.xml 文件,Android Studio 将在主编辑器中显示其内容。或者,您也可以将其下载到您的电脑上。使用 MainActivity.xml 上的上下文相关菜单(右键单击),如图 17-3 所示,然后“另存为”然后,您可以用程序编辑器打开 XML 文件。

img/463887_1_En_17_Fig3_HTML.jpg

图 17-3

将 XML 文件保存到计算机

清单 17-4 显示了 MainActivity.xml 首选项文件的内容。

<?xml version='1.0' encoding='utf-8' standalone="yes" ?>
<map>
    <string name="lastname">hagos</string>
    <string name="firstname">ted</string>
</map>

Listing 17-4Contents of MainActivity.xml

请记住,我们在这里创建的首选项文件只能由 MainActivity 类访问。如果您需要与应用中的其他活动共享首选项文件,您需要创建一个应用级别的首选项。

在活动之间共享数据

要使一个首选项文件对 app 中的所有活动都可用,我们只需要在代码中做一点小小的修改。

| -什么 | **packageName** 实际上是对 **getPackageName()** 的调用。我们只是在这行中构造一个文件名。 | | ➋ | **这是我们唯一需要做的改变**;不要调用 **getPreferences** ,让我们使用 **getSharedPreferences** 。这个函数接受两个参数。你已经知道了第二个,很容易猜到第一个参数是干什么的。第一个参数指定首选项文件的文件名。 |
val filename = "$packageName TESTFILE"                  ➊
val pref = getSharedPreferences(filename, Context.MODE_PRIVATE)   ➋
val editor = pref.edit()

editor.putString("lastname", "Breslav")
editor.putString("firstname", "Andrey")
editor.apply()

Listing 17-5How to Create an Application Level Preferences File

实际上, getPreferences (我们上一节的例子)只是对 getSharedPreferences 的包装调用,前者只是将当前活动的名称作为第一个参数传递给后者。

要从共享首选项文件中检索数据,再次使用 getSharedPreferences ,指定要读取哪个文件,然后使用 getString 方法,如清单 17-6 所示。

| -什么 | 获取 SharedPreferences 对象。通过将首选项文件作为第一个参数传递来指定其名称。 | | ➋ | 第一个参数是关键;它是要检索的首选项的名称。第二个参数是默认值,以防键不存在。 |
val pref = getSharedPreferences("$packageName TESTFILE", Context.MODE_PRIVATE)  ➊

val mlastname = pref.getString("lastname", "")  ➋
val mfirstname = pref.getString("firstname", "")

Listing 17-6How to Read From an Application Preferences File

让我们为此做另一个小演示项目。表 17-2 显示了项目详情。

表 17-2

项目详细信息

|

项目详细信息

|

价值

|
| --- | --- |
| 应用名称 | CH17SharedPreferences(共享首选项) |
| 公司域 | 使用您的网站名称 |
| Kotlin 支架 | 是 |
| 波形因数 | 仅限手机和平板电脑 |
| 最低 SDK | API 23 棉花糖 |
| 活动类型 | 空的 |
| 活动名称 | 主要活动 |
| 布局名称 | 活动 _ 主要 |

我们想做的是:

  1. 让用户输入他的姓和名;为此,我们将使用两个版本。

  2. 当用户点击“Go to 2 nd Activity”按钮时,我们将创建一个启动“SecondActivity”的意图

  3. 在 MainActivity 进入暂停状态之前,我们将把姓氏和名字数据保存到指定的首选项文件中。

  4. 当 SecondActivity 进入用户视图时,我们将在 TextView 中显示“Click LOAD DATA”提示。

  5. 当单击“加载数据”按钮时,我们将检索首选项文件,并将其显示为 TextView 的文本属性。

图 17-4 向我们展示了我们应用的基本故事板。

img/463887_1_En_17_Fig4_HTML.png

图 17-4

我们的项目快照,正在运行

清单 17-7 和 17-8 显示了 activity_main.xml 和 activity_second.xml 的完整代码,因此您可以看到视图对象的属性。

<?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=".SecondActivity"
  tools:layout_editor_absoluteY="81dp">

  <Button
    android:id="@+id/btnloaddata"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="34dp"
    android:layout_marginTop="33dp"
    android:text="Load data "
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/txtoutput" />

  <TextView
    android:id="@+id/txtoutput"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="34dp"
    android:layout_marginTop="87dp"
    android:text="TextView"
    android:textSize="30sp"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

Listing 17-8/app/res/activity_second.xml

<?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"
  tools:layout_editor_absoluteY="81dp">

  <EditText
    android:id="@+id/txtlastname"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="16dp"
    android:layout_marginTop="40dp"
    android:ems="10"
    android:inputType="textPersonName"
    android:text="Name"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

  <EditText
    android:id="@+id/txtfirstname"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="16dp"
    android:layout_marginTop="15dp"
    android:ems="10"
    android:inputType="textPersonName"
    android:text="Name"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/txtlastname" />

  <Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="24dp"
    android:layout_marginTop="57dp"
    android:text="Go to 2nd Activity"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/txtfirstname" />

</android.support.constraint.ConstraintLayout>

Listing 17-7/app/res/layout/activity_main.xml

这就解决了布局问题。在 MainActivity 中,我们需要做的事情如下:

  1. 接受来自用户的一些输入,姓氏和名字。

  2. 单击按钮时,使用明确的意图启动 SecondActivity。

  3. 在 MainActivity 进入“暂停”状态之前,让我们保存首选项文件。

清单 17-9 显示了 MainActivity 的完整和带注释的代码。

| -什么 | 我们正在创建一个将启动 SecondActivity 的明确意图。我们不会在这里保存首选项文件——我们将在稍后的 **onPause** 回调中保存。 | | ➋ | 让我们从这里调用 **saveData** 函数。onPause 函数会在 MainActivity 从用户视野中消失之前被 Android 运行时调用,最终进入“暂停”状态。 | | ➌ | **saveData** 函数是我们实际保存首选项文件的地方。这些代码你都见过了,我们就不再注释了。 | | -你好 | 一条简单的消息告诉用户我们已经保存了数据 | | ➎ | Android 运行时将在 MainActivity 再次对用户完全可见之前调用 **onResume** 函数,如果它来自“暂停”状态。我认为最好在这里重新初始化所有的 UI 元素。 |
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    button.setOnClickListener {
      val intent = Intent(this@MainActivity, SecondActivity::class.java) ➊
      startActivity(intent)
    }
  }

  override fun onPause() {
    super.onPause()
    saveData()     ➋
  }

  private fun saveData() {   ➌

    val filename = "$packageName TESTFILE"
    val pref = getSharedPreferences(filename, Context.MODE_PRIVATE)
    val edit = pref.edit()

    edit.putString("lastname", txtlastname.text.toString())
    edit.putString("firstname", txtfirstname.text.toString())
    edit.apply()

    Toast.makeText(this, "Saved data", Toast.LENGTH_LONG).show() ➍
  }

  override fun onResume() { ➎
    super.onResume()

    txtfirstname.setText("")
    txtlastname.setText("")
    txtfirstname.setHint("first name")
    txtlastname.setHint("last name")
  }

}

Listing 17-9MainActivity, Annotated

这就是我们在主活动中需要做的一切。在 SecondActivity 中,我们需要在单击按钮时读取指定的首选项文件。清单 17-10 显示了 SecondActivity 的完整和带注释的代码。

| -什么 | 当按钮被单击时,让我们读取指定的首选项文件。 | | ➋ | 让我们提取姓氏(以及名字)。 | | ➌ | 连接姓氏和名字数据,并将其显示为 TextView 的文本属性。 |
import android.content.Context
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_second.*

class SecondActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_second)

    btnloaddata.setOnClickListener {
    val filename = "$packageName TESTFILE"
    val pref = getSharedPreferences(filename, Context.MODE_PRIVATE) ➊

      val mlastname = pref.getString("lastname", "") ➋
      val mfirstname = pref.getString("firstname", "")

      txtoutput.text = "$mfirstname $mlastname " ➌
    }
  }

  override fun onResume() {
    super.onResume()

    txtoutput.text = "Click 'LOAD DATA'"
  }
}

Listing 17-10
SecondActivity

这应该可以让你开始使用 SharedPreferences。在我们结束这一章之前,我想留给你一些关于共享优先权的信息。编辑对象。你已经知道是提交应用函数负责实际保存文件。它还具有其他功能,如清除和删除。他们是这样做的:

  • 删除(字符串参数) 这个调用删除一个命名的偏好。String 参数代表键。因此,像remove("lastname")这样的调用将从首选项文件中删除 lastname 键。

  • 清() 删除首选项文件中的所有键。

我将让您来试验这两个编辑器功能。

章节总结

  • Android 有几种持久化数据的方法。它的范围从简单的机制 SharedPreferences 一直到健壮的和一些更复杂的 ContentProviders 和 HTTP 数据库,比如 FireBase。

  • SharedPreferences 使用类似字典或地图的习惯用法。它以键值对的形式存储数据。

  • 您可以将偏好设置文件设为某个活动的专用文件,也可以将其提供给应用中的所有活动。

在下一章,我们将会看到另一种将数据保存到文件中的方法;但是,它不会局限于基本类型。您将学习如何在没有强制结构(如键值对)的情况下使用文件。

十八、内部存储

我们将介绍的内容:

  • Android 的文件 I/O 介绍

  • 内部存储与外部存储

  • 如何使用内部存储

在前一章中,我们学习了如何使用首选项文件。SharedPreferences 使用类似字典的结构,可以用键值格式保存数据行;但是你只能在里面保存基本类型。当您需要处理不限于键值对和基本类型的文件结构时,您可以使用 Java I/O(输入/输出)中良好的 ole 文件类。这就是本章的主题。

文件存储概述

当您需要处理视频、音频、json 或纯文本文件时,您可以使用 Java 文件 I/O 来处理本地文件。您将使用相同的文件InputStream、OutputWriter 以及 Java 中的其他 I/O 类——如果您以前使用过它们的话。Android 的不同之处在于你保存它们的位置。在 Java 桌面应用中,你可以把你的文件放在任何你想放的地方。安卓就不是这样了。就像在 Java web 应用中一样,Android 应用不能在任何地方自由地创建和读取文件。在某些地方,您的应用具有读写权限。

如果你以前没有使用过 Java I/O 也不用担心,我们不会使用任何难以遵循的代码。我们将使用的所有 I/O 例程都在初学者的能力范围之内。

内部和外部存储

Android 区分内部和外部存储。内部存储指的是闪存驱动器中由所有已安装的应用共享的部分。外部存储指的是用户可以安装的存储空间——通常是 sd 卡,但不是必须的。只要它能被用户安装,它可以是任何东西;它甚至可以是内部闪存驱动器的一部分。

每个选项都有利弊,所以你需要考虑你的应用的需求和每个选项的限制。下面的列表显示了一些利弊。

内部存储

  • 内存始终可供您的应用使用。不存在用户卸载 sd 卡或任何设备的危险。它保证会一直在那里。

  • 存储空间的大小将小于外部存储,因为您的应用将只分配到一部分闪存存储空间,该存储空间由所有其他应用共享。在早期版本的 Android 中,这是一个问题,但现在不那么担心了。根据 Android 兼容性定义,从 Android 6.0 开始,Android 手机或平板电脑必须为用户空间保留至少 1.5 GB 的非易失性空间(/data 分区)。对于大多数应用来说,这个空间应该足够了。你可以在这里阅读兼容性定义 https://bit.ly/android6compatibilitydefinition

  • 当你的应用在此空间中创建文件时,只有你的应用可以访问这些文件。除非手机是 root 的,但大多数用户不会 root 他们的手机,所以一般来说,这不是什么大问题。

  • 卸载你的应用时,它创建的所有文件都将被删除。

外部存储器

  • 它通常比内部存储空间更大;但是

  • 它可能并不总是可用(例如,当用户移除 SD 卡或将其安装为 USB 驱动器时。

  • 这里的所有文件对所有应用和用户都是可见的。任何人和任何应用都可以在这里创建和保存文件。他们还可以删除文件。

  • 当一个 app 在这个空间创建了一个文件,它可以比 app 活得更久;我的意思是,当你卸载应用时,文件不会被删除。

缓存目录

无论您选择内部存储还是外部存储,您可能仍然需要在文件位置上再做一个决定。你可以把你的文件放在一个缓存目录或更永久的地方。如果需要空间,缓存目录中的文件可能会被 Android 操作系统或第三方应用回收。所有不在缓存目录中的文件都非常安全,除非您手动删除它们。在本章中,我们不会使用缓存目录或外部存储。我们将只使用内部存储,并将文件放在标准位置。

如何使用内部存储

如前所述,在 Android 中使用文件存储就像在 Java I/O 中使用普通的类一样。有几个选项可以使用,如 openFileInput()openFileOutput() 或其他一些可以使用 InputStreamsOutputStreams 的方法。您只需要记住,这些调用不会让您指定文件路径。你可以只提供文件名,如果你不介意的话,继续使用它们——实际上这就是我们将在本章中使用的。另一方面,如果您需要更大的灵活性,您可以使用getfiledir()getCacheDir() 来获取一个指向文件位置根目录的 File 对象——如果您想要使用内部存储的缓存目录,请使用 getCacheDir() 。当你有一个文件对象时,你可以从那里创建你自己的目录和文件结构。

这是 Android 文件存储领域的一般情况。同样,在本章中,我们将只使用标准位置的内部存储(不是缓存)。

写入文件需要几个简单的步骤。您需要:

  1. 决定文件名

  2. 获取文件输出流对象

  3. 将内容转换为 ByteArray

  4. 使用 FileOutputStream 写入 ByteArray

  5. 不要忘记关闭文件

清单 18-1 向我们展示了它在代码中的样子。

| -什么 | **openFileOutput** 返回一个 FileOutputStream 我们需要这个对象,这样我们就可以写入文件。调用的第一个参数是您想要创建的文件的名称。第二个参数是上下文模式;你已经从上一章知道了这一点。我们使用 MODE_PRIVATE 是因为我们希望文件对应用是私有的。 | | ➋ | **使用**扩展名意味着我不必显式或手动关闭文件。一旦我们使用完它,Android 运行时会为我们关闭它。考虑到许多开发人员忘记关闭文件,这是非常方便的。在应用终止前保持文件句柄打开会导致内存泄漏。 **use** 扩展相当于 Java 的 **try-with-resources。** | | ➌ | **write** 方法需要一个 ByteArray。因此,我们需要将 Editable(EditText 的数据类型)转换为 String,然后将其转换为 ByteArray。 |
val filename = "ourfile.txt"
val out = openFileOutput(filename, Context.MODE_PRIVATE) ➊
out.use { ➋
  out.write(txtinput.text.toString().toByteArray()) ➌
}

Listing 18-1How to Save to a File

从文件中读取比写入文件涉及更多的步骤。您通常需要执行以下操作:

  1. 获取文件输入流

  2. 从流中读取,一次一个字节

  3. 坚持读下去,直到没什么可读的了。当你到达文件末尾时,你会知道你所读取的最后一个字节的值是否为 -1 。到时候就该收手了。

  4. 当您到达文件末尾时,您需要将从流中获取的字节存储到一个临时容器中。StringBuilder 或 StringBuffer 应该可以做到这一点。因为字符串是不可变的,所以使用加号运算符构建字符串对象既浪费又低效。每次使用加号运算符时,它都会创建一个新的 String 对象;如果您的文件包含 2000 个字符,您将创建 2000 个 String 对象。

    如果你正在阅读一个文本文件,情况就是这样。如果您正在读取音频或视频文件等其他内容,您将使用不同的数据结构。

  5. 当你到达文件的末尾时,停止阅读。用你读过的东西做你需要做的事情,不要忘记关闭文件。

清单 18-2 向我们展示了这在代码中的样子。

| -什么 | **openFileInput** 返回一个 FileInputStream 这是我们需要的对象,所以可以从文件中读取。它唯一需要的参数是要读取的文件的名称。 | | ➋ | 我们不可能一下子读完整个文件。我们会一段一段地读。当我们得到一些块时,我们将把它们存储在 StringBuilder 对象中。 | | ➌ | **read** 方法从输入流中读取一个字节的数据,并将其作为整数返回。我们需要一次从流中读取一个字节,直到到达文件结尾(EOF)标记。 | | -你好 | 当流中没有更多的字节要读取时,EOF 被标记为 **-1** 。我们将用它作为 **while** 循环的条件。直到 **bytes_read** 不等于 **-1** 为止,继续读取。 | | ➎ | **read** 方法返回一个 int 它是文件中每个字母的 ASCII 值,作为整数返回。在将它放入 StringBuilder 之前,我们必须将其转换成一个**字符**。 | | ➏ | 如果我们还没到 EOF,让我们读另一个字节。 | | -好的 | 当我们用完要读取的字节时,我们将退出循环并打印 StringBuilder 的内容。 |
val filename = "ourfile.txt"
val input = openFileInput(filename) ➊

input.use {
  var buffer = StringBuilder() ➋
  var bytes_read = input.read() ➌
  while(bytes_read != -1) { ➍
    buffer.append(bytes_read.toChar()) ➎
    bytes_read = input.read() ➏
  }
  println(buffer.toString()) ➐
}

Listing 18-2How to Read From a File

当然,我们会做一些小的演示项目。它巩固了我们的学习。表 18-1 显示了演示项目的详细信息。

表 18-1

项目详细信息

|

项目详细信息

|

价值

|
| --- | --- |
| 应用名称 | ch18 内部存储 |
| 公司域 | 使用您的网站名称 |
| Kotlin 支架 | 是 |
| 波形因数 | 仅限手机和平板电脑 |
| 最低 SDK | API 23 棉花糖 |
| 活动类型 | 空的 |
| 活动名称 | 主要活动 |
| 布局名称 | 活动 _ 主要 |

我们想做的是:

  1. 我们将设置两个活动:MainActivity 和 SecondActivity。

  2. 在 MainActivity 中,用户可以在多行编辑文本中自由键入文本。

  3. 当点击按钮“2 Activity”时,我们将启动一个打开 SecondActivity 的明确意图。

  4. 但是在我们离开 MainActivity 之前,我们将创建一个文件并将 EditText 的内容保存到该文件中。这个调用并不十分昂贵,但是我们将在后台线程中运行这个代码,因为它是一个 I/O 调用。你永远无法确定一个 I/O 调用会多于还是少于 16 ms,所以还是小心为妙。

  5. 在 SecondActivity 中,一旦它对用户可见,我们将读取文件的内容(我们刚刚保存在 MainActivity 中的那个)并使用多行 TextEdit 显示给用户。

  6. 仍然在 SecondActivity 中,当用户单击按钮“1 st Activity”时,我们将启动一个返回主 Activity 的明确意图。

  7. 在 MainActivity 的 onResume 中,我们将尝试读取文件并使其可用于编辑。

图 18-1 显示了我们 app 的两个屏幕。

img/463887_1_En_18_Fig1_HTML.png

图 18-1

该应用在运行时

清单 18-3 和 18-4 显示了 activity_main.xml 的完整代码(MainActivity 的 UI。Kt)和 activity _ second . XML(second activity 的 UI。Kt)。

<?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">

  <EditText
    android:id="@+id/txtinput"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginTop="34dp"
    android:ems="10"
    android:inputType="textMultiLine"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

  <Button
    android:id="@+id/btnsecondactivity"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="16dp"
    android:layout_marginTop="8dp"
    android:text="2nd Activity"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/txtinput"
    app:layout_constraintVertical_bias="0.963"
    tools:layout_editor_absoluteX="16dp" />
</android.support.constraint.ConstraintLayout>

Listing 18-3/app/res/layout/activity_main.xml

接下来是 activity_second 的 xml 定义。

<?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=".SecondActivity">

  <Button
    android:id="@+id/btnmainactivity"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="18dp"
    android:layout_marginStart="16dp"
    android:text="1st activity"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent" />

  <TextView
    android:id="@+id/txtoutput"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginTop="29dp"
    android:inputType="textMultiLine"
    android:text="TextView"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

Listing 18-4/app/res/layout/activity_second.xml

主要活动有几个方面。在我们看完整的代码之前,让我们先看看它的重要部分。

当 MainActivity 打开时,我们运行一些代码来检查“ourfile.txt”(文件的名称)是否已经存在。如果是,我们将读取它并在 EditText 中显示内容,这样用户就可以编辑它。这段代码位于 onResume() 回调函数中,这是放置代码的好地方,因为一旦用户看到活动,运行时就会调用它。

清单 18-5 显示了 onResume 回调和 loadData 函数。我只注释了三点——从哪里调用 loadData 以及与文件输入/输出相关的所有代码的开始/结束行。你已经熟悉了其余的代码,因为它们已经在本章的前面和/或前面的章节中解释过了。

代码很简单,但是对于初学者来说可能在结构上有挑战性。所以,还是一步一步来吧。

| -什么 | 一旦用户看到活动,我们就调用**loadData()**;这发生在 **onResume** 回调中。 | | ➋ | I/O 代码的开始 | | ➌ | I/O 代码结束。其余的是锅炉板穿线和例外。 |
val Log = Logger.getLogger(MainActivity::class.java.name)

override fun onResume() {
  super.onResume()
  loadData()                                    ➊
}

private fun loadData() {

  val filename = "ourfile.txt"
  Thread(Runnable{
    try {
      val input = openFileInput(filename)       ➋
      input.use {
        var buffer = StringBuilder()
        var bytes_read = input.read()

        while(bytes_read != -1) {
          buffer.append(bytes_read.toChar())
          bytes_read = input.read()
        }
        runOnUiThread(Runnable{
          txtinput.setText(buffer.toString())
        })
      }                                         ➌
    }

    catch(fnfe:FileNotFoundException) {
      Log.warning("file not found, occurs only once")
    }
    catch(ioe: IOException) {
      Log.warning("IOException : $ioe")
    }
  }).start()
}

Listing 18-5
loadData Function

重点关注清单 18-5 中➋点和➌点之间的代码。它们是唯一对读取文件重要的文件。线程Runnablerunnonuithreadtry、catch 都是内务代码。他们在那里是因为我们试图进行防御性编码。我们在后台运行,因为 I/O 代码可能需要一些时间来完成。我们使用 try-catch 块是因为 I/O 代码可能会抛出异常。我们使用了 runOnUiThread ,因为当我们在后台线程中时,我们不能向 UI 写任何东西。这些就是结构杂技的原因。

清单 18-6 再次显示了 loadData 函数,但是这次没有 I/O 代码。你只能看到管家代码。

| -什么 | 在这里运行你的后台代码。我们所有的文件输入/输出代码都在这里。 | | ➋ | 这是你写代码的地方,这样**可以抛出异常**。Java I/O 调用会抛出异常——这就是为什么我们需要把它们放在这里。 | | ➌ | 如果你需要更新 UI,你必须回到 UI 线程。在后台线程中,您不能更改用户界面。 | | -你好 | 如果确实发生了异常,请在这里做您需要做的任何事情,以便应用可以恢复。至少,在这里记录一些东西,这样当您以后查看日志时就可以看到发生了什么错误。显式处理异常(像这样)的好处是,如果在运行时遇到不利的事情,应用不会崩溃。这样,你就有机会优雅地恢复。 | | ➎ | **start** 方法将螺纹踢入高速档。它得到了线程,嗯,开始。 |
Thread(Runnable {
  ...                         ➊

  try {
    ...                       ➋

    runOnUiThread(Runnable {
      ...                     ➌
    })
  }
  catch(ioe:IOException) {
    ...                       ➍
  }

}).start()                    ➎

Listing 18-6loadData Without the I/O Codes

从➊到➎包含了所有在后台线程中运行的东西。这个过度扩展语句的基本结构是这样的:所有的 I/O 代码和 try-catch 块都被写在省略号的位置。

接下来,当应用对用户完全可见时,它会等待输入。用户可以在多行编辑文本中添加文本。如果用户点击“2 Activity”按钮,我们将启动带有明确意图的 SecondActivity。MainActivity 从“运行中”转换到“暂停”状态,但在此之前,运行时将调用 MainActivity 的 onPause 方法。我们将在这里编写代码,将数据保存到文件中。清单 18-7 显示了带注释的 saveData 函数。

| -什么 | 我们将在后台线程中运行,因为这是一个 I/O 调用。 | | ➋ | 让我们打开一个文件进行输入。这给了我们一个**文件输入流**。将文件名作为第一个参数传递,将上下文模式作为第二个参数传递。 | | ➌ | 现在我们可以写入文件了。请记住,您只能在 FileInputStream 对象中写入字节数组,因此您必须将 EditText 的运行时值转换为 ByteArray。 | | -你好 | 现在我们必须回到 UI 线程,即使我们只显示一条祝酒词。 |
private fun saveData() {
  val filename = "ourfile.txt"
  Thread(Runnable {                                            ➊
    try {
      val out = openFileOutput(filename, Context.MODE_PRIVATE) ➋
      out.use {
        out.write(txtinput.text.toString().toByteArray())      ➌
      }
      runOnUiThread(Runnable {                                 ➍
        Toast.makeText(this, “Saved", Toast.LENGTH_LONG).show()
      })
    }
    catch(ioe:IOException) {
      Log.warning("Error while saving ${filename} : ${ioe}")
    }
  }).start()
}

override fun onPause() {
  super.onPause()
  saveData()
}

Listing 18-7annotated saveData function

希望这能澄清主要活动的结构。SecondActivity 要简单得多,但它也遵循与 MainActivity 相同的结构流程。清单 18-8 和 18-9 分别显示了 MainActivity 和 SecondActivity 的完整和带注释的代码。

| -什么 | 当单击按钮时,我们将简单地启动一个显式的意图来打开 SecondActivity。这里我们不做任何 I/O 代码。 | | ➋ | 函数 **saveData** 包含将 EditText 的运行时内容写入文件的所有 I/O 代码。 | | ➌ | 在 MainActivity 进入“暂停”状态并从用户视野中消失之前,运行时会调用**on pause;**这就是我们称之为 **saveData 的地方。** | | -你好 | 当 MainActivity 第一次出现在用户的视图中时,运行时调用 **onResume** 。这里我们将调用 **loadData** 函数。它将读取文件并在 TextView 对象中显示其内容。 |
import android.content.Context
import android.content.Intent
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*
import java.io.FileNotFoundException
import java.io.IOException
import java.util.logging.Logger

class MainActivity : AppCompatActivity() {

  val Log = Logger.getLogger(MainActivity::class.java.name)
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    btnsecondactivity.setOnClickListener {
      startActivity(Intent(this, SecondActivity::class.java))     ➊
    }
  }

  private fun saveData() {                                        ➋
    val filename = "ourfile.txt"
    Thread(Runnable {
      try {
        val out = openFileOutput(filename, Context.MODE_PRIVATE)
        out.use {
          out.write(txtinput.text.toString().toByteArray())
        }
        runOnUiThread(Runnable {
          Toast.makeText(this, “Saved", Toast.LENGTH_LONG).show()
        })
      }
      catch(ioe:IOException) {
        Log.warning("Error while saving ${filename} : ${ioe}")
      }
    }).start()
  }

  override fun onPause() {                                        ➌
    super.onPause()
    saveData()
  }

  override fun onResume() {                                       ➍
    super.onResume()
    loadData()
  }

  private fun loadData() {

    val filename = "ourfile.txt"
    Thread(Runnable{
      try {
        val input = openFileInput(filename)
        input.use {
          var buffer = StringBuilder()
          var bytes_read = input.read()

          while(bytes_read != -1) {
            buffer.append(bytes_read.toChar())
            bytes_read = input.read()
          }
          runOnUiThread(Runnable{
            txtinput.setText(buffer.toString())
          })
        }
      }
      catch(fnfe:FileNotFoundException) {
        Log.warning("file not found, occurs only once")
      }
      catch(ioe: IOException) {
        Log.warning("IOException : $ioe")
      }
    }).start()
  }
}

Listing 18-8MainActivity, Annotated

让我们继续第二项活动。

| -什么 | 当 SecondActivity 出现在用户视图中时,我们将调用 **loadData。** | | ➋ | 你以前见过这个代码。这与 MainActivity 中的代码相同。它读取文件并使用 TextView 对象显示其内容。我想我们可以重构代码,在某个地方抽象这个函数,这样我们就可以遵循 DRY(不要重复自己)原则,但这意味着我们需要解释更多的代码和概念。为了可读性,我在这里违反了 DRY 原则。请记住,不要在生产代码中这样做。 | | ➌ | 当点击“第一活动”按钮时,我们返回到主活动。我们也可以在这里调用 **finish()** ,但是我不想完全破坏 SecondActivity,所以我使用了一个显式的意图来返回 MainActivity。 |
import android.content.Intent
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_second.*

class SecondActivity : AppCompatActivity() {

  override fun onResume() {                   ➊
    super.onResume()
    loadData()
  }

  private fun loadData() {                    ➋
    val filename = "ourfile.txt"
    Thread(Runnable {
      val input = openFileInput(filename)
      input.use {
        var buffer = StringBuilder()
        var bytes_read = input.read()
        while(bytes_read != -1) {
          buffer.append(bytes_read.toChar())
          bytes_read = input.read()
        }
        runOnUiThread(Runnable{
          txtoutput.setText(buffer.toString())
        })
      }

    }).start()
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_second)

    btnmainactivity.setOnClickListener {          ➌
      startActivity(Intent(this, MainActivity::class.java))
    }
  }
}

Listing 18-9SecondActivity, Annotated

这就结束了这一章。这一章我已经说过几次了,但还是值得重复一遍。I/O 代码并不是最难的——是锅炉板代码让程序看起来比实际更复杂。但是你不能逃避,你需要线程和异常处理代码来观察你的代码中良好的内务处理。

章节总结

  • 当您对存储的需求超出了简单的键值对结构和基本数据类型时,请使用 Java I/O 类。

  • 您可以将文件存储在始终可用但有限的内部存储器中,或者存储在更大但可能会被卸载的外部存储器中。

  • 即使您认为 I/O 调用将少于 16 ms,也要在后台线程中运行代码。你永远不知道在 I/O 调用中会发生什么。

  • Java I/O 调用抛出异常;适当地处理它们。

在下一章,我们将看看 Android 应用的另一个重要组成部分:广播接收器。他们实际上做了你认为他们做的事情——接收广播。我们将研究一些类型的广播,像往常一样,我们将做一个小的演示项目。

十九、广播接收器

我们将介绍的内容:

  • 广播接收器简介

  • 自定义和系统广播

  • 清单和上下文注册的接收器

Android 的应用模型在许多方面都是独一无二的,但让它脱颖而出的是它允许你使用不是你自己制作的其他应用的功能来创建一个应用——我不仅仅指库,我指的是完整的应用。你已经了解了意图——它们是什么,它们能做什么。我们已经了解了如何使用 Intents 来启动其他组件,我们甚至使用它在组件之间传递数据。

还有一种方法可以使用意图。我们可以用它向所有组件发送广播。广播是由 Android 运行时或其他应用(包括您自己的应用)发送的意图,以便每个应用或组件都能听到它。大多数应用会忽略广播,但你可以让你的应用听它。你可以收听这条消息,以便对广播做出回应。这就是本章的主题。

广播接收器简介

因此,我们可以启动发送(广播)到所有应用和组件的意图。但是这有什么好处呢?要回答这个问题,我们需要回忆一下,谈谈 Android 在互操作性和可插拔性方面的哲学。还记得在第十二章中,我们第一次谈到意图吗?我们看了图 19-1 中的图片。

img/463887_1_En_19_Fig1_HTML.png

图 19-1

用户如何与“通讯录”应用交互

用户不在乎使用哪个应用来发送电子邮件、短信或打电话。当用户点击电子邮件时,它会启动一个隐含的意图,即“嘿,我想发一封电子邮件。谁感兴趣?”设备中的每个应用都会听到这个消息,但只有那些收听的人才能做出回应。这就是 BroadcastReceivers 的全部理念——向所有人发布一条消息,如果一些应用订阅了这条消息,他们就可以做出回应。它使用一个发布-订阅模型。

系统广播与自定义广播

意向广播可以由操作系统(系统广播)或应用(自定义广播)发送。每当发生有趣的事情时(例如,当 WiFi 打开[或关闭]时,当电池电量下降到指定阈值时,插入耳机时,或设备切换到飞行模式时等),操作系统都会发送系统广播。).来自系统的广播动作的一些例子如下:

  • android.app.action.ACTION_PASSWORD_CHANGED

  • android.app.action.ACTION_PASSWORD_EXPIRING

  • android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED

  • android.bluetooth.a2dp.profile.action.PLAYING_STATE_CHANGED

  • android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED

  • android.intent.action.BATTERY_CHANGED

  • android.intent.action.BATTERY_LOW

  • android.intent.action.BATTERY_OKAY

文档中列出了大约 150 多种。你可以在 BROADCAST_ACTIONS 上找到它们。Android SDK 中的 TXT 文件。

另一方面,定制广播是你编造的。这些是您发送的意图,以通知您的应用的一些组件(或其他应用)发生了一些“有趣”的事情(例如,文件已完成下载或您已完成计算素数,等等)。).

清单注册与上下文注册

如果你想做一些事情作为对广播的响应,你需要监听它,为了做到这一点,你需要注册一个接收器。有两种注册方式:通过清单和通过上下文。

清单中注册的接收者看起来像清单 19-1 。

| -什么 | 就像活动一样,需要在清单中声明一个 **BroadcastReceiver** 。你必须在它自己的节点中声明它。像活动声明一样,它需要是**应用的子节点。** | | ➋ | ".MyReceiver”是 BroadcastReceiver 类的名称。因此,假设您的应用中有一个名为 MyReceiver 的类,它继承了 BroadcastReceiver。我们干脆把它写成”。MyReceiver,“就像它上面的活动一样”。主要活动”。完整的形式其实是**net . working dev . ch 19 broadcast receiver something . my receiver**,但是我们可以使用简写形式,因为**包**名称已经在前面声明了;看看清单的第二行,你会找到包裹的完整名称。任何需要在清单中声明的后续类都可以简单地使用缩写形式,如“.我的收件人“或”。主要活动”。 | | ➌ | **意向过滤器**是我们实际注册的方式。我们告诉操作系统我们对事件感兴趣**com . working dev . do something**。如果意向是以广播形式发送的,此应用会对其做出响应。 |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="net.workingdev.ch19broadcastreceiverdosomething">
<application
  android:allowBackup="true"
  android:icon="@mipmap/ic_launcher"
  android:label="@string/app_name"
  android:roundIcon="@mipmap/ic_launcher_round"
  android:supportsRtl="true"
  android:theme="@style/AppTheme">
  <activity android:name=".MainActivity">
    <intent-filter>
      <action android:name="android.intent.action.MAIN" />
      <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
  </activity>
  <receiver                             ➊
    android:name=".MyReceiver"          ➋
    android:enabled="true"
    android:exported="true">
    <intent-filter>                     ➌
      <action android:name="com.workingdev.DOSOMETHING"/>
    </intent-filter>
  </receiver>

</application>

Listing 19-1BroadcastReceiver Declared in AndroidManifest.xml

通过清单注册的接收者不需要为了响应广播而当前正在运行。接收者在清单上注册的事实足以解析意图。

当接收者通过上下文对象以编程方式注册时,它看起来像清单 19-2 。

| -什么 | 这是我们前面看到的``节点的编程等价物。要创建一个 **IntentFilter** 对象,向其构造函数传递一个广播动作。广播操作是您想要订阅的事件。在这种情况下,我们希望在发出动作为**com . working dev . do something**的意向时得到通知;此意图是自定义广播的一个示例,而不是系统广播。 | | ➋ | 使用活动的 **registerReceiver** 方法注册接收者。该方法有两个参数:a.BroadcastReceiver 的实例,以及 b.IntentFilter 的实例 | | ➌ | 当您以编程方式注册接收方时,请确保您也注销了它。这就是我们在这里做的。它在一个 **try-catch** 结构中,因为它可以抛出一个异常。如果你试图注销一个还没有注册的接收者(或者一个已经注销的接收者),运行时将抛出 **IllegalArgumentException** 。我没有为注册部分这样做,因为 **registerReceiver** 不会抛出任何异常,即使你(意外地)不止一次注册了同一个接收者。一旦注册了一个接收者,运行时将忽略任何注册它的进一步尝试。 | | -你好 | 这是 BroadcastReceiver 类的基本定义。 |
val Log = Logger.getLogger(javaClass.name)

override fun onCreate(savedInstanceState: Bundle?) {

  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)

  val action_filter = IntentFilter("com.workingdev.DOSOMETHING") ➊
  val receiver = MyReceiver()

  btnregister.setOnClickListener {
    registerReceiver(receiver, action_filter)  ➋
  }

  btnunregister.setOnClickListener {
    try {
      unregisterReceiver(receiver)            ➌
    }

    catch(iae:IllegalArgumentException) {
      Log.warning("IllegalArgument\n ${iae}")
    }
    catch(e:Exception) {
      Log.warning("IllegalArgument\n ${e}")
    }
  }
}

inner class MyReceiver : BroadcastReceiver() {                     ➍
  override fun onReceive(context: Context?, intent: Intent?) {
    println("got it");
    Toast.makeText(this@MainActivity, "Got it", Toast.LENGTH_LONG).show()
  }
}

Listing 19-2How to Register and Unregister a BroadcastReceiver

以编程方式注册的接收器只能在应用(用于注册接收器)仍在运行时响应广播。

广播接收器基础

创建广播接收器时,需要遵循几个步骤。它们是:

  1. 决定您想收听哪个 的广播节目。你想听系统广播还是自定义广播?如果您希望促进应用组件之间的一些消息传递,通常会使用自定义广播。使用 BroadcastReceiver 的一个用例是,当您使用 DownloadManager 系统服务下载大文件时,该服务会在下载完成时发出广播—您可能希望听到广播,以便在下载后立即采取行动。

  2. 决定如何注册接收者,通过上下文还是通过清单?您可以通过任何一种方式(清单或上下文)收听自定义广播,但是有些广播操作是受限制的,您不能通过清单注册来收听它们。我们将很快讨论这个问题。

  3. 创建一个继承自 BroadcastReceiver 类的类。

  4. 覆盖并实现新类的 onReceive 方法。当一个广播被发送时,意图过滤器与动作匹配,操作系统将意图解析到你的应用,最终特定的 BroadcastReceiver 类,运行时调用 onReceive 方法。 onReceive 方法是 BroadcastReceiver 类的核心。无论你想在播配的时候做什么,这都是你需要写的地方。

通常,如果您通过 Android 清单或上下文对象注册了 BroadcastReceiver,就可以收听广播。让我们继续一点。前面,我使用了术语“通过上下文注册”和“以编程方式注册”——它们是同一个,意思相同。“通过上下文注册”是指在上下文对象上调用 registerReceiver 方法。所以声明

registerReceiver(receiver, intent_filter)

与语句相同

this.registerReceiver(receiver, intent_filter)

它们都是在当前活动的上下文中调用的——Activity 类实际上继承自 Context 对象,Service 类也是如此。因此,您可以从活动或服务内部调用 registerReceiver 方法。如果你在一个不从上下文继承的类中,你仍然可以通过获取应用的上下文来注册一个接收者。代码看起来像这样:

getApplicationContext().registerReceiver(receiver, intent_filter)   // or
applicationContext.registerReceiver(receiver, intent_filter)

回到清单与上下文注册,有些广播动作不能在清单中注册;但是你可以通过上下文注册它们。一个例子是android.intent.action. TIME_TICK,这是一个受保护的意图,只能由系统发送。它每 60 秒发送一次,你只有通过上下文注册才能收听。

在 Android 的早期版本中,已经有一些广播被限制在清单之外。写这篇文章的时候,Android 9(或者 API 级)出来了。在本书中,我们一直使用 API 级别 23 作为目标,但是您将从阅读每个 Android 版本的行为变化文档中受益。我在下面列出了一些 Android 官方文档的链接。这些文件以这样或那样的方式影响着广播受众。

  • Android 9 (API 28)行为改变http://bit.ly/behaviorchanges9 。谈谈如果我们想瞄准 Android 9,开发者应该知道的 API 的所有变化。这位医生对广播接收器有话要说。

  • 背景执行限制。bit.ly/bgexeclimit。这是关于你的应用在后台运行时能做什么和不能做什么。不要以为因为不在 UI 线程里,就可以到处跑,想干嘛干嘛。这份文件谈到了这些限制;它还谈到了对广播接收器的限制。

  • broadcast receiverexceptions。bit.ly/broadcastexceptions。从 Android 8 开始(继续到 9),除了一些例外,所有隐式广播动作现在都是清单的禁区。这份文件列举了那些被免除的行为。如果您想知道哪些隐式广播动作仍然可以通过清单注册,请阅读本文档。

隐式与显式广播动作

Android 区分了隐式和显式广播动作。它将显式广播定义为只针对一个应用的广播,而不管有多少其他应用在监听它。另一方面,任何注册的应用都可以听到明确的广播。为了我们的目的和使我们的生活更简单,文档告诉我们不要通过清单收听系统广播。从 Android 8 开始,所有隐式广播(除了那些在 http://bit.ly/broadcastexceptions 列出的)都不能被通过清单注册的接收器听到。但是如果您通过上下文注册,您仍然可以收听这些广播操作。

所有这些新限制的主要原因都与性能优化和节能有关。考虑一下:当设备的 WiFi 连接出现问题时,会发送 CONNECTIVITY_ACTION 广播。如果有十几个应用在监听这个广播,它们都会醒来并采取行动。每次 WiFi 掉线重新连上都会这样。请记住,清单注册的接收者不需要活着才能接收广播;事实上,当他们得到广播时,他们会活过来。这种行为会导致大量功耗。如果你的应用在不运行时不需要被告知 WiFi 连接,那么通过上下文来注册更负责任。

演示应用:自定义广播

让我们构建一个小项目,这样您可以自己尝试广播接收器。表 19-1 显示了该项目的详细情况。

表 19-1

项目详细信息

|

项目详细信息

|

价值

|
| --- | --- |
| 应用名称 | ch19 上下文记录 |
| 公司域 | 使用您的网站名称 |
| Kotlin 支架 | 是 |
| 波形因数 | 仅限手机和平板电脑 |
| 最低 SDK | API 23 棉花糖 |
| 活动类型 | 空的 |
| 活动名称 | 主要活动 |
| 布局名称 | 活动 _ 主要 |

想做什么:

  1. 创建一个响应隐式自定义广播的 BroadcastReceiver

  2. 一旦活动对用户可见,我们将立即注册接收者;和

  3. 我们将在活动进入“暂停”状态之前注销接收者。

  4. MainActivity 的 UI 只有一个按钮。当点击该按钮时,它将发送一个自定义的广播意图。

清单 19-3 显示了 UI 的极简代码。

<?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">

  <Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="26dp"
    android:layout_marginTop="43dp"
    android:text="send broadcast"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

Listing 19-3/app/res/layout/activity_main.xml

我们需要添加一个继承自 BroadcastReceiver 的类。方法之一是从主菜单栏文件新建Kotlin 文件/类。或者,我们也可以使用项目工具窗口的 appjava 文件夹中的上下文菜单,如图 19-2 所示。从那里,你可以去其他广播接收器

img/463887_1_En_19_Fig2_HTML.jpg

图 19-2

新广播接收器

你需要填写类的名字。在本例中,我将该类命名为“MyReceiver”

我们不会在接收器中做任何特别的事情。我们将简单地在日志记录器中显示 toast 消息打印内容。清单 19-4 显示了 MyReceiver 的代码。

| -什么 | 当广播意图与接收器匹配时,操作系统调用广播接收器的 **onReceive** 方法。在这里,您应该为接收方编写应用的业务逻辑(例如,保存文件、根据 WiFi 条件路由程序逻辑等)。). |
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast
import java.util.logging.Logger

class MyReceiver : BroadcastReceiver() {

  val Log = Logger.getLogger(javaClass.name)

  override fun onReceive(context: Context, intent: Intent) {  ➊
    Toast.makeText(context, "Got it", Toast.LENGTH_LONG).show()
    Log.info("Got it")
  }
}

Listing 19-4
MyReceiver.java

在主活动中,我们将执行以下操作:

  1. 创建 MyReceiver 的实例。我们只需要做一次。这就是为什么我们将在 onCreate 回调中创建实例。

  2. 每当接收者对用户可见时注册它。我们将把这段代码放在 MainActivity 的 onResume 回调中。

  3. 当用户不再与 MainActivity 交互时,注销接收者。

  4. 当点击该按钮时,我们将发送一个自定义的广播意图。

清单 19-5 显示了 MainActivity 的完整和带注释的代码。

img/463887_1_En_19_Fig3_HTML.jpg

图 19-3

我们的应用,跑步

| -什么 | **receiver** 变量保存了 **MyReceiver** 类的实例(我们的 BroadcastReceiver)。我们将变量声明为属性,因为我们将在 **onResume** 和 **onPause** 方法中引用它。我们使用了 **lateinit** 关键字,因为我们现在还不会定义它。 | | ➋ | 让我们使用一个基本的日志对象。 | | ➌ | 现在我们在 **onCreate** 中,让我们定义 MyReceiver 对象。 | | -你好 | 当点击按钮时,我们想要创建一个广播意图,并将其*动作*设置为 DOSOMETHING。 | | ➎ | 启动意图。 | | ➏ | 我们正在进行 **onResume** 回调。每当 MainActivity 对用户可见时,操作系统都会调用这个方法。这是注册接收者的好地方。我们只想在使用应用时收到通知。 | | -好的 | 我们在**中,因为**,在 MainActivity 进入“暂停”状态并从用户的视野中消失之前,操作系统调用这个方法。这是注销接收者的好地方。我们不想在不使用应用时收到通知。 |
import android.content.Intent
import android.content.IntentFilter
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
import java.util.logging.Logger

class MainActivity : AppCompatActivity() {

  lateinit var receiver:MyReceiver                      ➊
  val Log = Logger.getLogger(javaClass.name)            ➋

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    receiver = MyReceiver()                             ➌

    button.setOnClickListener {
      val intent = Intent("com.workingdev.DOSOMETHING") ➍
      sendBroadcast(intent)                             ➎
    }
  }

  override fun onResume() {                             ➏
    super.onResume()

    val filter = IntentFilter("com.workingdev.DOSOMETHING")
    registerReceiver(receiver, filter)
    Log.info("Registered receiver")
  }

  override fun onPause() {                              ➐
    super.onPause()

    try {
      unregisterReceiver(receiver)
      Log.info("Unregistered receiver")
    }
    catch(iae: IllegalArgumentException) {
      Log.warning(iae.toString())
    }
  }
}

Listing 19-5
MainActivity.java

另一种发送广播意图的方式是通过 Android 调试桥或 adb ,简称。它是一个命令行工具,允许您与设备(物理设备或仿真设备)进行通信。adb 可以做很多事情,比如安装/卸载 apk、显示日志、在设备上运行 Linux 命令、模拟电话呼叫等等。出于我们的目的,我们将使用发送一个广播意图。

adb 在 Android SDK 的平台工具文件夹里。打开命令行窗口,切换到 Android SDK 的目录。如果你忘记了它在哪里,去 Android Studio 的设置 (Windows 和 Linux)或者首选项 (macOS)。对于 Windows 和 Linux,您可以通过按下按键 CTRL + ALT + S ,或者对于 macOS,您可以按下按键 Command +、(逗号)。

在那里,进入外观和行为➤系统设置➤安卓 SDK,如图 19-4 所示。Android SDK 的位置就在那里。

img/463887_1_En_19_Fig4_HTML.jpg

图 19-4

首选项,Android SDK

回到命令行窗口,切换到 Android SDK 文件夹。从那里,切换到平台工具文件夹,然后运行以下命令:

adb shell am broadcast -a com.workingdev.DOSOMETHING

如果您在 macOS 或 Linux 上,您可能需要在命令前面加上点号和正斜杠,就像这样:

./adb shell am broadcast -a com.workingdev.DOSOMETHING

演示应用:系统广播

下一个项目将与上一个项目相似,但我们将收听系统广播。我们将监听系统每 60 秒发出的 ACTION_TIME_TICK。这是一个受保护的意图,所以我们必须在运行时注册接收者。表 19-2 显示了该项目的详细情况。

表 19-2

系统广播的项目详情

|

项目明细

|

|
| --- | --- |
| 应用名称 | ch19 系统广播 |
| 公司域 | 使用您的网站名称 |
| Kotlin 支架 | 是 |
| 波形因数 | 仅限手机和平板电脑 |
| 最低 SDK | API 23 棉花糖 |
| 活动类型 | 空的 |
| 活动名称 | 主要活动 |
| 布局名称 | 活动 _ 主要 |

这个应用非常简单。没有要设置的 UI 元素。这是我们想要做的:

  1. 创建一个 BroadcastReceiver 来侦听 ACTION_TIME_TICK 意图。我们将把它实现为一个内部类——这样做的唯一原因是使代码的表示更加简洁。如果您愿意,您完全可以将 receiver 类实现为独立的类。

  2. 我们希望只有当用户与应用交互时才能收听广播。所以我们将在 MainActivity 的 onResume 回调中注册接收者;我们将在中注销它,因为回调。

  3. 每当收到 ACTION_TIME_TICK 时,我们将简单地使用 Toast 对象向控制台和用户屏幕输出一条消息。

| -什么 | 创建 BroadcastReceiver 的实例 | | ➋ | 创建用于监听 ACTION_TIME_TICK 广播的 **intentfilter** | | ➌ | 在 Resume 上注册**内的接收器;当用户看到应用时,运行时会调用此方法。** | | -你好 | 在应用进入“暂停”状态之前注销接收器。通过这种方式,只要我们的应用在用户的视野内,接收器就只收听广播。当应用不再出现在用户的视图中时,我们不想收到任何广播通知。 | | ➎ | 这是 BroadcastReceiver 的类定义。它是作为内部类实现的,同样有效。这对我们有用,因为我们没有试图在 **onReceive** 回调中做任何实质性的事情。如果程序逻辑过于复杂,BroadcastReceiver 最好在 MainActivity 之外实现。 |
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast

import java.util.logging.Logger

class MainActivity : AppCompatActivity() {

  lateinit var intentfilter:IntentFilter
  lateinit var timereceiver:TimeReceiver
  var current_count = 0

  val Log = Logger.getLogger(javaClass.name)

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    timereceiver = TimeReceiver()                         ➊
    intentfilter = IntentFilter(Intent.ACTION_TIME_TICK)  ➋
  }

  override fun onResume() {
    super.onResume()
    Log.info("App is resuming")
    registerReceiver(timereceiver,intentfilter)           ➌
  }

  override fun onPause() {
    super.onPause()
    Log.info("App is paused")
    try {
      unregisterReceiver(timereceiver)                    ➍
    }
    catch(iae:IllegalArgumentException) {
      Log.warning(iae.toString())
    }
  }

  inner class TimeReceiver : BroadcastReceiver() {        ➎
    override fun onReceive(context: Context?, intent: Intent?) {
      current_count += 1
      var message = "Counter:${current_count}"
      Log.info(message)
      Toast.makeText(this@MainActivity, message, Toast.LENGTH_LONG).show()
    }
  }
}

Listing 19-6
MainActivity

其他说明

BroadcastReceivers 和 Intents 在让解耦的组件互相对话方面做得很有效。如果你想方便应用之间的交流,使用 BroadcastReceivers 是很好的;它们是进程间通信的良好解决方案。但是,如果你自己的应用的组件之间的通信受到限制,广播接收器是一个昂贵的解决方案。用全球广播不合适。

如果您只是想简化应用组件之间的消息传递,您可能需要考虑一个 LocalBroadcastManager 类。当您使用此功能时,广播数据不会离开您的应用。它不是进程间的。遗憾的是,本章不会讨论 LocalBroadcastManager。但是希望你已经对广播接收器的概念和使用有了一些好的基础。

章节总结

  • 您可以使用 BroadcastReceivers 和 Intents 来创建真正解耦的应用。

  • 您可以让您的应用收听特定的广播,并在广播发送时做一些有趣的事情。

  • BroadcastReceivers 可用于在您的应用中路由程序逻辑。您可以让应用以特定方式运行,以响应运行时环境的变化(例如,低电量、无 WiFi 连接)。

  • 可以通过清单或上下文对象注册 BroadcastReceivers。如果你的目标是 Android 9.0,请确保阅读允许通过清单注册的广播动作。Android 团队不鼓励应用通过清单注册,而是使用上下文注册。

在下一章,你将学习如何准备你的应用进行分发。

二十、应用分发

我们将介绍的内容:

  • 清理

  • 准备发布

  • 签署应用

  • Google Play

在某些时候,您可能希望将您的应用分发给更多的人。Android 应用可以相当自由地分发,没有太多限制;你可以在你的网站上下载,甚至直接通过电子邮件发送给用户,但许多开发者选择在谷歌 Play 商店或亚马逊应用商店等市场上发布他们的应用,以最大限度地扩大影响。不管你打算如何发布,在发布应用之前,你需要做一些事情。

发布应用可能是一项非常复杂的活动,它不仅限于应用分发的技术和程序方面,例如在 developer 上创建帐户。,制作打磨好的图标,给你的 app 签名。它还可能涉及创建文案和宣传文本、社交媒体活动以及许多其他与技术完全无关的事情。本章将只关注 app 分发的技术要求。

**通常,发布应用有两个阶段:

  1. 准备 app 发布。这是我们打扫卫生的地方。你需要在发布前清理应用。这是我们删除所有调试信息和其他设置或记录我们在开发过程中使用的内容的地方。你当然不希望你的用户意外地看到你在编码时为自己留下的“明白了”或“我在这里”的痕迹。你可能还想为应用考虑图标和其他视觉素材。在这个阶段投资一个实际的设备并在上面测试你的应用是一个好主意。最重要的是,在这个阶段,我们将建立一个开发者证书。

  2. 发布 app 。你需要宣传这个应用,销售它,并分发它。如果您将在谷歌 Play 商店发布该应用,您需要注册一个发布者帐户,并使用 Google Play 的开发者控制台进行发布。

准备发布应用

这里我们需要做的三件主要事情是:

  1. 准备发布的材质和素材

  2. 配置要发布的应用

  3. 构建一个发布就绪的应用

准备用于发布的材质和素材

无论你的代码多么漂亮或者聪明,用户永远也看不到它。他将看到的是你的视图对象、图标和应用的其他图形素材。确保它们是抛光的。

如果你不考虑应用的图标,那你就是失职。此图标帮助用户识别您的应用,因为它位于主屏幕上。这个图标还会出现在其他几个区域,比如启动窗口、下载部分,更重要的是,如果你在 Google marketplace 上发布你的应用,这个图标也会显示在那里。应用图标可能在给你的潜在用户创造第一印象方面起着重要作用,所以这是一个好主意,你可以在 http://bit.ly/androidreleaseiconguidelines 找到谷歌的应用图标指南。

如果你要在谷歌市场上发布应用,还需要考虑图形素材,比如屏幕截图和促销文本。请务必阅读谷歌的图形素材指南,该指南可在 http://bit.ly/androidreleasegraphicassets 找到

配置要发布的应用

这是你清理和净化应用的部分。我们在这里提到的东西绝不是强制性的,但在构建发布版本之前浏览它们是一个好主意。

检查包名

在前面的章节中,您已经使用了" com.example.myapp" 作为包名。这对于测试或实践应用来说没问题,但当你将应用发布给公众时就不一样了。软件包名称使应用在市场上独一无二,一旦你决定了软件包名称,你就不能再改变它了。所以,考虑一下吧。

删除日志记录和调试信息

调试和日志信息在开发过程中是有用的——甚至是不可或缺的,但是你不能让你的用户看到它们。在发布应用之前,删除应用的所有调试和日志信息。

调试信息很容易处理,您只需删除清单文件的 <应用> 标签中的Android:debuggeable属性。不幸的是,对于日志记录信息,情况就不一样了。

有多种方法可以解决日志问题;这些解决方案可以像手动删除所有日志语句一样简单(但是繁琐),也可以像编写 sed 或 awk 程序来自动删除日志调用一样复杂。有些人通过配置 ProGuard 来处理日志问题(这超出了本书的范围),有些人甚至会使用像 Timber(GitHub 项目)这样的第三方库来替换 Android 的日志类。无论您采用哪种方法,请注意,您需要在构建发布版之前去掉日志语句。

检查应用权限

在开发过程中的某个时候,您可能已经试验了应用的一些特性,并且您可能已经在清单上设置了权限,比如使用网络、写入外部存储等的权限。检查清单上的 < uses-permission > 标记,确保不授予应用不需要的权限。

远程服务器和 URL

如果您的应用依赖于 web APIs 或云服务,请确保应用的发布版本使用的是生产 URL,而不是测试路径。在开发过程中,您可能已经获得了沙盒和测试 URLs 您需要将它们升级到生产版本。

构建发布就绪的应用

我们在本书中所做的所有项目和示例都通过一个简单的过程部署在模拟器中。我们点击了运行按钮。Android Studio 将该应用构建并组装成 APK,部署在目标设备中。之后 app 运行。在整个过程中,有一步是 Android Studio 为我们做的,而你并不知道。你一点都没有意识到。

Android Studio 执行了一项非常重要的任务,在任何设备(仿真或实际设备)上交付或安装任何 APK 之前,都需要执行这项任务。安卓工作室签了那个 APK。

在您可以在任何设备上安装和运行应用之前,应用的 APK 必须经过数字签名。当我们点击运行按钮时,Android Studio 会自动对所有应用进行签名。但是它使用一个调试证书,这个证书只对开发和测试有用。发布应用时,不能使用相同的证书。包括谷歌在内的大多数应用商店都不接受带有调试证书的应用。

在我们发布应用之前,我们必须用正确的证书对其进行签名,而不是调试证书。我们不需要去像 Thawte 或 Verisign 这样的认证机构——自签名证书就可以了。

启动 Android Studio,如果它尚未打开的话。打开您的项目。从主菜单栏进入构建生成签名 APK ,如图 20-1 所示。

img/463887_1_En_20_Fig1_HTML.jpg

图 20-1

生成签名的 APK

单击“下一步”按钮。您应该会看到“密钥库”对话框,如图 20-2 所示。

img/463887_1_En_20_Fig2_HTML.jpg

图 20-2

密钥库对话框

密钥存储路径询问我们的 Java 密钥存储库(JKS)文件在哪里。在这一点上,你还没有。所以,点击新建。你会看到创建新密钥库的对话框,如图 20-3 所示。

img/463887_1_En_20_Fig3_HTML.jpg

图 20-3

新密钥库

注意

在 Java 中,keystore 是安全证书的存储库——授权证书或公钥证书。

表 20-1 显示了对密钥库输入项的描述。

表 20-1

密钥库项目和描述

|

密钥库项目

|

描述

|
| --- | --- |
| 密钥库路径 | 要保存密钥库的位置。这完全取决于。一定要记住这个地方。 |
| 密码 | 这是密钥库的密码。 |
| 别名 | 此别名标识密钥。它只是一个友好的名字。 |
| (钥匙)密码 | 这是钥匙的密码。这与密钥库的密码不同(但是如果您愿意,也可以使用相同的密码)。 |
| 有效期,以年计 | 默认为 25 年;你可以接受默认值。如果在 Google Play 上发布,证书的有效期必须到 2033 年 10 月——所以,25 年应该没问题。 |
| 其他信息 | 只有名字和姓氏字段是必需的。 |

填写完“新建密钥库”对话框后,单击“确定”这将把你带回生成签名 APK 窗口,如图 20-4 所示;但是现在,创建了 JKS 文件,并用它填充了密钥库对话框。

img/463887_1_En_20_Fig4_HTML.jpg

图 20-4

生成签名的 APK,已填充

点击“下一步”

img/463887_1_En_20_Fig5_HTML.jpg

图 20-5

签名 APK APK 目的地文件夹

接下来,我们选择签约 APK 的目的地,如图 20-5 所示。你需要记住这个位置。这是 Android Studio 存储签名 APK 的地方。另外,确保构建类型被设置为“发布”

单击“完成”后,Android Studio 将为您的应用生成签名的 APK。这是您将提交给 Google Play 的文件。你甚至可以在你的网站或其他市场上出售这款 APK——它已经准备好发布了。

发布应用

在向 Google Play 提交应用之前,您需要一个开发者帐户。如果你还没有,可以在 https://developer.android.com 报名。我对接下来的活动做了很多假设。我假设:

  1. 你已经有一个谷歌账户(Gmail);

  2. 你在用谷歌浏览器去https://developer.android.com;和

  3. 您的 Google 帐户已登录 Chrome。

如果你的谷歌账户没有登录 Chrome,你可能会看到类似图 20-6 的东西。Chrome 会要求你选择一个账户(或者创建一个)。

img/463887_1_En_20_Fig6_HTML.jpg

图 20-6

选择帐户

当你整理好你的谷歌账户后,你将被带到 developer.android.com 网站,如图 20-7 所示。

注意

这里显示的截图是他们在写作时出现的。谷歌不时对网站进行修改。当你读到这本书的时候,Google Play 网站可能不再像这些截图一样了。

点击 Google Play ,如图 20-7 所示。

img/463887_1_En_20_Fig7_HTML.jpg

图 20-7

developer.android.com

点击启动游戏控制台,如图 20-8 所示。

img/463887_1_En_20_Fig9_HTML.jpg

图 20-9

Google Play 控制台,注册

img/463887_1_En_20_Fig8_HTML.jpg

图 20-8

启动游戏控制台

您需要通过四个步骤来完成注册(如图 20-9 ):

  1. 使用您的 Google 帐户登录。

  2. 接受开发者协议。

  3. 交报名费。

  4. 填写您的帐户详细信息。

一旦完成注册和支付,您现在就可以访问 Google Play 控制台,如图 20-10 所示。

img/463887_1_En_20_Fig10_HTML.jpg

图 20-10

游戏控制台

您可以从这里开始向商店提交应用的流程。单击“创建应用”按钮开始。

章节总结

  • 你的代码可能很棒,但是用户永远看不到它们。也要注意用户会看到的东西,比如图标和其他图形素材。

  • 在发布代码之前清理它们。删除所有日志和调试信息。

  • 对你自己的工作进行代码评审。如果你有伙伴或者其他人可以和你一起审查代码,那就更好了。如果你的应用使用服务器、RESTful URLs 等。,确保它们是生产就绪的,而不是沙盒。

  • 如果你想将你的应用发布到 Google Play 或 Amazon 等市场,你不能使用调试证书。

  • 如果你想在 Google Play 上出售你的应用,你需要一个 Google Play 账户。我一次性支付了 25 美元的费用,但那是几年前的事了。

  • 别忘了在真实设备上测试你的应用。

  • 我们试图提炼和简化将你的应用放入 Play Store 的过程,但是这一章并不能代替 Android 开发者的发布清单。你还是应该读一下。你可以在 https://bit.ly/appstorelaunchchecklist 找到。**

第一部分:Kotlin 语言

第二部分:Kotlin 与安卓编程

posted @ 2024-08-13 14:02  绝不原创的飞龙  阅读(95)  评论(0)    收藏  举报