首页 > 其他分享 >通过构建安卓应用学习-Kotlin-全-

通过构建安卓应用学习-Kotlin-全-

时间:2024-05-22 15:18:30浏览次数:13  
标签:val Kotlin 安卓 应用程序 构建 fun Android 我们

通过构建安卓应用学习 Kotlin(全)

原文:zh.annas-archive.org/md5/201D65C8BC4C6A97336C0B7173DD6D6D

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

“教育的目的是培养具有技能和专业知识的优秀人才。真正的教育提升了人的尊严,增加了他或她的自尊。如果每个人都能意识到真正的教育,并在人类活动的各个领域中不断推进,世界将会变得更美好。”

— Dr. APJ Abdul Kalam

每天有超过 20 亿的 Android 用户活跃,这种数据覆盖全球数十亿人口,为我们提供了实现改变的真正机会。手机不再仅仅是一种语音通信设备,而是一种能够赋能和赋权给下一个十亿 Android 设备用户的重要角色。

技术的目的是赋能人类,强大设备的可用性为创新提供了机会,可以通过技术改善人们的生活。无论是为农民建立的提供有关天气或作物价格的有用信息的应用,还是为有特殊需求的儿童建立的表达自己的应用,或者为经济困难的妇女建立的经营小型家庭企业的应用,未来的机会是丰富而令人兴奋的。

本书旨在使希望探索 Android 的力量并体验和享受构建 Android 应用的旅程的开发人员能够实现这一目标。这些章节已经组织得很好,使新手开发人员能够理解并从基础开始,或者如果您是经验丰富的开发人员,可以提前了解并探索 Kotlin 的强大之处。

为了使这个应用开发之旅更加有趣和轻松,谷歌已将 Kotlin 作为 Android 应用开发的官方语言之一。Kotlin 表达力强,简洁而强大。它还确保与现有的 Android 语言(如 Java 和运行时)无缝互操作。

这本书适合谁

这本书对于任何刚开始学习 Android 应用开发的人都会很有用。学习 Android 应用开发从未如此令人兴奋,因为我们有多种选择的官方支持语言。本书逐步介绍了使用 Kotlin 进行 Android 应用开发所需的所有细节,并使读者在进步的过程中体验这一旅程。

这本书也一定会帮助那些已经使用 Java 进行 Android 应用开发并试图转换到 Kotlin 或评估 Kotlin 易用性的人。熟悉 Java 的 Android 应用开发人员将发现 Java 和 Kotlin 代码之间的比较非常有用,并将能够欣赏 Kotlin 的强大之处。

本书内容

本书的目标是确保使用 Java 的高级 Android 应用开发人员和刚开始学习 Android 应用开发的人都能享受阅读本书的乐趣。

第一章提供了有关为 Android 应用开发设置系统的逐步信息,列出了开始所需的所有细节。

第二章详细介绍了为 Kotlin 配置环境所需的步骤。尽管最新稳定版本的 Android Studio 已经内置了 Kotlin 支持,但本章的信息将帮助您配置您选择的开发 IDE。

第三章介绍和讨论了 Kotlin 语言构造的细节,如数据类型、变量和常量。

第四章进一步增强了对语言构造的讨论,提供了有关类和对象的信息,并解释了如何定义和处理它们。

第五章,类型检查和空安全,讨论了 Kotlin 的显著特性-类型检查和空安全。Kotlin 通过其设计完全消除了空引用。

第六章,函数和 Lambda,提供了有关定义函数并在程序中使用它的信息。本章还讨论了 Lambda 并提供了有关它们使用的信息。

第七章,开发您的基于位置的闹钟,讨论了 Google 基于位置的服务的基本原理,使用 Google 地图 API 以及在地图上自定义标记。

第八章,使用 Google 的位置服务,展示了如何使用基于位置的服务构建应用程序,并解释了如何构建我们自己的基于位置的闹钟。

第九章,连接外部世界-网络,涵盖了网络和与外部世界通信的概念。我们讨论了 Android 框架提供的开箱即用选项,以及像 Picasso 和 Glide 这样的第三方库。

第十章,开发一个简单的待办事项列表应用程序,讨论了使用 Android Studio 构建用户界面的方法,并提供了有关使用 ListViews 和 Dialogs 的信息。

第十一章,使用数据库持久化,简要介绍了关系数据库,详细讨论了 SQLite 上的 CRUD 操作,并研究了 ORM 的使用,特别是来自 Google 的 Room ORM。

第十二章,为任务设置提醒,讨论了如何设置并向用户推送应用程序的通知。它还解释了如何利用 Firebase 和 Amazon SNS 等云服务。我们还讨论了服务和广播接收器。

第十三章,测试和持续集成,讨论了测试的重要性,Android Studio 通过 Android 测试支持库提供的开箱即用支持,如何使用 Crashlytics 跟踪崩溃报告以及 Beta 测试。本章还介绍了 CI,并详细介绍了诸如 Jenkins、Bamboo 和 Fastlane 等工具的使用步骤。

第十四章,使您的应用程序面向全球,解释了如何将您的应用程序发布到 Google Play 商店和亚马逊应用商店。

第十五章,使用 Google Faces API 构建应用程序,讨论了使用 Google Faces API 以及如何构建使用它的应用程序。本章还提供了有关创建 Paint 实例并使用画布绘制图像的信息。本章还讨论了在图像上绘制矩形等形状。

要充分利用本书

具有面向对象编程和 Android 活动生命周期知识将很有用,但不是强制性的。

Android Studio 的最新稳定版本(发布时为 3.1.3 版)提供了对 Kotlin 的开箱即用支持。

下载示例代码文件

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

您可以按照以下步骤下载代码文件:

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

  2. 选择“支持”选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名并按照屏幕上的说明操作。

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

  • Windows 的 WinRAR/7-Zip

  • Mac 的 Zipeg/iZip/UnRarX

  • Linux 的 7-Zip/PeaZip

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Learning-Kotlin-by-building-Android-Applications。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有来自丰富图书和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。去看看吧!

使用的约定

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

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。以下是一个例子:“将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。”

代码块设置如下:

<dimen name="board_padding">16dp</dimen>
<dimen name="cell_margin">2dp</dimen>
<dimen name="large_text">64sp</dimen>

任何命令行输入或输出都是这样写的:

brew cask install fastlane

粗体:表示一个新术语、一个重要词或屏幕上看到的词。例如,菜单或对话框中的词在文本中会出现如此。以下是一个例子:“从管理面板中选择系统信息。”

警告或重要说明看起来像这样。

提示和技巧看起来像这样。

第一章:为 Android 开发进行设置

Java 是全球使用最广泛的语言之一,直到最近,它还是 Android 开发的首选语言。Java 在其所有伟大之处仍然存在一些问题。多年来,我们看到了许多试图解决 Java 问题的 JVM 语言的发展。其中一个相当新的是 Kotlin。Kotlin 是由 JetBrains 开发的一种新的编程语言,JetBrains 是一家生产软件开发工具的软件开发公司(他们的产品之一是 Android Studio 基于的 IntelliJ IDEA)。

在本章中,我们将看看:

  • Kotlin 在 Android 开发中的优势

  • 为 Android 开发做好准备

为什么要用 Kotlin 开发 Android?

在所有的 JVM 语言中,Kotlin 是唯一一个为 Android 开发者提供了更多功能的语言。Kotlin 是除了 Java 之外唯一一个与 Android Studio 集成的 JVM 语言。

让我们来看看一些 Kotlin 的惊人特性。

简洁

Java 最大的问题之一是冗长。任何尝试在 Java 中编写一个简单的“hello world”程序的人都会告诉你需要多少行代码。与 Java 不同,Kotlin 不是一种冗长的语言。Kotlin 消除了很多样板代码,比如getterssetters。例如,让我们比较一下 Java 中的 POJO 和 Kotlin 中的 POJO。

Java 中的学生 POJO

public class Student {

    private String name;

    private String id;

    public String getName() {
        return name;
    }

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

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}

Kotlin 中的学生 POJO

class Student() {
  var name:String
  var id:String
}

正如您所看到的,相同功能的 Kotlin 代码要少得多。

告别 NullPointerException

使用 Java 和其他语言的主要痛点之一是访问空引用。这可能导致您的应用程序崩溃,而不向用户显示充分的错误消息。如果您是 Java 开发人员,我相信您对NullPointerException非常熟悉。关于 Kotlin 最令人惊讶的一点是空安全性。

使用 Kotlin,NullPointerException只能由以下原因之一引起:

  • 外部 Java 代码

  • 显式调用抛出NullPointerException

  • 使用!!运算符(我们稍后将学习更多关于这个运算符的知识)

  • 关于初始化的数据不一致

这有多酷?

Java 的互操作性

Kotlin 被开发成能够与 Java 舒适地工作。对于开发人员来说,这意味着您可以使用用 Java 编写的库。您还可以放心地使用传统的 Java 代码。而且,有趣的部分是您还可以在 Java 中调用 Kotlin 代码。

这个功能对于 Android 开发者来说非常重要,因为目前 Android 的 API 是用 Java 编写的。

设置您的环境

在开始 Android 开发之前,您需要做一些准备工作,使您的计算机能够进行 Android 开发。我们将在本节中逐一介绍它们。

如果您对 Android 开发不是很了解,可以跳过本节。

Java

由于 Kotlin 运行在 JVM 上,我们必须确保我们的计算机上安装了Java 开发工具包(JDK)。如果您没有安装 Java,请跳转到安装 JDK 的部分。如果您不确定,可以按照以下说明检查您的计算机上安装的 Java 版本。

在 Windows 上:

  1. 打开 Windows 开始菜单

  2. 在 Java 程序列表下,选择关于 Java

  3. 将会显示一个弹出窗口,其中包含计算机上 Java 版本的详细信息:

在 Mac 或其他 Linux 机器上:

  1. 打开终端应用程序。要做到这一点,打开启动台并在搜索框中输入终端。终端应用程序将显示如下截图所示。选择它:

  1. 在终端中,输入以下命令来检查您的计算机上的 JDK 版本:java -version

  2. 如果你已经安装了 JDK,Java 的版本将会显示在下面的截图中:

安装 JDK

  1. 打开浏览器,转到 Java 网站:www.oracle.com/technetwork/java/javase/downloads/index.html

  2. 在“下载”选项卡下,单击 JDK 下的下载按钮,如下面的屏幕截图所示:

  1. 在下一个屏幕上,选择“接受许可协议”复选框,然后单击与您的操作系统匹配的产品的下载链接

  2. 下载完成后,继续安装 JDK

  3. 安装完成后,您可以再次运行版本检查命令,以确保您的安装成功

Android Studio

许多IDE支持 Android 开发,但最好和最常用的 Android IDE 是 Android Studio。 Android Studio 基于由 JetBrains 开发的 IntelliJ IDE。

安装 Android Studio

转到 Android Studio 页面,developer.android.com/sdk/installing/studio.html,然后单击“下载 Android Studio”按钮:

在弹出窗口上,阅读并接受条款和条件,然后单击下载适用于 Mac 的 Android Studio按钮:

按钮的名称因使用的操作系统而异。

下载将开始,并且您将被重定向到一个说明页面(developer.android.com/studio/install)。

按照您的操作系统的指定说明安装 Android Studio。安装完成后,打开 Android Studio 并开始设置过程。

准备 Android Studio

在完整安装屏幕上,请确保选择了“我没有以前的 Studio 版本”或“我不想导入我的设置”选项,然后单击“确定”按钮:

在欢迎屏幕上,单击“下一步”转到安装类型屏幕:

然后,选择标准选项,然后单击“下一步”继续:

在“验证设置”屏幕上,通过单击“完成”按钮确认您的设置:

在“验证设置”屏幕上,列出的 SDK 组件将开始下载。您可以单击“显示详细信息”按钮查看正在下载的组件的详细信息:

下载和安装完成后,单击“完成”按钮。就是这样。您已经完成了安装和设置 Android Studio。

创建您的第一个 Android 项目

在欢迎使用 Android Studio 屏幕上,单击“开始新的 Android Studio 项目”:

这将启动创建新项目向导。在配置新项目屏幕上,将TicTacToe输入为应用程序名称。指定公司域。包名称是从公司域和应用程序名称生成的。

将项目位置设置为您选择的位置,然后单击“下一步”:

选择 SDK

在目标 Android 设备屏幕上,您必须选择设备类型和相应的最低 Android 版本,以便运行您的应用程序。 Android软件开发工具包(SDK)提供了构建 Android 应用程序所需的工具,无论您选择的语言是什么。

每个新版本的 SDK 都带有一组新功能,以帮助开发人员在其应用程序中提供更多令人敬畏的功能。然而,困难在于 Android 在非常广泛的设备范围上运行,其中一些设备无法支持最新版本的 Android。这使开发人员在实施出色的新功能或支持更广泛的设备范围之间陷入困境。

Android 试图通过提供以下内容来使这个决定更容易:

  • 提供有关使用特定 SDK 的设备百分比的数据,以帮助开发人员做出明智的选择。要在 Android Studio 中查看此数据,请单击最低 SDK 下拉菜单下的“帮助我选择”。这将显示当前支持的 Android SDK 版本列表及其支持的功能,以及如果将其选择为最低 SDK,则应用程序将支持的 Android 设备的百分比:

您可以在 Android 开发者仪表板(developer.android.com/about/dashboards/)上查看最新和更详细的数据。

  • Android 还提供了支持库,以帮助向后兼容某些新功能,这些功能是在较新的 SDK 版本中添加的。每个支持库都向后兼容到特定的 API 级别。支持库通常根据其向后兼容的 API 级别进行命名。一个例子是 appcompat-v7,它提供了对 API 级别 7 的向后兼容性。

我们将在后面的部分进一步讨论 SDK 版本。现在,您可以选择 API 15:Android 4.0.3(冰淇淋三明治)并单击“下一步”:

接下来的屏幕是“在移动屏幕上添加活动”。这是您选择默认活动的地方。 Android Studio 提供了许多选项,从空白屏幕的活动到带有登录屏幕的活动。现在,选择“基本活动”选项,然后单击“下一步”:

在下一个屏幕上,输入活动的名称和标题,以及活动布局的名称。然后,单击完成

构建您的项目

单击“完成”按钮后,Android Studio 会在后台为您生成和配置项目。 Android Studio 执行的后台进程之一是配置 Gradle。

Gradle

Gradle 是一个易于使用的构建自动化系统,可用于自动化项目的生命周期,从构建和测试到发布。在 Android 中,它接受您的源代码和配置的 Android 构建工具,并生成一个Android Package Kit (APK)文件。

Android Studio 生成了构建初始项目所需的基本 Gradle 配置。让我们来看看这些配置。打开build.gradle

Android 部分指定了所有特定于 Android 的配置,例如:

  • compileSdkVersion:指定应用程序应使用的 Android API 级别进行编译。

  • buildToolsVersion:指定应用程序应使用的构建工具版本。

  • applicationId:在发布到 Play 商店时用于唯一标识应用程序。您可能已经注意到,它目前与创建应用程序时指定的包名称相同。在创建时,applicationId默认为包名称,但这并不意味着您不能使它们不同。您可以。只是记住,在发布应用程序的第一个版本后,不应再次更改applicationId。包名称可以在应用程序的清单文件中找到。

  • minSdkVersion:如前所述,这指定了运行应用程序所需的最低 API 级别。

  • targetSdkVersion:指定用于测试应用的 API 级别。

  • versionCode:指定应用的版本号。在发布之前,每个新版本都应更改此版本号。

  • versionName:为您的应用指定一个用户友好的版本名称。

依赖项部分指定了构建应用程序所需的依赖项。

Android 项目的各个部分

我们将看看项目的不同部分。屏幕截图显示了我们的项目:

让我们进一步了解项目的不同部分:

  • manifests/AndroidManifest.xml:指定 Android 系统运行应用程序所需的有关应用程序的重要细节。其中一些细节是:

  • 包名

  • 描述应用程序的组件,包括活动,服务等等

  • 声明应用程序所需的权限

  • res目录:包含应用程序资源,如图像,xml 布局,颜色,尺寸和字符串资源:

  • res/layout目录:包含定义应用程序用户界面UI)的 xml 布局

  • res/menu目录:包含定义应用程序菜单内容的布局

  • res/values目录:包含资源,如颜色(res/values/colors.xml)和字符串(res/values/strings.xml

  • 以及您的 Java 和/或 Kotlin 源文件

运行您的应用程序

Android 使您能够在将应用程序发布到 Google Play 商店之前,就可以在实际设备或虚拟设备上运行您的应用程序。

Android 模拟器

Android SDK 配备了一个在计算机上运行并利用其资源的虚拟移动设备。这个虚拟移动设备称为模拟器。模拟器基本上是一个可配置的移动设备。您可以配置其 RAM 大小,屏幕大小等。您还可以运行多个模拟器。当您想要在不同的设备配置(如屏幕大小和 Android 版本)上测试应用程序,但又负担不起实际设备时,这是非常有帮助的。

您可以在开发者页面上阅读有关模拟器的更多信息,网址为developer.android.com/studio/run/emulator

创建 Android 模拟器

Android 模拟器可以从Android 虚拟设备(AVD)管理器创建。您可以通过单击 Android Studio 工具栏上的图标来启动 AVD 管理器,如下面的屏幕截图所示:

或者,通过从菜单中选择工具| Android | AVD 管理器:

在“您的虚拟设备”屏幕上,单击“创建虚拟设备...”按钮:

如果您已经创建了模拟器,按钮将位于屏幕底部:

下一步是选择要模拟的设备类型。AVD 管理器允许您为电视,手机,平板电脑和 Android 穿戴设备创建模拟器。

确保在屏幕左侧的类别部分中选择“手机”。浏览屏幕中间的设备列表并选择一个。然后,单击“下一步”:

在系统映像屏幕上,选择您希望设备运行的 Android 版本,然后单击“下一步”:

如果您想要模拟的 SDK 版本尚未下载,请单击其旁边的下载链接以下载它。

在验证配置屏幕上,通过单击“完成”按钮来确认虚拟设备设置:

您将被发送回“您的虚拟设备”屏幕,您的新模拟器将显示如下:

您可以单击“操作”选项卡下的播放图标来启动模拟器,或者单击铅笔图标来编辑其配置。

让我们继续通过单击播放图标来启动刚刚创建的模拟器:

您可能已经注意到,虚拟设备右侧带有一个工具栏。该工具栏称为模拟器工具栏。它使您能够模拟功能,如关闭、屏幕旋转、音量增加和减少以及缩放控件。

单击工具栏底部的 More(...)图标还可以让您访问额外的控件,以模拟指纹、设备位置、消息发送、电话呼叫和电池电量等功能:

从模拟器运行

从模拟器运行您的应用程序非常容易。单击 Android Studio 工具栏上的播放图标,如下面的屏幕截图所示:

在弹出的“选择部署目标”屏幕上,选择要在其上运行应用程序的设备,然后单击“确定”:

Android Studio 将在模拟器上构建和运行您的应用程序:

如果您尚未运行模拟器,您的模拟器将显示在可用虚拟设备部分下。选择它们将启动模拟器,然后在其上运行您的应用程序:

在实际设备上运行

要在实际设备上运行您的应用程序,您可以构建并将 APK 复制到设备上,并从那里运行。为此,Android 要求设备启用允许从未知来源安装应用程序的选项。请执行以下步骤:

  1. 在您的设备上打开“设置”应用程序。

  2. 选择“安全”。

  3. 查找并打开“未知来源”选项。

  4. 您将收到有关从未知来源安装应用程序的危险的提示。仔细阅读并单击“确定”以确认。

  5. 就是这样。您现在可以上传您的 APK 并在手机上运行它。

您可以通过返回到“设置”|“安全”并关闭该选项来轻松禁用“未知来源”设置。

我们都可以同意以这种方式运行您的应用程序并不是非常理想的,特别是用于调试。考虑到这一点,Android 设备具有在不必将应用程序上传到设备的情况下非常轻松地运行和调试应用程序的能力。这可以通过连接设备使用 USB 电缆来完成。为此,Android 要求启用开发者模式。请按照以下说明启用开发者模式:

  1. 在您的设备上打开“设置”应用程序。

  2. 向下滚动并选择“关于手机”。

  3. 在“手机状态”屏幕上,向下滚动并点击“版本号”多次,直到看到一个提示,上面写着“您现在是开发者!”

  4. 返回到“设置”屏幕。现在应该会看到“开发人员选项”条目。

  5. 选择“开发人员选项”。

  6. 在“开发人员选项”屏幕上,打开屏幕顶部的开关。如果关闭,您将收到一个“允许开发设置?”对话框。单击“确定”以确认。

  7. 向下滚动并打开 USB 调试。您将收到一个允许 USB 调试?对话框。单击“确定”以确认。

  8. 接下来,通过 USB 将您的设备连接到计算机。

  9. 您将收到另一个允许 USB 调试?对话框,其中包含您计算机的 RSA 密钥指纹。选择“始终允许此计算机”选项,然后单击“确定”以确认。

您现在可以在设备上运行您的应用程序。再次单击工具栏上的“运行”按钮,在“选择部署目标”对话框中选择您的设备,并单击“确定”:

就是这样。您现在应该在您的设备上看到您的应用程序:

摘要

在本章中,我们经历了检查和安装 JDK 的过程,这是 Android 开发所必需的。我们还安装并设置了我们的 Android Studio 环境。我们创建了我们的第一个 Android 应用程序,并学会在模拟器和实际设备上运行它。

在下一章中,我们将学习如何配置和设置 Android Studio 和我们的项目以使用 Kotlin 进行开发。

第二章:为 Kotlin 配置您的环境

在本章中,我们将介绍准备 Android Studio 和配置我们在上一章中创建的项目以进行 Kotlin 开发的过程。

在这个过程中,我们将学习如何:

  • 在 Android Studio 中下载并安装 Kotlin 插件

  • 在 Android 项目中配置 Kotlin

  • 在 Kotlin 类中引用 Java 代码,反之亦然

  • 将 Java 类转换为 Kotlin 类

安装 Kotlin 插件

要在项目中使用 Kotlin,首先必须在 Android Studio 中安装 Kotlin 插件:

  1. 选择 Android Studio | 首选项,并在首选项窗口中选择插件:

  1. 在插件窗口上,点击屏幕底部的“安装 JetBrains 插件...”按钮:

  1. 在 Jetbrains 插件浏览屏幕上,搜索Kotlin并从选项列表中选择 Kotlin。然后,点击安装按钮:

  1. 下载和安装完成后,点击“重新启动 Android Studio”按钮以重新启动 IDE。

最新版本的 Android Studio 和 3.0 以上的版本提供了对 Kotlin 的全面支持。在 3.0 以下的版本中,可以通过安装插件来启用 Kotlin 支持,如前所示。

使我们的项目准备好使用 Kotlin

要能够开始向我们的项目添加 Kotlin 代码,首先必须配置我们的项目以支持 Kotlin。

  1. 首先,选择工具 | Kotlin | 配置项目中的 Kotlin:

  1. 接下来,在选择配置器弹出窗口中选择带 Gradle 的 Android 选项:

  1. 在带 Gradle 的 Android 中配置 Kotlin 弹出窗口中,选择要使用的 Kotlin 版本,然后点击确定:

建议保留默认选择的版本,因为这通常是最新版本。

这将导致项目中的build.gradle文件发生一些变化:

  • 在项目的build.gradle(Project:TicTacToe)文件中,应用了以下更改:

  • 声明项目中使用的 Kotlin 插件的版本

  • Kotlin Gradle 插件被声明为项目的类路径依赖之一:!

  • 在应用模块的build.gradle(Module:app)文件中,应用了以下更改:

  • kotlin-android插件应用于模块

  • Kotlin 的Standard库被声明为应用模块的compile时依赖:!

  • 点击立即同步以构建项目

从 Android Studio 3.0 开始,Android Studio 内置了 Kotlin 支持。因此,您无需安装 Kotlin 插件即可使用它。

现在我们已经完全配置了 Kotlin,让我们试试它。

Kotlin 和 Java 并存?

Kotlin 的一个令人惊奇的特点是它能够与 Java 在同一个项目中共存和工作。

让我们尝试创建一个 Kotlin 类。Android Studio Kotlin 插件使这一过程与创建 Java 类一样简单。选择文件 | 新建 | Kotlin 文件/类

新建 Kotlin 文件/类弹出窗口中,输入类的名称,从种类下拉菜单中选择Class,然后点击确定:

新类看起来是这样的:

package com.packtpub.eunice.tictactoe

class HelloKotlin {
}

Kotlin 中的默认可见性修饰符是 public,因此无需像在 Java 类中那样指定 public 修饰符。

让我们在我们的新 Kotlin 类中添加以下方法:

fun displayMessage(view: View) {
    Snackbar.make(view, "Hello Kotlin!!", Snackbar.LENGTH_LONG).setAction("Action", null).show()
}

前面的方法将一个 Android 视图(android.view.View)作为参数,并将其与消息一起传递给 Snackbar 的make()方法以显示消息。

Kotlin 能够使用 Java 代码的能力称为互操作性。这个功能也可以反过来工作,允许从 Java 类调用 Kotlin 代码。让我们试试看:

打开MainActivity.java。在onCreate()方法中,用以下代码替换以下行:

Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG).setAction("Action", null).show();

用以下内容替换:

new HelloKotlin().diplayMessage(view);

代码的前一行创建了HelloKotlin类的一个实例,并调用了它的displayMessage()方法。

构建并运行您的应用程序:

是的,就是这么简单。

Kotlin 转 Java?

到目前为止,我们已经学习了创建 Kotlin 类并在MainActivity.java类中访问其方法的过程。我们的项目目前包括一个 Java 类和一个 Kotlin 类,但我们希望整个项目都是 Kotlin。那么,我们该怎么办?我们需要将MainActivity.java类重写为 Kotlin 吗?不需要。Kotlin 插件添加到 Android Studio 的功能之一是能够将 Java 代码转换为 Kotlin 的能力。

要做到这一点,请打开MainActivity.java类,然后转到“代码”|“将 Java 文件转换为 Kotlin 文件”:

您将收到有关转换准确性的警告消息。目前,我们不需要担心这个问题。只需单击“确定”继续:

您的MainActivity.java类现在应该是这样的:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val toolbar = findViewById(R.id.toolbar) as Toolbar
        setSupportActionBar(toolbar)

        val fab = findViewById(R.id.fab) as FloatingActionButton
        fab.setOnClickListener { view ->   
    HelloKotlin().displayKotlinMessage(view) }
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        // Inflate the menu; this adds items to the action bar if it is   
       //present.
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        val id = item.itemId

        return if (id == R.id.action_settings) {
            true
        } else super.onOptionsItemSelected(item)

    }
}

您还会注意到文件的扩展名也已更改为.kt

再次构建并运行您的应用程序:

总结

在本章中,我们学习了如何为 Kotlin 开发配置 Android Studio 和 Android 项目。我们还学习了如何从 Java 创建和调用 Kotlin 类。我们还学习了使用 Kotlin 插件将 Java 源文件转换为 Kotlin。如果您有用 Java 编写的旧代码,并且想逐渐转换为 Kotlin,这些功能特别有帮助。

在使用Convert Java to Kotlin功能时,请记住,在某些情况下,您需要对生成的 Kotlin 文件进行一些更正。

在接下来的几章中,我们将为我们的项目添加更多功能(您可能已经猜到,这是一个简单的井字棋游戏)。在这个过程中,我们将更深入地了解 Kotlin 作为一种语言的基础知识。我们将涵盖诸如数据类型、类、函数、协程和空安全等主题。

第三章:数据类型、变量和常量

在本章中,我们将开始构建我们的井字游戏,同时学习 Kotlin 中的数据类型、变量和常量。

到本章结束时,我们将有:

  • 检查了应用程序的当前 UI

  • 设计了游戏的 UI

  • 在 Kotlin 中学习了基本类型

用户界面

在 Android 中,应用程序 UI 的代码是用 XML 编写的,并存储在布局文件中。让我们来看看在创建项目时创建的默认 UI。

打开 res/layout/activity_main.xml。确保在屏幕底部选择了 Text。Android Studio 应该会显示右侧的预览和 UI 的 XML 代码:

如果您在右侧看不到预览,请转到 View | Tool Windows | Preview 启用它。

现在,让我们来看看主活动布局的各个元素:

  1. 父元素是 CoordinatorLayoutCoordinatorLayout 在 Android 5.0 中作为设计库的一部分引入。它提供了更好的控制,可以在其子视图之间进行触摸事件。当单击按钮时,我们已经看到了这种功能是如何工作的,SnackBar 出现在 FloatingActionButton 下方(而不是覆盖它)。

  2. 标记为 2 的元素是 Toolbar,它充当应用程序的顶部导航。通常用于显示应用程序的标题、应用程序标志、操作菜单和导航按钮。

  3. include 标签用于将一个布局嵌入到另一个布局中。在这种情况下,res/layout/content_main.xml 文件包含了我们在运行应用程序时看到的 TextView(显示 Hello World! 消息)。我们的大多数 UI 更改将在 res/layout/content_main.xml 文件中完成。

  4. FloatingActionButton,您可能已经注意到,是一个可操作的浮动在屏幕上的 ImageView

构建我们的游戏 UI

我们的井字游戏屏幕将包括游戏板(一个 3x3 的网格)、显示轮到谁了的 TextView 和用于重新开始游戏的 FloatingActionButton

我们将使用 TableLayout 来设计游戏板。打开 res/layout/content_main.xml 文件,并将 TextView 声明替换为 TableLayout 声明,如下所示:

<TableLayout
    android:id="@+id/table_layout"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:gravity="center">

    <TableRow
        android:id="@+id/r0"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:background="@android:color/black">
        <TextView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:gravity="center"
            android:background="@android:color/white"
            android:layout_marginBottom="2dp"
            android:layout_marginTop="0dp"
            android:layout_column="0"
            android:layout_marginRight="2dp"
            android:layout_marginEnd="2dp"
            android:textSize="64sp"
            android:textColor="@android:color/black"
            android:clickable="true"/>
        <TextView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:gravity="center"
            android:background="@android:color/white"
            android:layout_marginBottom="2dp"
            android:layout_marginTop="0dp"
            android:layout_column="2"
            android:layout_marginRight="2dp"
            android:layout_marginEnd="2dp"
            android:textSize="64sp"
            android:textColor="@android:color/black"
            android:clickable="true"/>
        <TextView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:gravity="center"
            android:background="@android:color/white"
            android:layout_marginBottom="2dp"
            android:layout_marginTop="0dp"
            android:layout_column="2"
            android:layout_marginRight="2dp"
            android:layout_marginEnd="2dp"
            android:textSize="64sp"
            android:textColor="@android:color/black"
            android:clickable="true"/>
    </TableRow>

    </TableLayout>

这里有几件事情需要注意:

  • TableRow 元素表示表格的一行。从前面的代码中,行的每个元素都由一个 TextView 表示。

  • 每个 TextView 都具有相似的属性。

  • 前面的代码声明了一个 1x3 的表格,换句话说,是一个具有一行和三列的表格。由于我们想要创建一个 3x3 的网格,我们需要添加另外两个 TableRow 元素。

前面的代码已经包含了很多重复的代码。我们需要找到一种方法来减少重复的数量。这就是 res/values 的作用所在。

在添加两个额外的 TableRow 元素之前,让我们更好地组织我们的代码。打开 res/values/styles.xml 并添加以下代码:

<!--Table Row Attributes-->
<style name="TableRow">
    <item name="android:layout_width">match_parent</item>
    <item name="android:layout_height">wrap_content</item>
    <item name="android:gravity">center_horizontal</item>
    <item name="android:background">@android:color/black</item>
</style>

<!--General Cell Attributes-->
<style name="Cell">
    <item name="android:layout_width">100dp</item>
    <item name="android:layout_height">100dp</item>
    <item name="android:gravity">center</item>
    <item name="android:background">@android:color/white</item>
    <item name="android:layout_marginTop">@dimen/cell_margin</item>
    <item name="android:layout_marginBottom">@dimen/cell_margin</item>
    <item name="android:textSize">@dimen/large_text</item>
    <item name="android:textColor">@android:color/black</item>
    <item name="android:clickable">true</item>

</style>

<!--Custom Left Cell Attributes-->
<style name="Cell.Left">
    <item name="android:layout_column">0</item>
    <item name="android:layout_marginRight">@dimen/cell_margin</item>
</style>

<!--Custom Middle Cell Attributes-->
<style name="Cell.Middle">
    <item name="android:layout_column">1</item>
    <item name="android:layout_marginRight">@dimen/cell_margin</item>
    <item name="android:layout_marginLeft">@dimen/cell_margin</item>
</style>

<!--Custom Right Cell Attributes-->
<style name="Cell.Right">
    <item name="android:layout_column">2</item>
    <item name="android:layout_marginLeft">@dimen/cell_margin</item>
</style>

您可以通过以 Parent.child 的格式命名它们来创建继承自父级的子样式,例如,Cell.LeftCell.MiddleCell.Right 都继承了 Cell 样式的属性。

接下来,打开 res/values/dimens.xml。这是您在布局中声明的尺寸。将以下代码添加到资源元素中:

<dimen name="board_padding">16dp</dimen>
<dimen name="cell_margin">2dp</dimen>
<dimen name="large_text">64sp</dimen>

现在,打开 res/values/strings.xml。这是您在应用程序中声明所需的字符串资源。在资源元素中添加以下代码:

<string name="x">X</string>
<string name="o">O</string>
<string name="turn">%1$s\'s Turn</string>
<string name="winner">%1$s Won</string>
<string name="draw">It\'s a Draw</string>

然后,打开 res/layout/content_main.xml 文件,并将 TableRow 声明替换为以下内容:

<TableRow
    android:id="@+id/r0"
    style="@style/TableRow">
    <TextView
        style="@style/Cell.Left"
        android:layout_marginTop="0dp"/>
    <TextView
        style="@style/Cell.Middle"
        android:layout_marginTop="0dp"/>
    <TextView
        style="@style/Cell.Right"
        android:layout_marginTop="0dp"/>
</TableRow>
<TableRow
    android:id="@+id/r1"
    style="@style/TableRow">
    <TextView
        style="@style/Cell.Left"/>
    <TextView
        style="@style/Cell.Middle"/>
    <TextView
        style="@style/Cell.Right"/>
</TableRow>

<TableRow
    android:id="@+id/r2"
    style="@style/TableRow">

    <TextView
        style="@style/Cell.Left"
        android:layout_marginBottom="0dp"/>
    <TextView
        style="@style/Cell.Middle"
        android:layout_marginBottom="0dp"/>
    <TextView
        style="@style/Cell.Right"
        android:layout_marginBottom="0dp"/>
</TableRow>

我们现在已经声明了所有三行。正如您所看到的,我们的代码看起来更有组织性。

构建并运行以查看到目前为止的进展:

让我们继续添加一个 TextView,用于显示轮到谁了。打开 res/layout/activity_main.xml 并在 include 元素之前添加以下 TextView 声明:

<TextView
    android:id="@+id/turnTextView"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/turn"
    android:textSize="64sp"
    android:textAlignment="center"
    android:layout_marginTop="@dimen/fab_margin"/>

通过替换以下代码来改变FloatingActionButton的图标和背景颜色:

app:srcCompat="@android:drawable/ic_dialog_email" 

使用以下内容:

app:srcCompat="@android:drawable/ic_input_add"
app:backgroundTint="@color/colorPrimary"

再次构建和运行:

就这样。我们完成了 UI 设计。

基本类型

在 Kotlin 中,没有原始数据类型的概念。所有类型都是带有成员函数和属性的对象。

变量和常量

使用var关键字声明变量,并使用val关键字声明常量。在声明变量或常量时,不必显式定义其类型。类型将从上下文中推断出来。val只能初始化一次。只有在明确声明为可空类型时,变量或常量才能被赋予空值。通过在类型的末尾添加?来声明可空类型:

var a: String = "Hello"
var b = "Hello"

val c = "Constant"
var d: String? = null // nullable String

b = null // will not compile

b = 0 // will not compile
c = "changed" // will not compile

例如,在上面的代码中,ab都将被视为String。当尝试重新分配推断类型的变量时,不同类型的值将引发错误。val只能初始化一次。

属性

在 Kotlin 中,通过简单地引用名称来访问属性。尽管gettersetter不是必需的,但你也可以创建它们。属性的getter和/或setter可以作为其声明的一部分创建。如果属性是val,则不允许有setter。属性需要在创建时初始化:

var a: String = ""              // required
    get() = this.toString()     // optional
    set(v) {                    // optional
        if (!v.isEmpty()) field = v
    }

让我们继续声明一些我们在MainActivity类中需要的属性:

var gameBoard : Array<CharArray> = Array(3) { CharArray(3) } // 1
var turn = 'X' // 2
var tableLayout: TableLayout? = null // 3
var turnTextView: TextView? = null // 4

  1. gameBoard是一个 3x3 的矩阵,表示一个井字棋游戏板。它将用于存储棋盘上每个单元格的值。

  2. turn是一个 char 类型的变量,用于存储当前是谁的回合,X 还是 O。

  3. tableLayout是一个android.widget.TableLayout,将在onCreate()方法中用 xml 布局中的视图进行初始化。

  4. turnTextView是一个android.widget.TextView,用于显示当前是谁的回合。这也将在onCreate()方法中用 xml 布局中的视图进行初始化。

总结

在本章中,我们为简单的井字棋游戏设计了用户界面。我们还学习了如何在 Kotlin 中使用变量和常量。

在下一章中,我们将继续实现游戏逻辑,同时学习类和对象。

第四章:类和对象

在本章中,我们将继续在学习 Kotlin 中的类和对象的同时继续开发我们的 TicTacToe 游戏。

在本章结束时,我们将有:

  • 在 Kotlin 中学习了类和对象

  • 为游戏的一部分逻辑工作

类的结构

就像 Java 一样,在 Kotlin 中,使用class关键字声明类。类的基本结构包括:

  • class关键字

  • 类的名称

  • 页眉

  • 类的主体用大括号括起来

页眉可以由主构造函数、父类(如果适用)和要实现的接口(如果适用)组成。

在四个部分中,只有前两个是强制的。如果类没有主体,您可以跳过大括号。

构造函数

就像在 Java 中一样,类可以有多个构造函数,但是在 Kotlin 中,主构造函数可以作为类的页眉的一部分添加。

例如,让我们向HelloKotlin类添加一个构造函数:

import kotlinx.android.synthetic.main.activity_main.*

class HelloKotlin constructor(message: String) {

    fun displayKotlinMessage(view: View) {
        Snackbar.make(view, "Hello Kotlin!!",
        Snackbar.LENGTH_LONG).setAction("Action", null).show()
    }
}

在先前的代码中,HelloKotlin类具有一个主构造函数,该构造函数接受一个名为message的字符串。

由于构造函数没有任何修饰符,因此我们可以完全摆脱constructor关键字:

class HelloKotlin (message: String) {

    fun displayKotlinMessage(view: View) {
        Snackbar.make(view, "Hello Kotlin!!", 
        Snackbar.LENGTH_LONG).setAction("Action", null).show()
    }

}

在 Kotlin 中,次要构造函数必须调用主构造函数。让我们看一下代码:

class HelloKotlin (message: String) {

    constructor(): this("Hello Kotlin!!")

    fun displayKotlinMessage(view: View) {
        Snackbar.make(view, "Hello Kotlin!!", 
        Snackbar.LENGTH_LONG).setAction("Action", null).show()
    }
}

关于次要构造函数的一些注意事项:

  • 它不接受任何参数。

  • 它使用默认消息调用主构造函数。

  • 它不使用大括号。这是因为它没有主体,因此不需要大括号。如果我们添加一个主体,我们将需要使用大括号。

如果displayKotlinMessage()方法想要使用构造函数中传递的message参数,该怎么办?

有两种方法可以解决这个问题。您可以在HelloKotlin中创建一个字段,并使用传递的message参数进行初始化:

class HelloKotlin (message: String) {

    private var msg = message

    constructor(): this("Hello Kotlin!!")

    fun displayKotlinMessage(view: View) {
        Snackbar.make(view, msg, 
        Snackbar.LENGTH_LONG).setAction("Action", null).show()
    }
}

您还可以向message参数添加适当的关键字,使其成为类的字段:

class HelloKotlin (private var message: String) {

    constructor(): this("Hello Kotlin!!")

    fun displayKotlinMessage(view: View) {
        Snackbar.make(view, message, 
        Snackbar.LENGTH_LONG).setAction("Action", null).show()
    }
}

让我们试一下我们所做的更改。在MainActivity类的onCreate()方法中,让我们替换HelloKotlin初始化:

HelloKotlin().displayKotlinMessage(view)

我们将其替换为一个初始化,该初始化还传递了一条消息:

HelloKotlin("Get ready for a fun game of Tic Tac Toe").displayKotlinMessage(view)

单击 FloatingActionButton 时,传递的消息将显示在底部。

数据类

构建应用程序时,我们大多数时候需要的是仅用于存储数据的类。在 Java 中,我们通常使用 POJO。在 Kotlin 中,有一个称为数据类的特殊类。

假设我们想为我们的 TicTacToe 游戏保留一个记分牌。我们将如何存储每个游戏会话的数据?

在 Java 中,我们将创建一个 POJO,用于存储有关游戏会话的数据(游戏结束时的板和游戏的获胜者):

public class Game {

    private char[][] gameBoard;
    private char winner;

    public Game(char[][] gameBoard, char winner) {
        setGameBoard(gameBoard);
        setWinner(winner);
    }

    public char[][] getGameBoard() {
        return gameBoard;
    }

    public void setGameBoard(char[][] gameBoard) {
        this.gameBoard = gameBoard;
    }

    public char getWinner() {
        return winner;
    }

    public void setWinner(char winner) {
        this.winner = winner;
    }
}

在 Kotlin 中,这大大简化为:

data class Game(var gameBoard: Array<CharArray>, var winner: Char)

前一行代码与前面的 26 行 Java 代码执行相同的操作。它声明了一个Game类,该类在其主构造函数中接受两个参数。正如前面所述,不需要getterssetters

Kotlin 中的数据类还带有许多其他方法:

  • equals()/hashCode()

  • toString()

  • copy()

如果您曾经编写过任何 Java 代码,您应该熟悉equals()hashCode()toString()。让我们继续讨论copy()

当您想要创建对象的副本,但部分数据已更改时,copy()方法非常方便,例如:

data class Student(var name: String, var classRoomNo: Int, var studentId: Int) // 1

var anna = Student("Anna", 5, 1) // 2
var joseph = anna.copy("Joseph", studentId = 2) // 3

在前面的代码片段中:

  1. 我们声明了一个名为Student的数据类。它在其主构造函数中接受三个参数:nameclassRoomNostudentId

  2. anna变量是Student的一个实例,具有以下属性:name:AnnaclassRoomNo:5studentId:1

  3. 变量joseph是通过复制anna并更改两个属性——namestudentId而创建的。

对象

在我们深入讨论对象之前,让我们对 TicTacToe 游戏进行一些添加。让我们初始化我们的视图。将以下代码添加到MainActivity类中的onCreate()方法:

turnTextView = findViewById(R.id.turnTextView) as TextView // 1

tableLayout = findViewById(R.id.table_layout) as TableLayout // 2

startNewGame(true)

将以下方法添加到MainActivity类中:

private fun startNewGame(setClickListener: Boolean) {
    turn = 'X'
    turnTextView?.text = 
    String.format(resources.getString(R.string.turn), turn)
    for (i in 0 until gameBoard.size) {
        for (j in 0 until gameBoard[i].size) {
            gameBoard[i][j] = ' '
            val cell = (tableLayout?.getChildAt(i) as 
            TableRow).getChildAt(j) as TextView
            cell.text = ""
            if (setClickListener) {
            }
        }
    }
}

private fun cellClickListener(row: Int, column: Int) {
    gameBoard[row][column] = turn
    ((tableLayout?.getChildAt(row) as TableRow).getChildAt(column) as TextView).text = turn.toString()
    turn = if ('X' == turn) 'O' else 'X'
    turnTextView?.text = String.format(resources.getString(R.string.turn), turn)
}
  1. 在一和二中,我们使用 XML 布局中对应的视图初始化turnTextViewtableLayout

  2. startNewGame()中:

  • 我们重新初始化turn

  • 我们将turnTextView设置为显示turn的值

  • 我们重置了gameBoard的所有值

  • 我们将tableLayout的所有单元格重置为空字符串

  1. cellClickListener()中:
  • 我们根据传递给cellClickListener()的参数将turn的值设置为gameBoard的特定元素

  • 我们还将tableLayout上对应单元格的值更改为turn

  • 我们根据turn的先前值将turn的值更改为下一个玩家

  • 我们将turnTextView上显示的值更改为turn的新值

每次单元格被点击时,我们需要调用cellClickListener()。为此,我们需要为每个单元格添加一个点击侦听器。在 Android 中,我们使用View.OnClickListener。由于View.OnClickListener是一个接口,我们通常创建一个实现其方法的类,并将该类设置为我们的点击侦听器。

Java 和 Kotlin 都有简化此过程的方法。在 Java 中,您可以通过使用匿名内部类来绕过它。匿名内部类允许您同时声明和创建类的实例:

// Java Anonymous Inner Class
cell.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {

    }
});

在上述代码中,我们声明并创建了一个实现View.OnClickListener接口的类的实例。

在 Kotlin 中,这是使用对象表达式完成的。

将以下代码添加到startNewGame()方法中if(setClickListener)语句的主体中:

cell.setOnClickListener(object : View.OnClickListener {
    override fun onClick(v: View?) {
        cellClickListener(i, j)
    }
})

Kotlin 允许我们进一步简化先前的代码行。我们将在第六章中讨论这一点,函数和 Lambda,当我们谈论Lambda时。

构建并运行。现在,当您点击任何单元格时,其中的文本将更改为turnTextView的文本,并且turnTextView的值也将更改为下一个玩家的值:

摘要

在本章中,我们学习了类、数据类和对象表达式,同时初始化了我们的视图并为我们的游戏应用程序添加了额外的逻辑。

在下一章中,我们将深入讨论类型检查和空安全性,以及为什么 Kotlin 的这些特性使其成为最安全的语言之一。

第五章:类型检查和空安全

如第一章所述,为 Android 开发设置,Kotlin 带来的一个伟大特性是空安全。在本章中,我们将学习 Kotlin 是如何成为一个空安全语言的,以及我们如何充分利用它。

在本章结束时,我们将学到:

  • 非空和可空类型

  • 安全调用操作符

  • Elvis 操作符

  • !!操作符

  • 安全和不安全的类型转换操作符

空安全

Java 和其他许多语言中开发者最常见的痛点之一是访问空引用的成员。在大多数语言中,这会导致运行时的空引用异常。大多数 Java 开发者将其称为NullPointerException

Kotlin 旨在尽可能消除空引用和异常的可能性。如第一章中所述,为 Android 开发设置,在 Kotlin 中,你可能遇到NullPointerException的四个可能原因:

  • 外部 Java 代码

  • 显式调用抛出NullPointerException

  • 使用!!操作符(我们稍后会学到更多关于这个操作符的知识)

  • 关于初始化的数据不一致性

那么 Kotlin 是如何确保这一点的呢?

可空和非空类型

可空类型是允许保存null值的引用,而非空类型是不能保存null值的引用。

Kotlin 的类型系统设计用于区分这两种引用类型。可空类型通过在类型末尾添加?来声明。例如:

var name: String = "Anna" // non-nullable String
var gender: String? = "Female" //nullable String

name = null // will not compile
gender = null // will compile

print("Length of name is ${name.length}") // will compile

print("Length of gender is ${gender.length}") // will not compile

在先前的代码中有一些需要注意的事项:

  • name不能被赋予null值,因为它是非空类型

  • 另一方面,gender可以被赋予null值,因为它声明为可空类型

  • 无法像访问name的成员方法或属性一样访问gender的成员方法或属性

有多种方式可以访问可空类型的方法或属性。你可以在条件中检查null并访问方法或属性。例如:

if (gender != null) {
    print("Length of gender is ${gender.length}") 
}

编译器跟踪null检查的结果,因此允许在if条件的主体中调用length。这是一个智能转换的例子:

  • 使用安全调用操作符(?.)

  • 使用 Elvis 操作符(?:)

  • 使用!!操作符

  • 执行智能转换

智能转换是 Kotlin 中的一个智能功能,编译器会跟踪if语句的结果,并在需要时自动执行转换。

安全调用操作符

访问可空类型的方法或属性的另一种方式是使用安全调用操作符:

val len = gender?.length
print("Length of gender is $len")

在先前的代码中,如果 gender 不为null,则len的值将是gender.length的结果。否则,len的值将为null

如果在gendernull时不需要执行任何操作,使用安全调用操作符是很好的。如果我们想在gendernull时为len赋予不同的值,我们可以将安全调用操作符与Elvis 操作符结合使用。

Elvis 操作符

Elvis 操作符类似于 Java 中的三元if操作符。它是简化if-else语句的一种方式。例如:

val len = if (gender != null) gender.length else 0

代码可以简化为:

val len = gender?.length ?: 0

在先前的代码中,如果gender?.length的值为null,则len的值将为0

!!操作符

如果我们不在乎遇到空指针异常,那么我们可以使用!!操作符。例如:

val len = gender!!.length

如果gendernull,则会导致空指针异常。

只有在确定变量的值或者不在乎遇到空指针异常时才使用!!操作符。

类型检查

就像在 Java 中一样,你可以确认变量的类型。在 Kotlin 中,使用is操作符来实现。例如:

if (gender is String) {
    println("Length of gender is ${gender.length}") // gender is automatically cast to a String
}

就像之前的 null 检查一样,编译器会跟踪类型检查的结果,并自动将 gender 转换为 String,从而允许调用 gender.length。这被称为智能转换。

转换运算符

要将变量转换为另一种类型,您必须使用转换运算符(as):

var fullname: String = name as String

如果您尝试将变量转换为的类型不是您要转换的类型,则转换运算符将抛出错误。为了防止这种情况,您可以使用安全转换运算符as?):

var gen: String? = gender as? String

安全转换运算符不会抛出错误,而是在转换不可能时返回 null

总结

在本章中,我们学习了 Kotlin 如何帮助使您的代码具有空安全性的不同方式。 Kotlin 中有不同的运算符用于实现这一点,我们讨论了如何使用它们。

在下一章中,我们将在学习 Kotlin 中的函数和 lambda 的同时完成我们的井字棋游戏。

第六章:函数和 Lambda

在本章中,我们将完成 TicTacToe 游戏的工作,并了解 Kotlin 中的函数。

在这个过程中,我们将:

  • 了解函数

  • 了解高阶函数及其使用方法

  • 了解 lambda 及其使用方法

函数

在 Kotlin 中,函数的声明格式如下:

return类型和parameters是可选的。没有return类型的函数默认返回UnitUnit相当于 Java 中的void

作为其主体的单个表达式的函数也可以省略大括号:

fun addStudent(name: String, age:Int, classRoomNo: Int = 1, studentId: Int) : Student = Student(name, classRoomNo, studentId, age)

如果类型可以被编译器推断出来,则return类型也可以省略:

fun addStudent(name: String, age:Int, classRoomNo: Int = 1, studentId: Int) = Student(name, classRoomNo, studentId, age)

参数

在 Kotlin 中,使用帕斯卡符号(parameter_name:Type)定义函数参数。每个参数的类型都必须明确声明。函数声明中的参数可以被赋予默认值。格式为:parameter_name:Type = defaultValue。例如:

data class Student(var name: String, var classRoomNo: Int, var studentId: Int, var age: Int)

fun addStudent(name: String, age:Int, classRoomNo: Int = 1, studentId: Int) : Student {

 return Student(name, classRoomNo, studentId, age)
}

var anna = addStudent("Anna", 18, 2, 1)
var joseph = addStudent(name = "Joseph", age = 19, studentId = 2)

在这个例子中:

  • 在调用addStudent()函数时,可以省略classRoomNo参数。例如,joseph将具有默认的classRoomNo值为1

  • 在某些情况下,如果没有将所有参数传递给函数,则传递的参数必须在其参数名称之前。

高阶函数和 lambda

术语高阶函数指的是一个函数,它要么接受另一个函数作为参数,要么返回一个函数,或者两者兼而有之。例如:

// 1
fun logStudent(name: String, age:Int, createStudent:(String, Int) -> Student) {
    Log.d("student creation", "About to create student with name $name")
    val student = createStudent(name, age)
    Log.d("student creation", "Student created with name ${student.name} and age ${student.age}")
}

// 2
logStudent(name = "Anna", age = 20, createStudent = { name: String, age: Int -> Student(name, 1, 3, age)})

在这里,logStudent()函数接受三个参数:nameagecreateStudentcreateStudent是一个函数,它接受一个String和一个Int作为参数,并返回一个Student对象。

createStudent函数未声明,而是作为表达式传递给logStudent()函数。这称为lambda 表达式

Lambda 表达式

Lambda 表达式是一个匿名函数,它不是声明的,而是立即作为表达式传递的。

让我们继续在 TicTacToe 应用程序中使用 lambda 表达式。打开MainActivity.kt。在startNewGame()函数中,替换以下代码行:

cell.setOnClickListener(object : View.OnClickListener {
    override fun onClick(v: View?) {
        cellClickListener(i, j)
    }
})

用以下代码替换它们:

cell.setOnClickListener { cellClickListener(i, j) } 

在前面的代码行中,我们有一个匿名对象,它实现了一个具有单个抽象方法(onClick())的 Java 接口。所有这些都可以用一个简单的 lambda 表达式来替代。

单个抽象方法SAM),通常指的是接口中的功能方法。该接口通常只包含一个称为 SAM 或功能方法的抽象方法。

现在,构建并运行以查看应用程序的状态:

让我们继续利用我们迄今为止学到的所有知识,以便完成游戏的工作。

Android Studio 提供了一个默认的工具链,支持大多数 JAVA 8 功能,包括 lambda 表达式。强烈建议您使用默认的工具链,并禁用所有其他选项,如 jackoptions 和 retrolambda。

实现游戏状态检查

在本节中,我们将处理一些函数,以帮助我们找出游戏的赢家。

首先,将以下函数添加到MainActivity类中:

private fun isBoardFull(gameBoard:Array<CharArray>): Boolean {
    for (i in 0 until gameBoard.size) { 
        for (j in 0 until gameBoard[i].size) { 
            if(gameBoard[i][j] == ' ') {
                return false
            }
        }
    }
    return true
}

此函数用于检查游戏板是否已满。在这里,我们遍历棋盘上的所有单元格,如果有任何一个单元格为空,则返回false。如果没有一个单元格为空,则返回true

接下来,添加isWinner()方法:

private fun isWinner(gameBoard:Array<CharArray>, w: Char): Boolean {
    for (i in 0 until gameBoard.size) {
        if (gameBoard[i][0] == w && gameBoard[i][1] == w && 
        gameBoard[i][2] == w) {
            return true
        }

        if (gameBoard[0][i] == w && gameBoard[1][i] == w && 
        gameBoard[2][i] == w) {
            return true
        }
    }
    if ((gameBoard[0][0] == w && gameBoard[1][1] == w && gameBoard[2]
    [2] == w) ||
            (gameBoard[0][2] == w && gameBoard[1][1] == w && 
        gameBoard[2][0] == w)) {
        return true
    }
    return false
}

在这里,您可以检查传递的字符是否是赢家。如果字符在水平、垂直或对角线行中出现三次,则该字符是赢家。

现在添加checkGameStatus()函数:

private fun checkGameStatus() {
    var state: String? = null
    if(isWinner(gameBoard, 'X')) {
        state = String.format(resources.getString(R.string.winner), 'X')
    } else if (isWinner(gameBoard, 'O')) {
        state = String.format(resources.getString(R.string.winner), 'O')
    } else {
        if (isBoardFull(gameBoard)) {
            state = resources.getString(R.string.draw)
        }
    }

    if (state != null) {
        turnTextView?.text = state
        val builder = AlertDialog.Builder(this)
        builder.setMessage(state)
        builder.setPositiveButton(android.R.string.ok, { dialog, id ->
            startNewGame(false)

        })
        val dialog = builder.create()
        dialog.show()

    }
}

上述函数利用isBoardFull()isWinner()函数来确定游戏的赢家是谁。如果 X 和 O 都没有赢,而且棋盘已满,则是平局。显示一个警报,显示游戏的赢家或告诉用户游戏是平局的消息。

接下来,在cellClickListener()函数的末尾添加一个调用checkGameStatus()

构建并运行:

最后,实现FloatingActionButton的功能。在onCreate()函数中,将以下内容替换为:

fab.setOnClickListener { view -> HelloKotlin("Get ready for a fun game of Tic Tac Toe").displayKotlinMessage(view) }

将其替换为:

fab.setOnClickListener {startNewGame(false)}

再次构建并运行。现在,当您点击FloatingActionButton时,棋盘将被清空,以便您重新开始游戏:

总结

在本章中,我们学习了如何在 Kotlin 中使用函数和 lambda,并完成了对我们的 TicTacToe 游戏的工作。

在接下来的几章中,我们将学习如何在 Android 上使用 Google 位置服务以及执行网络调用,同时致力于创建基于位置的闹钟。

第七章:开发您的基于位置的闹钟

了解用户位置并为他们提供定制服务是 Android 设备的强大功能之一。 Android 应用程序开发人员可以利用这一强大功能,为其应用程序的用户提供迷人的服务。因此,了解 Google 位置服务、Google Maps API 和位置 API 对于 Android 应用程序的开发人员非常重要。

在本章中,我们将开发我们自己的基于位置的闹钟LBA),并在开发应用程序的过程中,我们将了解以下内容:

  • 基于 Android 活动创建地图

  • 在 Android 应用程序中使用 Google Maps

  • 注册并获取 Google Maps 活动所需的密钥的过程

  • 为用户提供输入的屏幕

  • 在下一章中,通过添加闹钟功能并使用 Google 位置服务来完成我们的应用程序并创建一个可行的模型

创建一个项目

我们将看看创建 LBA 所涉及的步骤。我们将使用我们最喜欢的 IDE,Android Studio,来开发 LBA。

让我们开始启动 Android Studio。一旦它启动并运行,点击开始一个新的 Android Studio 项目。如果您已经打开了一个项目,请点击文件|新建项目。

在下一个屏幕上,输入这里显示的详细信息:

  • 应用程序名称LocationAlarm

  • 公司域:Android Studio 使用域名来生成我们开发的应用程序的包名。包确保我们的应用程序在 Play 商店中获得唯一标识符。通常,包名将是域名的反向,例如,在这种情况下将是com.natarajan.locationalarm

  • 项目位置:我们希望开发和保存项目代码的路径。您可以选择并选择您正在开发应用程序的路径。由于我们正在使用 Kotlin 开发我们的应用程序,因此我们必须选择包括 Kotlin 支持:

在接下来的屏幕上,我们将根据以下内容做出关于我们针对的 Android 设备的决定:

  • 它们提供的 API

  • 表单因素

对于我们的应用程序,我们将选择手机和平板电脑,并选择 API 为 API 15。 API 选择框下方的文本告诉我们,通过选择 API 15 及更高版本,我们将选择使我们的应用程序在大约 100%的设备上运行。

帮助我选择选项将帮助您了解按 Android 版本(API)分组的全球 Android 设备的分布。

我们不会在任何其他表单因素上运行我们的应用程序;因此,我们可以跳过这些选择区域,然后点击下一步:

在下一个屏幕上,我们将有一个选项来向我们的应用程序添加一个活动。

Android Studio 通过提供最常用的活动的现成模板,使开发人员更容易地包含他们的应用程序所需的活动类型。

我们正在开发 LBA,因此我们需要一个显示设置了闹钟的位置的地图。

点击Google Maps Activity,然后点击下一步:

我们将在下一个屏幕上配置活动。一般来说,原生 Android 应用程序是由 Kotlin/Java 类和 XML 定义的用户界面组合而成。屏幕上提供了以下输入来配置我们的应用程序:

  • 活动名称:这是我们地图活动的 Kotlin 类的名称。当我们选择地图活动时,默认情况下会显示名称 MapsActivity,我们将在这里使用相同的名称。

  • 布局名称:我们将用于设计用户界面的 XML 布局的名称。

  • 标题:我们希望应用程序为此活动显示的标题。我们将保留此标题为 Map,这是默认显示的。

完成这些条目后,点击完成按钮:

点击按钮后,我们将看到“构建'LocationAlarm' Gradle 项目信息”屏幕。

生成 Google Maps API 密钥

一旦构建过程完成,我们将看到以下资源文件屏幕默认打开并由 Android Studio 显示:

文件默认命名为google_maps_api.xml。该文件清楚地指示在运行应用程序之前,我们需要获取 Google Maps API 密钥。获取应用程序的 Google Maps API 密钥的过程将详细列出。

生成的密钥应该替换文件中提到的占位符 YOUR_KEY_HERE:

<resources>
 <!--
TODO: Before you run your application, you need a Google Maps API key.

To get one, follow this link, follow the directions and press "Create" at the end:

https://console.developers.google.com/flows/enableapi?apiid=maps_android_backend&keyType=CLIENT_SIDE_ANDROID&r=00:ED:1B:E2:03:B9:2E:F4:A9:0F:25:7A:2F:40:2E:D2:89:96:AD:2D%3Bcom.natarajan.locationalarm

You can also add your credentials to an existing key, using these values:
Package name:
 00:ED:1B:E2:03:B9:2E:F4:A9:0F:25:7A:2F:40:2E:D2:89:96:AD:2D
SHA-1 certificate fingerprint:
 00:ED:1B:E2:03:B9:2E:F4:A9:0F:25:7A:2F:40:2E:D2:89:96:AD:2D

Alternatively, follow the directions here:
 https://developers.google.com/maps/documentation/android/start#get-key

Once you have your key (it starts with "AIza"), replace the "google_maps_key"
 string in this file.
 -->
<string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">YOUR_KEY_HERE</string>
 </resources>

我们将使用文件中提供的链接生成我们应用程序所需的密钥。

console.developers.google.com 需要用户使用他们的 Google ID 进行登录。一旦他们登录,将会出现创建项目和启用 API 的选项。

选择并复制完整的链接(console.developers.google.com/flows/enableapi?apiid=maps_android_backend&keyType=CLIENT_SIDE_ANDROID&r=00:ED:1B:E2:03:B9:2E:F4:A9:0F:25:7A:2F:40:2E:D2:89:96:AD:2D;com.natarajan.locationalarm)并在您喜欢的浏览器中输入:

一旦用户登录到控制台,用户将被要求在 Google API 控制台中为 Google Maps Android API 注册应用程序。

我们将看到一些选项:

  • 选择一个项目

  • 创建一个项目

如下文本所示,在选择应用程序将注册的项目时,用户可以使用一个项目来管理所有开发的应用程序的 API 密钥,或者选择为每个应用程序使用不同的项目。

使用一个项目来管理各种 Android 应用程序所需的所有 API 密钥,或者为每个应用程序使用一个项目,这取决于用户。在撰写本文时,默认情况下,用户将被允许免费创建 12 个项目。

接下来,您需要阅读并同意 Google Play Android 开发者 API 和 Firebase API/服务条款的条款和条件(console.developers.google.com/flows/enableapi?apiid=maps_android_backend&keyType=CLIENT_SIDE_ANDROID&r=00:ED:1B:E2:03:B9:2E:F4:A9:0F:25:7A:2F:40:2E:D2:89:96:AD:2D;com.natarajan.locationalarm ):

选择创建一个项目并同意条款和条件。完成后,点击同意并继续

一旦项目创建成功,用户将看到一个屏幕。屏幕上显示“项目已创建,已启用 Google Maps Android API。接下来,您需要创建一个 API 密钥以调用 API。”用户还将看到一个按钮,上面写着创建 API 密钥

单击创建 API 密钥按钮后,用户将看到一个控制台,上面弹出一个消息,显示 API 密钥已创建。这是我们需要在应用程序中使用的 API 密钥:

复制 API 密钥,然后用生成的 API 密钥替换google_maps_api.xml文件中的 YOUR_API_KEY 文本,如下所示:

生成的带有生成的 Google Maps API 密钥的文件应该看起来像这样:

开发人员可以通过登录 Google API 控制台来检查生成的 API 密钥,并交叉检查为项目专门生成的正确 API 密钥的使用情况:

现在我们已经生成了 API 密钥并修改了文件以反映这一点,我们已经准备好分析代码并运行应用程序。

快速回顾一下,我们创建了包括 Google Maps 活动的应用程序,并创建了布局文件。然后我们生成了 Google Maps API 密钥并替换了文件中的密钥。

运行应用程序

要运行应用程序,请转到 Run | Run app 或单击播放按钮。

Android Studio 将提示我们选择部署目标,即具有开发人员选项和 USB 调试启用的物理设备,或者用户设置的虚拟设备,也称为模拟器。

一旦我们选择其中一个选项并点击“确定”,应用程序将构建并运行到部署目标上。应用程序将启动并运行,我们应该看到地图活动加载了悉尼的标记:

了解代码

我们成功运行了应用程序,现在是时候深入了解代码,了解它是如何工作的。

让我们从MapsActivity.kt Kotlin 类文件开始。

MapActivity类扩展了AppCompatActivity类,并实现了OnMapReadCallback接口。我们有一对变量,GoogleMapmMapbtn按钮初始化。

重写onCreate方法,当应用程序启动时,将从 XML 文件activity_maps.xml中加载内容。

从资源文件设置mapFragmentbtn的资源:

class MapsActivity : AppCompatActivity(), OnMapReadyCallback {

private lateinit var mMap: GoogleMap
 override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_maps)
// Obtain the SupportMapFragment and get notified when the map is ready to be used.
val mapFragment = supportFragmentManager
.findFragmentById(R.id.map) as SupportMapFragment
         mapFragment.getMapAsync(this)
     }

 }

自定义代码

默认生成的代码显示了悉尼的市场。这里显示的onMapReady方法在地图准备就绪并加载并显示标记时被调用。位置是根据提到的LatLng值找到的:

override fun onMapReady(googleMap: GoogleMap) {
 mMap = googleMap
// Add a marker in Sydney and move the camera
val sydney= LatLng(-33.852,151.211)
mMap.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))
mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))
}

现在让我们自定义此代码以在印度泰米尔纳德邦金奈上显示标记。要进行更改,第一步是了解LatLng代表什么。

纬度和经度一起用于指定地球上任何部分的精确位置。在 Android 中,LatLng类用于指定位置。

查找地点的纬度和经度

在浏览器中使用 Google Maps 可以轻松找到地点的纬度和经度。为了我们的目的,我们将在我们喜欢的浏览器中启动 Google Maps。

搜索您需要找到纬度和经度的位置。在这里,我们搜索位于印度泰米尔纳德邦金奈的智障儿童特殊学校 Vasantham。

一旦我们找到了我们搜索的位置,我们可以在 URL 中看到纬度和经度的值,如下所示:

我们搜索到的地方的纬度和经度值分别为 13.07 和 80.17。让我们继续在代码中进行以下更改。

onMapReady方法中,让我们进行以下更改:

  • Sydney变量重命名为chennai

  • 将 Lat 和 Lng 从悉尼更改为金奈

  • Marker文本更改为马德拉斯的标记

  • newLatLng更改为以chennai作为输入值

override fun onMapReady(googleMap: GoogleMap) {
 mMap = googleMap
// Add a marker in Chennai and move the camera
val chennai = LatLng(13.07975, 80.1798347)
 //val chennai = LatLng(-34.0, 151.0)
mMap.addMarker(MarkerOptions().position(chennai).title("Marker in Chennai"))
 mMap.moveCamera(CameraUpdateFactory.newLatLng(chennai))
 }

当我们保存所做的更改并再次运行应用程序时,我们将能够看到地图现在加载了位于印度金奈的标记:

一旦我们触摸标记,我们应该能够看到“马德拉斯的标记”文本显示在红色标记的顶部:

XML 布局

我们已经详细查看了 Kotlin 类,以及自定义 Lat 和 Lng 输入的方法。

让我们快速检查 XML 布局文件。我们还将了解添加一个按钮的过程,该按钮将带我们到一个屏幕,通过该屏幕用户将能够输入警报的 Lat 和 Lng 输入。

activity_maps.xml文件中,我们有地图片段和按钮元素包装在LinearLayoutCompat中,如下所示。我们将按钮元素链接到onClickSettingsButton方法:

<android.support.v7.widget.LinearLayoutCompat 

android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_weight="1.0"><fragment
android:id="@+id/map"
android:layout_weight="0.8"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.natarajan.locationalarm.MapsActivity" />

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.2"
android:id="@+id/settingsbtn"
android:onClick="onClickSettingsButton"
android:text="@string/Settings"/>

 </android.support.v7.widget.LinearLayoutCompat>

MapsActivity Kotlin 类中,我们可以定义一个名为onClickSettingsButton的方法,并在调用相同的方法时启动另一个名为SETTINGACTVITY的活动,如下所示:

fun onClickSettingsButton(view: View) {
 val intent = Intent("android.intent.action.SETTINGACTIVITY")
 startActivity(intent)
 }

开发用户输入屏幕

当点击Settings按钮时,我们的应用程序将带用户进入一个屏幕,用户可以在该屏幕上输入新位置的纬度和经度值,用户希望为该位置设置警报。

我们有一个非常简单的输入屏幕。我们有一个包含一对EditTextLinearLayout,一个用于纬度输入,另一个用于经度输入。这些编辑文本后面跟着一个按钮,允许用户提交输入的新位置坐标。

我们还有一个与按钮关联的onClickButton方法,当用户点击按钮时调用:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

     <EditText
android:id="@+id/latText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:hint='Latitude'
android:inputType="numberDecimal" />

     <EditText
android:id="@+id/langText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:hint="Longitude"
android:inputType="numberDecimal" />

     <Button
android:id="@+id/alarmbtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onClickButton"
android:text="Ok" />

 </LinearLayout>

我们已经准备好用户输入的 XML 布局;现在让我们创建一个新的 Kotlin 活动类,该类将使用这个设置的 XML 并与用户交互。

SettingsActivity类扩展了AppCompatActivity,包含了一对编辑文本元素和初始化的按钮元素。变量通过它们的 ID 从资源文件中识别和设置为正确的资源。当活动被调用和加载时,活动加载settings_activity XML。

onClickButton方法中,我们有一个简单的 Toast 消息,显示警报已设置。在接下来的章节中,我们将保存输入的内容,并在用户进入感兴趣的位置时触发警报:

class SettingsActivity : AppCompatActivity() {

public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
         setContentView(R.layout.settings_activity)

}
fun onClickButton(view: View) {
         Toast.makeText(this, "Alarm Set", Toast.*LENGTH_LONG*).show()
     }
}

当用户在输入纬度和经度后点击OK按钮时,将显示 Toast 消息,如下所示:

AndroidManifest 文件

清单文件是项目中最重要的文件之一。在这个文件中,我们必须列出我们打算在应用程序中使用的所有活动,并提供有关我们用于 Google Maps API 的 API 密钥的详细信息。

在清单文件中,我们有以下重要的指针:

  • 我们的应用程序使用ACCESS_FINE_LOCATION权限。这是为了获取用户位置的详细信息;我们需要这样做以便在用户到达设置的位置时启用警报。

ACCESS_COARSE_LOCATION是启用应用程序获取NETWORK_PROVIDER提供的位置详细信息的权限。ACCESS_FINE_LOCATION权限使应用程序能够获取NETWORK_PROVIDERGPS_PROVIDER提供的位置详细信息。

  • 我们有 Android geo API 密钥的元数据,这只是我们生成并放置在google_maps_api.xml中的 API 密钥。

  • 我们有一个启动器 MAIN 活动,它在钦奈位置上启动带有标记的地图。

  • 我们还有默认的活动设置,当点击提交按钮时触发:

*<?*xml version="1.0" encoding="utf-8"*?>* <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.natarajan.locationalarm">
*<!--
          T*he ACCESS_COARSE/FINE_LOCATION permissions are not required to use
          Google Maps Android API v2, but you must specify either coarse or fine
          location permissions for the 'MyLocation' functionality. 
     --><uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

     <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">
*<!--
        *      The API key for Google Maps-based APIs is defined as a string resource.
              (See the file "res/values/google_maps_api.xml").
              Note that the API key is linked to the encryption key used to sign the APK.
              You need a different API key for each encryption key, including the release key that is used to
              sign the APK for publishing.
              You can define the keys for the debug and release targets in src/debug/ and src/release/. *-->* <meta-data android:name="com.google.android.geo.API_KEY" android:value="@string/google_maps_key" />

         <activity android:name=".MapsActivity" android:label="@string/title_activity_maps">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>

         <activity android:name=".SettingsActivity">
             <intent-filter>
                 <action android:name="android.intent.action.SETTINGACTIVITY" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>

     </application>

 </manifest>

Build.gradle

build.gradle文件包括所需的 Google Maps 服务的依赖项。我们必须包括来自 Google Play 服务的 Play 服务地图。从 Google Play 服务中,我们包括我们感兴趣的服务。在这里,我们希望有一个地图服务可用,因此我们包括play-services-maps

apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android {
     compileSdkVersion 26
defaultConfig {
         applicationId "com.natarajan.locationalarm" minSdkVersion 15
targetSdkVersion 26
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:26.1.0'
 implementation 'com.google.android.gms:play-services-maps:11.8.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' }

总结

在本章中,我们讨论并学习了如何创建自己的 LBA。我们了解了 Google Maps API 的细节,API 密钥的生成,地图用户界面的创建,向地图添加标记,自定义标记,为用户输入创建用户界面屏幕等等。

我们还讨论了清单文件、build.gradle文件和 XML 布局文件以及相应的 Kotlin 类中的重要组件。在下一章中,我们将使用共享首选项保存从用户那里收到的输入,使用 Google API 的基于位置的服务,并在用户进入位置时启用和触发警报。

第八章:使用 Google 的位置服务

在上一章中,我们构建了我们的基于位置的警报(LBA)应用程序,包括 Google 地图,添加了标记和自定义位置,并为接收用户输入设置了 UI。

我们现在将专注于将 Google 位置 API 与我们的应用程序集成,并在用户的位置上接收更新。用户输入的感兴趣的位置将被保存并与接收到的警报位置更新进行比较,以便在用户到达感兴趣的区域时触发警报。

Google 提供了各种方式来访问和识别用户的位置。Google 位置 API 提供了关于用户上次已知位置的信息,显示位置地址,接收位置更改的持续更新等。开发人员可以添加地理围栏 - 围绕地理区域的围栏 - 任何时候用户通过地理围栏时都可以生成警报。

在本章中,我们将学习如何:

  • 使用 Google 位置 API

  • 接收用户当前位置的更新

  • 利用用户共享首选项来保存用户感兴趣的位置

  • 匹配并在用户到达感兴趣的位置时显示警报

本章的主要重点是介绍和解释我们应用程序中位置的概念和用法。考虑到这一目标,这些概念是通过应用程序在前台运行时接收位置更新来解释的。所需权限的处理也以更简单的方式处理。

集成共享首选项

我们的应用程序用户将输入他们希望触发警报的所需位置。用户输入位置的“纬度”和“经度”,以便我们将其与用户所在的当前位置进行比较,我们需要将他们输入的详细信息存储为所需位置。

共享首选项是基于文件的存储,包含键值对,并提供了更容易的读写方式。共享首选项文件由 Android 框架管理,文件可以是私有的或共享的。

让我们首先将共享首选项集成到我们的代码中,并保存用户在 UI 屏幕上输入的纬度和经度用于警报。

共享首选项为我们提供了以键值对的形式保存数据的选项。虽然我们可以使用通用的共享首选项文件,但最好为我们的应用程序创建一个特定的共享首选项文件。

我们需要为我们的应用程序定义一个共享首选项文件的字符串。导航到 app | src | main | res | values | strings.xml。让我们添加一个新的字符串PREFS_NAME,并将其命名为LocationAlarmFile

<resources>
     <string name="app_name">LocationAlarm</string>
     <string name="title_activity_maps">Map</string>
     <string name="Settings">Settings</string>
    <string name="PREFS_NAME">LocationAlarmFile</string> </resources>

我们将在我们的SettingsActivity类中添加以下代码,以捕获用户输入并将其保存在共享首选项文件中。共享首选项文件通过在资源文件中引用字符串PREFS_NAME来打开,并且文件以MODE_PRIVATE打开,这表示该文件仅供我们的应用程序使用。

一旦文件可用,我们打开编辑器并使用putString将用户输入的纬度和经度作为字符串共享。

val sharedPref = this?.getSharedPreferences(getString(R.string.PREFS_NAME),Context.MODE_PRIVATE) ?: return with(sharedPref.edit()){ putString("userLat", Lat?.text.toString())
     putString("userLang",Lang?.text.toString())
     commit()

从共享首选项中读取和显示:

      val sharedPref = 
 this?.getSharedPreferences(getString(R.string.PREFS_NAME), 
      Context.MODE_PRIVATE) ?: return AlarmLat = 
     java.lang.Double.parseDouble(sharedPref.getString("userLat",   
 "13.07975"))
         AlarmLong = 
     java.lang.Double.parseDouble(sharedPref.getString("userLang", 
 "80.1798347"))

用户将收到有关设置警报的警报:

用户输入的纬度将存储并从共享首选项中读取并显示:

用户输入的经度也将从共享首选项中读取并显示:

添加权限

Google Play 服务提供了可以集成和使用的基于位置的服务。添加位置服务并使用它们需要权限来识别并从用户那里获取位置更新。

要使用来自 Play 服务的 Google 位置服务,我们需要在build.gradle文件中包含play-services-location

dependencies {
    compile 'com.google.android.gms:play-services-location:11.8.0'
}

重要的是从 Google Play 服务中包含应用程序所需的特定功能。例如,在这里我们需要位置服务,因此我们需要指定位置的服务。包含所有 Google Play 服务将使应用程序大小变得庞大;请求不真正需要的权限。

我们还需要在 AndroidManifest.xml 文件中添加访问精确定位的权限。这使我们可以从网络提供商和 GPS 提供商获取位置详细信息:

 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

在运行时,我们需要检查设备是否已启用位置;如果没有,我们将显示一条消息,请求用户启用位置并授予权限。

checkLocation 布尔函数用于判断设备是否已启用位置:

private fun checkLocation(): Boolean {
         if(!isLocationEnabled())
             Toast.makeText(this,"Please enable Location and grant permission for this app for Location",Toast.LENGTH_LONG).show()
         return isLocationEnabled();
     }

private fun isLocationEnabled(): Boolean {
     locationManager = getSystemService(Context.LOCATION_SERVICE) as 
     LocationManager
     return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
 }

位置 API 的集成

我们将集成位置 API 到我们的应用程序中以接收位置更新。位置 API 的集成涉及代码的一些更改。让我们详细讨论这些更改。

类和变量

Google 位置 API 的集成需要 MapsActivity 实现 GoogleAPIClientConnectionCallbacks 和连接失败监听器。让我们继续对 MapsActivity 进行更改。之前,MapsActivity 扩展了 AppCompatActivity 并实现了 OnMapReadyCallback 接口。现在,由于我们需要使用位置 API,我们还必须实现 GoogleAPIClientConnectionCallbacksonConnectionFailedListener,如下所示:

class MapsActivity : AppCompatActivity(), OnMapReadyCallback ,GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, com.google.android.gms.location.LocationListener {

我们声明了 GoogleMap 所需的变量和其他变量,用于存储来自用户和位置 API 的纬度和经度:

    private lateinit var mMap: GoogleMap
    private var newLat: Double? = null
    private var newLang: Double? = null
    private var chennai: LatLng? = null

    private var AlarmLat: Double? = null
    private var AlarmLong: Double? = null
    private var UserLat: Double? = null
    private var UserLong: Double? = null

     //location variablesprivate val TAG = "MapsActivity" private lateinit var mGoogleApiClient: GoogleApiClient
    private var mLocationManager: LocationManager? = null
    lateinit var mLocation: Location
    private var mLocationRequest: LocationRequest? = null

我们声明 UPDATE_INTERVAL,即我们希望从位置 API 接收更新的间隔,以及 FASTEST_INTERVAL,即我们的应用程序可以处理更新的速率。我们还声明 LocationManager 变量:

 private val UPDATE_INTERVAL = 10000.toLong() // 10 seconds rate at 
     //  which we would like to receive the updates
     private val FASTEST_INTERVAL: Long = 5000 // 5 seconds - rate at  
     //  which app can handle the update lateinit var locationManager: LocationManager

onCreate 函数中,我们为 UI 设置内容视图,并确保 GoogleApiClient 已实例化。我们还请求用户启用位置如下:

onCreate()

   override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.*activity_maps*)
         // Obtain the SupportMapFragment and get notified when the map    
         is ready to be used. val mapFragment = *supportFragmentManager
                * .findFragmentById(R.id.*map*) as SupportMapFragment
         mapFragment.getMapAsync(this)

         mGoogleApiClient = GoogleApiClient.Builder(this)
                 .addConnectionCallbacks(this)
                 .addOnConnectionFailedListener(this)
                 .addApi(LocationServices.API)
                 .build()

         mLocationManager =   
 this.getSystemService(Context.LOCATION_SERVICE) as  
         LocationManager
         checkLocation()
 }

Google API 客户端

声明、初始化和管理 Google API 客户端的连接选项需要在 Android 应用程序的生命周期事件中处理。一旦建立连接,我们还需要获取位置更新。

onStart 方法中,我们检查 mGoogleAPIClient 实例是否不为空,并请求初始化连接:

   override fun onStart() {
         super.onStart();
         if (mGoogleApiClient != null) {
             mGoogleApiClient.connect();
         }
     }

onStop 方法中,我们检查 mGoogleAPIClient 实例是否已连接,如果是,则调用 disconnect 方法:

    override fun onStop() {
         super.onStop();
         if (mGoogleApiClient.isConnected()) {
             mGoogleApiClient.disconnect();
         }
     }

如果出现问题并且连接被挂起,我们在 onConnectionSuspended 方法中请求重新连接:

     override fun onConnectionSuspended(p0: Int) {

         Log.i(TAG, "Connection Suspended");
         mGoogleApiClient.connect();
     }

如果 Google 位置 API 无法建立连接,我们通过获取错误代码来记录连接失败的原因:

     override fun onConnectionFailed(connectionResult: 
        ConnectionResult) {
     Log.i(TAG, "Connection failed. Error: " + 
        connectionResult.getErrorCode());
     }

onConnected 方法中,我们首先检查是否有 ACCESS_FINE_LOCATION 权限,并且 ACCESS_COARSE_LOCATION 确实存在于清单文件中。

一旦确保已授予权限,我们调用 startLocationUpdates() 方法:

override fun onConnected(p0: Bundle?) {

         if (ActivityCompat.checkSelfPermission(this,   
            Manifest.permission.ACCESS_FINE_LOCATION) != 
            PackageManager.PERMISSION_GRANTED && 
            ActivityCompat.checkSelfPermission(this, 
            Manifest.permission.ACCESS_COARSE_LOCATION) != 
            PackageManager.PERMISSION_GRANTED) {

             return;
         }
         startLocationUpdates();

fusedLocationProviderClient 提供当前位置详细信息,并将其分配给 mLocation 变量:

var fusedLocationProviderClient :
         FusedLocationProviderClient =   
         LocationServices.getFusedLocationProviderClient(this);
         fusedLocationProviderClient .getLastLocation()
         .addOnSuccessListener(this, OnSuccessListener<Location> {   
         location ->
                     if (location != null) {
                         mLocation = location;
 } }) }

startLocationUpdates 创建 LocationRequest 实例,并提供我们设置的更新参数。我们还调用 FusedLocationAPI 并请求位置更新:


 protected fun startLocationUpdates() {
          // Create the location request mLocationRequest = LocationRequest.create()
                 .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
                 .setInterval(UPDATE_INTERVAL)
                 .setFastestInterval(FASTEST_INTERVAL);
         // Request location updates if (ActivityCompat.checkSelfPermission(this, 
          Manifest.permission.ACCESS_FINE_LOCATION) !=   
          PackageManager.PERMISSION_GRANTED && 
          ActivityCompat.checkSelfPermission(this, 
          Manifest.permission.ACCESS_COARSE_LOCATION) != 
          PackageManager.PERMISSION_GRANTED) {
             return;
         }

      LocationServices.FusedLocationApi.requestLocationUpdates(
 mGoogleApiClient, mLocationRequest, this);
     }

onLocationChanged 方法是一个重要的方法,我们可以在其中获取用户当前位置的详细信息。我们还从共享偏好中读取用户输入的警报的纬度和经度。一旦我们获得了这两组详细信息,我们调用 CheckAlarmLocation 方法,该方法匹配纬度/经度并在用户到达感兴趣的区域时提醒用户:

override fun onLocationChanged(location: Location) { 
        val sharedPref =  
 this?.getSharedPreferences(getString(R.string.*PREFS_NAME*), 
      Context.*MODE_PRIVATE*)
           ?: return
        AlarmLat = 
      java.lang.Double.parseDouble(sharedPref.getString("userLat", 
 "13.07975"))
        AlarmLong = 
      java.lang.Double.parseDouble(sharedPref.getString("userLang", 
 "80.1798347"))

         UserLat = location.latitude
 UserLong = location.longitude
 val AlarmLat1 = AlarmLat val AlarmLong1 = AlarmLong
         val UserLat1 = UserLat
         val UserLong1 = UserLong

         if(AlarmLat1 != null && AlarmLong1 != null && UserLat1 != null 
         && UserLong1 != null){

      checkAlarmLocation(AlarmLat1,AlarmLong1,UserLat1,UserLong1)
         }
     }

匹配位置

startLocationUpdates方法根据我们设置的间隔持续提供用户的当前纬度和经度。我们需要使用获取到的纬度和经度信息,并将其与用户输入的用于设置警报的纬度和经度进行比较。

用户输入感兴趣的位置时,我们会显示警报消息,告知用户已经到达设置了警报的区域:

fun checkAlarmLocation(AlarmLat : Double, AlarmLong : Double, UserLat : Double,UserLong : Double) {

    Toast.makeText(this,"Check Alarm Called" + AlarmLat + "," + AlarmLong + "," + UserLat + "," + UserLong,Toast.*LENGTH_LONG* ).show()

         var LatAlarm: Double
         var LongAlarm: Double
         var LatUser: Double
         var LongUser: Double

         LatAlarm = Math.round(AlarmLat * 100.0) / 100.0;
         LongAlarm = Math.round(AlarmLong * 100.0) / 100.0;

         LatUser = Math.round(UserLat * 100.0) / 100.0;
         LongUser = Math.round(UserLong * 100.0) / 100.0;

Toast.makeText(this,"Check Alarm Called" + LatAlarm + "," + LongAlarm + "," + LatUser + "," + LongUser,Toast.*LENGTH_LONG* ).show()

         if (LatAlarm == LatUser && LongAlarm == LongUser) {
             Toast.makeText(this, "User has reached the area for which 
             alarm has been set", Toast.LENGTH_LONG).show();
         }
     }

摘要

在本章中,我们继续开发基于位置的闹钟应用程序,利用了来自 Google Play 服务的 Google 位置 API,并利用了提供警报的功能,当用户进入感兴趣的区域时。

我们学习了如何使用共享偏好来持久化用户输入的数据,检索相同的数据,并使用位置 API 来将用户的当前位置与感兴趣的区域进行匹配。

第九章:连接外部世界-网络

我们生活在数字通信的时代。手持设备在通信中起着重要作用,并影响人们的互动方式。在上一章中,我们讨论了 Android 的一个强大功能——识别用户的位置并根据位置定制服务。在本章中,我们将专注于 Android 设备最有用和强大的功能之一——网络和连接到外部世界。

虽然我们将简要介绍网络连接的重要概念和 Android 框架对网络的支持,但我们将重点关注内置的第三方库的配置和使用。我们还将学习如何从 URL 加载图像并在我们创建的示例应用程序中显示它。

我们将涵盖以下内容:

  • 网络连接

  • Android 框架对网络的支持

  • 使用内置库

  • 使用第三方库

网络连接

了解和识别用户连接的网络的状态和类型对于为用户提供丰富的体验非常重要。Android 框架为我们提供了一些类,我们可以使用它们来查找网络的详细信息:

  • ConnectivityManager

  • NetworkInfo

虽然ConnectivityManager提供有关网络连接状态及其变化的信息,但NetworkInfo提供有关网络类型(移动或 Wi-Fi)的信息。

以下代码片段有助于确定网络是否可用,以及设备是否连接到网络:

fun isOnline(): Boolean {
    val connMgr = getSystemService(Context.CONNECTIVITY_SERVICE) as  
    ConnectivityManager
    val networkInfo = connMgr.activeNetworkInfo
    return networkInfo != null && networkInfo.isConnected
}

isOnline()方法根据ConnectivityManager返回的结果返回一个Boolean——true 或 false。connMgr实例与NetworkInfo一起使用,以查找有关网络的信息。

清单权限

访问网络并发送/接收数据需要访问互联网和网络状态的权限。应用程序的清单文件必须定义以下权限,以便应用程序利用设备的网络:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

互联网权限允许应用程序通过启用网络套接字进行通信,而访问网络状态权限使其能够查找有关可用网络的信息。

Android 框架为应用程序提供了一个默认意图MANAGE_NETWORK_USAGE,用于管理网络数据。处理该意图的活动可以针对特定的应用程序进行实现:

  <intent-filter>
   <action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
   <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>

Volley 库

通过 HTTP 协议与 Web 服务器通信并以字符串、JSON 和图像的形式交换信息的能力使应用程序更加交互,并为用户提供丰富的体验。Android 具有一个名为Volley的内置 HTTP 库,可以直接进行信息交换。

除了使信息交换更加容易外,Volley还提供了更容易处理请求的整个生命周期的手段,如调度、取消、设置优先级等。

Volley非常适用于轻量级网络操作,并使信息交换更加容易。对于大型下载和流操作,开发人员应使用下载管理器。

同步适配器

使应用程序中的数据与 Web 服务器同步,使开发人员能够为用户提供丰富的体验。Android 框架提供了同步适配器,可以在定义的周期间隔内进行数据同步。

类似于Volley,同步适配器具有处理数据传输的生命周期和提供无缝数据交换的所有设施。

同步适配器实现通常包含一个存根验证器、一个存根内容提供程序和一个同步适配器。

第三方库

除了 Android 框架的内置支持外,我们还有相当多的第三方库可用于处理网络操作。其中,来自 Square 的Picasso和来自 bumptech 的Glide是广泛使用的图像下载和缓存库。

在这一部分,我们将专注于实现这两个库——PicassoGlide——从特定 URL 加载图像并在我们的示例应用程序中显示它。

网络调用绝对不应该在主线程上进行。这样做会导致应用程序变得不够响应,并创建应用程序无响应的情况。相反,我们应该创建单独的工作线程来处理这样的网络调用,并在请求被处理时提供信息。

Picasso

在这个示例项目中,让我们了解如何使用 Square 的Picasso库从指定的 URL 加载图像。

让我们创建一个新的 Android 项目,并将其命名为 ImageLoader。我们需要确保已经勾选了 Kotlin 支持。

对于 Image Loader 示例,我们可以选择空活动继续:

让我们将活动命名为MainActivity,默认情况下会出现这个活动,并将 XML 命名为activity_main

用户界面 - XML

生成的默认 XML 代码将包含一个TextView。我们需要稍微调整 XML 代码,用ImageView替换TextView。这个ImageView将提供一个占位符,用于显示从 URL 获取的图片,使用Picasso

接下来的 XML 代码显示了默认 XML 包含TextView;我们将用ImageView替换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="com.natarajan.imageloader.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>

修改后的 XML 中包含一个ImageView,如下面的代码块所示。我们可以通过从小部件中拖动ImageView或在 XML 布局中输入代码来轻松添加它。在ImageView中,我们已经标记它以显示启动器图标作为占位符:

*<?*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="com.natarajan.imageloader.MainActivity">

     <ImageView
         android:id="@+id/imageView"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         app:srcCompat="@mipmap/ic_launcher"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintRight_toRightOf="parent"
         app:layout_constraintTop_toTopOf="parent"
         tools:layout_editor_absoluteX="139dp"
         tools:layout_editor_absoluteY="219dp" /> 
 </android.support.constraint.ConstraintLayout>

ImageViewer在占位符上显示启动器图标,用于从 URL 加载图像时显示。只要我们在 XML 中进行更改,启动器图标就会显示出来:

build.gradle

我们需要在build.gradle的依赖项中添加implementation com.square.picasso.picasso:2.71828。在撰写本文时,版本 2.71828 是最新版本。为了确保使用最新版本,最好检查square.github.io/picasso/,并在 Gradle 依赖项中使用最新版本。

我们需要在build.gradle文件的依赖项部分中添加以下行,以便我们的应用程序可以使用Picasso

implementation com.squareup.picasso:picasso:2.71828

修改后的build.gradle文件应该如下所示:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'
    implementation 'com.squareup.picasso:picasso:2.71828'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' }

Kotlin 代码

生成的默认 Kotlin 代码将有一个名为MainActivity的类文件。这个类文件扩展了AppCompatActivity,提供了支持库操作栏功能。

代码在onCreate方法中加载了activity_main中定义的 XML,并在加载时显示它。setContentView读取了在activity_main中定义的 XML 内容,并在加载时显示ImageView

package com.natarajan.imageloader

 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)
     }
 }

我们已经通过用ImageView替换默认的TextView对 XML 进行了更改。我们需要在我们的 Kotlin 代码中反映这些更改,并使用Picasso来加载图像。

我们需要为我们的程序添加ImageViewPicasso的导入,以便使用这些组件:

import android.widget.ImageView
import com.squareup.picasso.Picasso

由于我们已经导入了Picasso并确保了依赖项已添加,我们应该能够通过一行代码加载数据,Picasso.get().load("URL").into(ImageView)

Picasso.get().load("http://i.imgur.com/DvpvklR.png").into(imageView);

用于 Picasso 图片加载的最终修改后的 Kotlin 类应该如下所示:

package com.natarajan.imageloader 
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.ImageView
import com.squareup.picasso.Picasso
import kotlinx.android.synthetic.main.activity_main.*

 class MainActivity : AppCompatActivity() {

 override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.*activity_main*)
        Picasso.get().load("http://i.imgur.com/DvpvklR.png").into(imageView);
    }
 }

清单权限

我们需要确保我们的应用程序已经添加了访问互联网的权限。这是必需的,因为我们将从指定的 URL 下载图像,并在我们的ImageViewer中显示它。

我们已经详细介绍了所需的清单权限。让我们继续添加这个权限:

    <uses-permission android:name="android.permission.INTERNET"></uses-permission>

修改后的 XML 应该如下所示:

*<?*xml version="1.0" encoding="utf-8"*?>
* <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.natarajan.imageloader">

     <uses-permission android:name="android.permission.INTERNET">
    </uses-permission>

     <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>
     </application>
  </manifest>

现在我们已经完成了对 XML、Kotlin 代码、build.gradleAndroidManifest 文件的更改,是时候启动我们的应用程序并了解通过 Picasso 无缝加载图像的过程了。

一旦我们运行应用程序,我们应该能够看到我们的设备加载页面,显示应用程序名称 ImageLoader,并从以下 URL 显示图像:

Glide

Glide 是 bumptech 的另一个非常流行的图像加载库。我们将看看如何使用 Glide 并从特定的 URL 加载图像。

让我们继续对 build.gradle 和其他相关文件进行 Glide 所需的更改。

build.gradle

我们需要在应用程序的 build.gradle 文件中添加插件 kotlin-kapt 并添加依赖项。一旦同步了所做的更改,我们就可以在我们的代码中使用 Glide 并加载图像。

Glide 库使用注解处理。注解处理有助于生成样板代码,并使代码更易于理解。开发人员可以检查生成的代码并了解库生成的样板代码,以观察运行时实际工作的代码:

apply plugin: 'kotlin-kapt' implementation 'com.github.bumptech.glide:glide:4.7.1' kapt "com.github.bumptech.glide:compiler:4.7.1" 

Glide 库讨论了在依赖项中添加注解处理器以及 Glide。这适用于 Java。对于 Kotlin,我们需要像代码块中所示的那样添加 kapt Glide 编译器。

修改后的 build.gradle 依赖项应如下所示:

dependencies {
     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.0'
     implementation 'com.squareup.picasso:picasso:2.71828'
     implementation 'com.github.bumptech.glide:glide:4.7.1'
     kapt "com.github.bumptech.glide:compiler:4.7.1" 
 testImplementation 'junit:junit:4.12'
     androidTestImplementation 'com.android.support.test:runner:1.0.1'
     androidTestImplementation 
 'com.android.support.test.espresso:espresso-core:3.0.1' }

在项目级别的 build.gradle 文件中,我们需要在 repositories 部分添加 mavenCentral(),如下所示:

allprojects {
     repositories {
         google()
         mavenCentral()
         jcenter()
     }

我们已经完成了对 build.gradle 文件的更改;我们应该对 proguard-rules.pro 文件进行以下添加。proguard-rules.pro 文件使开发人员能够通过删除应用程序中未使用和不需要的代码的引用来缩小 APK 大小。

为了确保 Glide 模块受 proguard 缩小的影响,我们需要明确说明应用程序需要保留Glide 的引用。*-*keep 命令确保在构建中保留对 Glide 和相应模块的引用:

-keep public class * implements com.bumptech.glide.module.GlideModule
 -keep public class * extends com.bumptech.glide.module.AppGlideModule
 -keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
   **[] $VALUES;
   public *;
 }
# for DexGuard only -keepresourcexmlelements manifest/application/meta-data@value=GlideModule

Kotlin 代码

我们定义了一个名为 ImageLoaderGlideModule 的单独类,它扩展了 AppGlideModule()。类上的 @GlideModule 注解使应用程序能够访问 GlideApp 实例。GlideApp 实例可以在我们应用程序的各个活动中使用:

package com.natarajan.imageloader
*/**
  ** Created by admin on 4/14/2018. **/* import com.bumptech.glide.annotation.GlideModule
 import com.bumptech.glide.module.AppGlideModule

@GlideModule
 class ImageLoaderGlideModule : AppGlideModule()

我们需要在 MainActivity Kotlin 类中进行以下更改,以便通过 Glide 加载图像并在应用启动时显示它。

Picasso 类似,Glide 也有一个简单的语法,用于从指定的 URL 加载图像:

GlideApp.with(this).load("URL").into(imageView);

修改后的 MainActivity Kotlin 类应如下所示:

package com.natarajan.imageloader

 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)

     if(imageView != null){

   GlideApp.with(this).load("http://goo.gl/gEgYUd").into(imageView);
       }
    }
 }

我们已经完成了 Glide 所需的所有更改——build.gradleProguard.rules 和 Kotlin 类文件。我们应该看到应用程序从指定的 URL 加载图像并在 ImageView 中显示它。

摘要

网络和连接到外部世界是 Android 设备非常强大的功能。我们介绍了网络的基础知识,检查网络状态,可用网络类型,以及 Android 框架提供的内置功能来执行网络操作。

我们还详细讨论了第三方库 PicassoGlide,以及在我们的应用程序中实现这些库。

在下一章中,我们将致力于开发一个简单的待办事项列表应用程序,并讨论各种概念,如列表视图、对话框等,并学习如何在应用程序中使用它们。

第十章:开发一个简单的待办事项列表应用程序

在本章中,我们将构建一个简单的待办事项列表应用程序,允许用户添加、更新和删除任务。

在这个过程中,我们将学到以下内容:

  • 如何在 Android Studio 中构建用户界面

  • 使用 ListView

  • 如何使用对话框

创建项目

让我们从在 Android Studio 中创建一个新项目开始,名称为 TodoList。在“为移动添加活动”屏幕上选择“添加无活动”:

当项目创建完成后,通过选择“文件”|“新建”|“Kotlin 活动”来创建一个 Kotlin 活动,如下面的屏幕截图所示:

这将启动一个新的 Android Activitywizard**。在“为移动添加活动”屏幕上,选择“基本活动”,如下面的屏幕截图所示:

现在,在“自定义活动”屏幕上检查启动器活动,并单击“完成”按钮:

构建您的 UI

在 Android 中,用户界面的代码是用 XML 编写的。您可以通过以下任一方式构建您的 UI:

  • 使用 Android Studio 布局编辑器

  • 手动编写 XML 代码

让我们开始设计我们的 TodoList 应用程序。

使用 Android Studio 布局编辑器

Android Studio 提供了一个布局编辑器,让您可以通过将小部件拖放到可视化编辑器中来构建布局。这将自动生成 UI 的 XML 代码。

打开content_main.xml文件。

确保屏幕底部选择了“设计”选项卡,如下面的屏幕截图所示:

要向布局添加组件,只需从屏幕左侧的 Palette 中拖动项目。要查找组件,可以滚动浏览 Palette 上的项目,或者单击 Palette 搜索图标并搜索所需的项目。

如果 Palette 没有显示在您的屏幕上,请选择“查看”|“工具窗口”|“Palette”以显示它。

继续在您的视图中添加ListView。当选择一个视图时,它的属性会显示在屏幕右侧的 XML 属性编辑器中。属性编辑器允许您查看和编辑所选组件的属性。继续进行以下更改:

  • 将 ID 设置为 list_view

  • 将 layout_width 和 layout_height 属性都更改为 match_parent

如果属性编辑器没有显示,请选择“查看”|“工具窗口”|“属性”以显示它。

现在,在编辑器窗口底部选择“文本”以查看生成的 XML 代码。您会注意到 XML 代码现在在ConstraintLayout中放置了一个ListView

<?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"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.packtpub.eunice.todolist.MainActivity"
    tools:showIn="@layout/activity_main">

    <ListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:layout_editor_absoluteX="4dp"
        tools:layout_editor_absoluteY="8dp" />
</android.support.constraint.ConstraintLayout>

布局始终有一个根元素。在前面的代码中,ConstraintLayout是根元素。

您可以选择使用布局编辑器,也可以自己编写 XML 代码。使用布局编辑器还是编写 XML 代码的选择取决于您。您可以使用您最熟悉的选项。我们将继续随着进展对 UI 进行添加。

现在,构建并运行您的代码。如下面的屏幕截图所示:

如您所见,该应用目前并不完整。让我们继续添加更多内容。

由于我们将使用FloatingActionButton作为用户用来向待办事项列表添加新项目的按钮,我们需要将其图标更改为一个清晰表明其目的的图标。

打开activity_main.xml文件:

android.support.design.widget.FloatingActionButton的一个属性是app:srcCompat。这用于指定FloatingActionButton的图标。将其值从@android:drawable/ic_dialog_email更改为@android:drawable/ic_input_add

再次构建和运行。现在底部的FloatingActionButton看起来像一个添加图标,如下面的屏幕截图所示:

为用户界面添加功能

目前,当用户单击“添加”按钮时,屏幕底部会显示一个滚动消息。这是因为onCreate()方法中的一段代码定义并设置了FloatingActionButtonOnClickListener

fab.setOnClickListener { view ->
    Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
            .setAction("Action", null).show()
}

这对于我们的待办事项列表应用程序来说并不理想。让我们继续在MainActivity类中创建一个新方法来处理单击事件:

fun showNewTaskUI() {
}

该方法目前什么也不做。我们将很快添加代码来显示适当的 UI。现在,用对新方法的调用替换setOnClickListener()调用中的代码:

fab.setOnClickListener { showNewTaskUI() }

添加新任务

要添加新任务,我们将向用户显示一个带有可编辑字段的 AlertDialog。

让我们从为对话框构建 UI 开始。右键单击res/layout目录,然后选择新建 | 布局资源文件,如下面的屏幕截图所示:

在新资源文件窗口上,将根元素更改为LinearLayout,并将文件名设置为dialog_new_task。单击“确定”以创建布局,如下面的屏幕截图所示:

打开dialog_new_task布局,并向LinearLayout添加一个EditText视图。布局中的 XML 代码现在应该如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <EditText
        android:id="@+id/task"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="text"/>

</LinearLayout>

inputType属性用于指定字段可以接受什么类型的数据。通过指定此属性,用户将显示适当的键盘。例如,如果inputType设置为数字,则显示数字键盘:

现在,让我们继续添加一些我们将在下一节中需要的字符串资源。打开res/values/strings.xml文件,并将以下代码添加到resources标记中:

<string name="add_new_task_dialog_title">Add New Task</string>
<string name="save">Save</string>
  • add_new_task_dialog_title字符串将用作对话框的标题

  • save字符串将用作对话框上按钮的文本

使用AlertDialog的最佳方法是将其封装在DialogFragment中。DialogFragment消除了处理对话框生命周期事件的负担。它还使您能够轻松地在其他活动中重用对话框。

创建一个名为NewTaskDialogFragment的新 Kotlin 类,并用以下代码替换类定义:

class NewTaskDialogFragment: DialogFragment() {  // 1

    // 2
    interface NewTaskDialogListener {
        fun onDialogPositiveClick(dialog: DialogFragment, task: String)
        fun onDialogNegativeClick(dialog: DialogFragment)
    }

    var newTaskDialogListener: NewTaskDialogListener? = null  // 3

    // 4
    companion object {
        fun newInstance(title: Int): NewTaskDialogFragment {

            val newTaskDialogFragment = NewTaskDialogFragment()
            val args = Bundle()
            args.putInt("dialog_title", title)
            newTaskDialogFragment.arguments = args
            return newTaskDialogFragment
        }
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {  // 5
        val title = arguments.getInt("dialog_title")
        val builder = AlertDialog.Builder(activity) 
        builder.setTitle(title) 

        val dialogView =    
     activity.layoutInflater.inflate(R.layout.dialog_new_task, null) 
        val task = dialogView.findViewById<EditText>(R.id.task)

        builder.setView(dialogView)
                .setPositiveButton(R.string.save, { dialog, id ->
                    newTaskDialogListener?.onDialogPositiveClick(this, 
                 task.text.toString);
                })
                .setNegativeButton(android.R.string.cancel, { dialog, 
                 id ->
                    newTaskDialogListener?.onDialogNegativeClick(this)
                })
        return builder.create()
     }

  override fun onAttach(activity: Activity) { // 6
        super.onAttach(activity)
        try {
            newTaskDialogListener = activity as NewTaskDialogListener  
        } catch (e: ClassCastException) {
            throw ClassCastException(activity.toString() + " must  
            implement NewTaskDialogListener")
        }

    }
}

让我们更仔细地看看这个类做了什么:

  1. 该类扩展了DialogFragment类。

  2. 它声明了一个名为NewTaskDialogListener的接口,该接口声明了两种方法:

  • onDialogPositiveClick(dialog: DialogFragment, task: String)

  • onDialogNegativeClick(dialog: DialogFragment)

  1. 它声明了一个类型为NewTaskDialogListener的变量。

  2. 它在伴随对象中定义了一个newInstance()方法。通过这样做,可以在不必创建NewTaskDialogFragment类的实例的情况下访问该方法。newInstance()方法执行以下操作:

  • 它接受一个名为titleInt参数

  • 它创建了NewTaskDialogFragment的一个实例,并将title作为其参数的一部分传递

  • 返回NewTaskDialogFragment的新实例

  1. 它重写了onCreateDialog()方法。此方法执行以下操作:
  • 它尝试检索传递的标题参数

  • 实例化AlertDialog构建器,并将检索到的标题分配为对话框的标题

  • 它使用DialogFragment实例的父活动的LayoutInflater来填充我们创建的布局

  • 然后,将充气的视图设置为对话框的视图

  • 为对话框设置两个按钮:保存取消

  • 单击“保存”按钮时,将检索EditText中的文本,并通过onDialogPositiveClick()方法将其传递给newTaskDialogListener变量

  1. onAttach()方法中,我们尝试将传递的Activity对象分配给前面创建的newTaskDialogListener变量。为使其工作,Activity对象应该实现NewTaskDialogListener接口。

现在,打开MainActivity类。更改类声明以包括NewTaskDialogListener的实现。您的类声明现在应该如下所示:

class MainActivity : AppCompatActivity(), NewTaskDialogFragment.NewTaskDialogListener {

并通过向MainActivity类添加以下方法来添加NewTaskDialogListener中声明的方法的实现:

    override fun onDialogPositiveClick(dialog: DialogFragment, task:String) {
    }

    override fun onDialogNegativeClick(dialog: DialogFragment) {
    }

showNewTaskUI()方法中,添加以下代码行:

val newFragment = NewTaskDialogFragment.newInstance(R.string.add_new_task_dialog_title)
newFragment.show(fragmentManager, "newtask")

在上述代码行中,调用NewTaskDialogFragment中的newInstance()方法以生成NewTaskDialogFragment类的实例。然后调用DialogFragmentshow()方法来显示对话框。

构建并运行。现在,当您单击添加按钮时,您应该在屏幕上看到一个对话框,如下截图所示:

您可能已经注意到,单击保存按钮时什么都没有发生。在onDialogPositiveClick()方法中,添加此处显示的代码行:

Snackbar.make(fab, "Task Added Successfully", Snackbar.LENGTH_LONG).setAction("Action", null).show()

正如我们可能记得的那样,这行代码在屏幕底部显示一个滚动消息。

构建并运行。现在,当您在New Task对话框上单击 SAVE 按钮时,屏幕底部会显示一个滚动消息。

我们目前没有存储用户输入的任务。让我们创建一个集合变量来存储用户添加的任何任务。在MainActivity类中,添加一个类型为ArrayList<String>的新变量,并用空的ArrayList进行实例化:

private var todoListItems = ArrayList<String>()

onDialogPositiveClick()方法中,在方法定义的开头放置以下代码行:

todoListItems.add(task)
listAdapter?.notifyDataSetChanged()

这将向todoListItems数据添加传递给listAdapter的任务变量,并调用notifyDataSetChanged()来更新ListView

保存数据很好,但是我们的ListView仍然是空的。让我们继续纠正这一点。

在 ListView 中显示数据

要对 XML 布局中的 UI 元素进行更改,您需要使用findViewById()方法来检索布局的Activity中元素的实例。这通常在ActivityonCreate()方法中完成。

打开MainActivity.kt,并在类顶部声明一个新的ListView实例变量:

private var listView: ListView? = null

接下来,使用布局中相应元素的ListView变量进行实例化。通过在onCreate()方法的末尾添加以下一行代码来完成此操作:

listView = findViewById(R.id.list_view)

ListView中显示数据,您需要创建一个Adapter,并向其提供要显示的数据以及如何显示该数据的信息。根据您希望在ListView中显示数据的方式,您可以使用现有的 Android Adapters 之一,也可以创建自己的 Adapter。现在,我们将使用最简单的 Android Adapter 之一,ArrayAdapterArrayAdapter接受一个数组或项目列表,一个布局 ID,并根据传递给它的布局显示您的数据。

MainActivity类中,添加一个新的变量,类型为ArrayAdapter

private var listAdapter: ArrayAdapter<String>? = null

向类中添加此处显示的方法:

private fun populateListView() {
    listAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, todoListItems)
    listView?.adapter = listAdapter
}

在上述代码行中,我们创建了一个简单的ArrayAdapter并将其分配给listView作为其Adapter

现在,在onCreate()方法中添加对前一个方法的调用:

populateListView()

构建并运行。现在,当您单击添加按钮时,您将看到您的条目显示在 ListView 上,如下截图所示:

更新/删除待办事项

如果用户在输入新任务时出现错误怎么办?我们需要为他们提供一种能够编辑列表项或完全删除该项的方法。我们可以提供菜单项,仅在用户单击项目时显示。菜单项将为用户提供编辑或删除所选项的机会。

如果用户选择编辑选项,我们将显示我们的任务对话框,并为用户填写任务字段以进行所需的更改。

让我们首先向strings.xml资源文件添加以下一组字符串:

<string name="update_task_dialog_title">Edit Task</string>
<string name="edit">Edit</string>
<string name="delete">Delete</string>

接下来,我们需要在 UI 中添加一个菜单。

添加菜单

让我们首先创建菜单资源文件。右键单击res目录,然后选择 New | Android resource file。输入to_do_list_menu作为文件名。将资源类型更改为菜单,然后单击确定,如下面的屏幕截图所示:

用以下代码替换to_do_list_menu,xml文件中的代码行:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/edit_item"
        android:title="@string/edit"
        android:icon="@android:drawable/ic_menu_edit"
        android:visible="false"
        app:showAsAction="always"/>
    <item
        android:id="@+id/delete_item"
        android:title="@string/delete"
        android:icon="@android:drawable/ic_menu_delete"
        android:visible="false"
        app:showAsAction="always"/>
</menu>

在上述代码行中,我们创建了两个菜单项,editdelete项。我们还将每个菜单项的可见性设置为false

接下来,打开MainActivity类,并在类顶部添加以下两个新变量:

private var showMenuItems = false
private var selectedItem = -1 

showMenuItems变量将用于跟踪菜单项的可见状态,而selectedItem变量存储当前选定列表项的位置。

然后,重写onCreateOptionsMenu()方法,如果showMenuItems变量设置为true,则启用菜单项:

override fun onCreateOptionsMenu(menu: Menu): Boolean {
    val inflater = menuInflater
    inflater.inflate(R.menu.to_do_list_menu, menu)
    val editItem = menu.findItem(R.id.edit_item)
    val deleteItem = menu.findItem(R.id.delete_item)

    if (showMenuItems) {
        editItem.isVisible = true
        deleteItem.isVisible = true
    }

    return true
}

接下来,打开MainActivity类,并添加以下方法:

private fun showUpdateTaskUI(selected: Int) {
    selectedItem = selected
    showMenuItems = true
    invalidateOptionsMenu()
}

当调用此方法时,它将分配传递给它的参数给selectedItem变量,并将showMenuItems的值更改为true。然后调用invalidateOptionsMenu()方法。invalidateOptionsMenu()方法通知操作系统已对Activity相关的菜单进行了更改。这将导致菜单被重新创建。

现在,我们需要为ListView实现一个ItemClickListener。在onCreate()方法中,添加以下代码行:

listView?.onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, id -> showUpdateTaskUI(position) }

在这些代码行中,当单击项目时,将调用showUpdateTaskUI()方法。

再次构建和运行。这次,当您单击列表项时,菜单项将显示出来,如下面的屏幕截图所示:

接下来,我们需要更新NewTaskDialogFragment类以接受和处理所选任务。打开NewTaskDialogFragment类。

更新newInstance()方法以接受String类型的额外参数,并通过以下代码将该参数作为DialogFragment参数的一部分传递:

fun newInstance(title: Int, selected: String?): NewTaskDialogFragment { // 1
    val newTaskDialogFragment = NewTaskDialogFragment()
    val args = Bundle()
    args.putInt("dialog_title", title)
    args.putString("selected_item", selected) // 2
    newTaskDialogFragment.arguments = args
    return newTaskDialogFragment
}

注意:更改的地方标有数字。

接下来,更新onCreateDialog()方法以检索并显示所选任务的文本,如下面的代码所示:

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    val title = arguments.getInt("dialog_title")
    val selectedText = arguments.getString("selected_item") // 1
    val builder = AlertDialog.Builder(activity)
    builder.setTitle(title)

    val dialogView = activity.layoutInflater.inflate(R.layout.dialog_new_task, null)

    val task = dialogView.findViewById<EditText>(R.id.task)

    task.setText(selectedText)  // 2

    builder.setView(dialogView)
            .setPositiveButton(R.string.save, { dialog, id ->

                newTaskDialogListener?.onDialogPositiveClick(this, task.text.toString());
            })
            .setNegativeButton(android.R.string.cancel, { dialog, id ->

                newTaskDialogListener?.onDialogNegativeClick(this)
            })

    return builder.create()
}

接下来,我们需要实现当用户选择菜单项时的功能。这是通过重写onOptionsItemSelected()方法来完成的:

override fun onOptionsItemSelected(item: MenuItem?): Boolean {

if (-1 != selectedItem) {
if (R.id.edit_item == item?.itemId) {  // 1

val updateFragment = NewTaskDialogFragment.newInstance(R.string.update_task_dialog_title, todoListItems[selectedItem])
            updateFragment.show(fragmentManager, "updatetask")

        } else if (R.id.delete_item == item?.itemId) {  // 2

todoListItems.removeAt(selectedItem)
listAdapter?.notifyDataSetChanged()
selectedItem = -1
            Snackbar.make(fab, "Task deleted successfully", 
            Snackbar.LENGTH_LONG).setAction("Action", null).show()

        }
    }
return super.onOptionsItemSelected(item)
}

在上述方法中,检查所选菜单项的 ID 与两个菜单项的 ID 是否匹配。

  1. 如果所选菜单项是编辑按钮:
  • 生成并显示NewTaskDialogFragment的新实例。在生成新实例的调用中,检索并传递所选任务。
  1. 如果是delete按钮:
  • 所选项目从todoListItems中删除

  • 通知listAdapter数据已更改

  • selectedItem变量被重置为-1

  • 并且,将显示一个提示,通知用户删除成功删除

正如您可能已经注意到的,在调用show()方法时,第二个参数是一个String。这个参数是标签。标签充当一种 ID,用于区分Activity管理的不同片段。我们将使用标签来决定在调用onDialogPositiveClick()方法时执行哪些操作。

用以下方法替换onDialogPositiveClick()方法:

override fun onDialogPositiveClick(dialog: DialogFragment, task:String) {

    if("newtask" == dialog.tag) {
        todoListItems.add(task)
        listAdapter?.notifyDataSetChanged()

        Snackbar.make(fab, "Task Added Successfully", 
        Snackbar.LENGTH_LONG).setAction("Action", null).show()

    } else if ("updatetask" == dialog.tag) {
        todoListItems[selectedItem] = task

        listAdapter?.notifyDataSetChanged()

        selectedItem = -1

        Snackbar.make(fab, "Task Updated Successfully", 
        Snackbar.LENGTH_LONG).setAction("Action", null).show()
    }
}

在上述代码行中,以下内容适用:

  1. 如果对话框的标签是newtask
  • 任务变量被添加到todoListItems数据中,并通知listAdapter更新ListView

  • 还会显示一个提示,通知用户任务已成功添加

  1. 如果对话框的标签是updatetask
  • 选定的项目用任务变量替换在todoListItems数据集中,并通知listAdapter更新ListView

  • selectedItem变量被重置为-1

  • 此外,还会显示一个滚动消息通知用户任务已成功更改

构建并运行。选择一个任务并点击编辑菜单项。这将弹出编辑任务对话框,并自动填充所选任务的详细信息,如下面的截图所示:

对任务详情进行更改,然后点击保存按钮。这将关闭对话框,更新您的ListView以显示更新后的任务,并在屏幕底部显示一个消息为“任务成功更新”的滚动消息,如下面的截图所示:

接下来,选择一个任务并点击删除菜单项。这将删除所选的任务,并在屏幕底部显示一个消息为“任务成功删除”的滚动消息,如下面的截图所示:

摘要

在本章中,我们构建了一个简单的 TodoList 应用程序,允许用户添加新任务,并编辑或删除已添加的任务。在这个过程中,我们学会了如何使用 ListViews 和 Dialogs。在当前状态下,TodoList 应用程序在重新启动时会重置数据。这并不理想,因为用户很可能希望在重新启动应用程序后查看他们的旧任务。

在下一章中,我们将学习有关不同的数据存储选项以及如何使用它们来使我们的应用程序更加可用。我们将扩展 TodoList 应用程序以将用户的任务持久化到数据库中。

第十一章:使用数据库持久化

在本章中,我们将通过正确地将用户输入的任务持久化到数据库中,改进上一章的待办事项列表应用。

在本章中,我们将学习以下内容:

  • 数据库的概念

  • 移动开发可用的不同类型的数据库

  • 如何连接到一些不同的可用数据库

数据库简介

数据库简单地是一组数据,以使访问和/或更新它变得容易的方式组织起来。组织数据可以以许多方式进行,但它们可以分为两种主要类型:

  • 关系数据库

  • 非关系数据库

关系数据库

关系数据库是一种根据数据之间的关系组织数据的数据库。在关系数据库中,数据以表格的形式呈现,有行和列。表格存储了相同类型的数据集合。表格中的每一列代表表格中存储的对象的属性。表格中的每一行代表一个存储的对象。表格有一个标题,指定了要存储在数据库中的对象的不同属性的名称和类型。在关系数据库中,每个属性的数据类型在创建表格时指定。

让我们来看一个例子。这里的表代表了一组学生:

表的每一行代表一个学生。列代表每个学生的不同属性。

关系数据库是使用RDBMS关系数据库管理系统)维护的。数据是使用一种称为SQL(结构化查询语言)的语言访问和管理的。一些最常用的 RDBMS 是 Oracle、MySQL、Microsoft SQL Server、PostgreSQL、Microsoft Access 和 SQLite。MySQL、PostgreSQL 和 SQLite 是开源的。

Android 开发的 RDBMS 选择是 SQLite。这是因为 Android 操作系统捆绑了 SQLite。

在上一章中,我们构建了一个待办事项列表应用,允许用户添加、更新和删除任务。我们使用了ArrayList作为我们的数据存储。让我们继续扩展应用程序,改用关系数据库。

使用 SQLite

首先要做的是定义数据库的架构。数据库的架构定义了数据库中的数据是如何组织的。它定义了数据组织到哪些表中,并对这些表的限制(例如列的允许数据类型)进行了定义。建议创建一个合同类,指定数据库的详细信息。

创建一个新的 Kotlin 对象,名为TodoListDBContract,并用以下代码替换其内容:

object TodoListDBContract {

        const val DATABASE_VERSION = 1
        const val DATABASE_NAME = "todo_list_db"

    class TodoListItem: BaseColumns {
        companion object {
            const val TABLE_NAME = "todo_list_item"
            const val COLUMN_NAME_TASK = "task_details"
            const val COLUMN_NAME_DEADLINE = "task_deadline"
            const val COLUMN_NAME_COMPLETED = "task_completed"
        }
    }

}

在上述代码中,TodoListItem类代表了我们数据库中的一个表,并用于声明表的名称和其列的名称。

要创建一个新的 Kotlin 对象,首先右键单击包,然后选择新建

| Kotlin 文件/类。然后在新的 Kotlin 文件/类对话框中,在Kind字段中选择Object

接下来要做的是创建一个数据库助手类。这将帮助我们抽象出对数据库的连接,并且不将数据库连接逻辑保留在我们的 Activity 中。继续创建一个名为TodoListDBHelper的新的 Kotlin 类。该类应该在其默认构造函数中接受一个Context参数,并扩展android.database.sqlite.SQLiteOpenHelper类,如下所示:

class TodoListDBHelper(context: Context): SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {

现在,按照以下代码将以下代码添加到TodoListDBHelper类中:

private val SQL_CREATE_ENTRIES = "CREATE TABLE " + TodoListDBContract.TodoListItem.TABLE_NAME + " (" +
        BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
        TodoListDBContract.TodoListItem.COLUMN_NAME_TASK + " TEXT, " +
        TodoListDBContract.TodoListItem.COLUMN_NAME_DEADLINE + " TEXT, " +
        TodoListDBContract.TodoListItem.COLUMN_NAME_COMPLETED + " INTEGER)"  // 1

private val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS " + TodoListDBContract.TodoListItem.TABLE_NAME   // 2

override fun onCreate(db: SQLiteDatabase) { // 3
 db.execSQL(SQL_CREATE_ENTRIES)
}

override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {// 4
 db.execSQL(SQL_DELETE_ENTRIES)
 onCreate(db)
}

override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
 onUpgrade(db, oldVersion, newVersion)
}

在上述代码中,以下内容适用:

  • SQL_CREATE_ENTRIES是一个 SQL 查询,用于创建一个表。它指定了一个_id字段,该字段被设置为数据库的主键。

在关系数据库中,表需要有一个列来唯一标识每个行条目。这个唯一的列被称为主键。将列指定为AUTOINCREMENT告诉 RDBMS 在插入新行时自动生成此字段的新值。

  • SQL_DELETE_ENTRIES是一个 SQL 查询,用于删除表(如果存在)。

  • onCreate()方法中,执行 SQL 查询以创建表。

  • onUpgrade()中,表被删除并重新创建。

由于表在数据库中将有一个 ID 字段,我们必须在Task类中添加一个额外的字段来跟踪它。打开Task.kt,添加一个名为taskIdLong类型的新字段。

var taskId: Long? = null

接下来,添加如下所示的构造函数:

constructor(taskId:Long, taskDetails: String?, taskDeadline: String?, completed: Boolean) : this(taskDetails, taskDeadline) {
        this.taskId = taskId
        this.completed = completed
    }

将数据插入数据库

打开TodoListDBHelper,并添加以下所示的方法:

fun addNewTask(task: Task): Task {
        val db = this.writableDatabase // 1

// 2
        val values = ContentValues()
        values.put(TodoListDBContract.TodoListItem.COLUMN_NAME_TASK, task.taskDetails)
        values.put(TodoListDBContract.TodoListItem.COLUMN_NAME_DEADLINE, task.taskDeadline)
        values.put(TodoListDBContract.TodoListItem.COLUMN_NAME_COMPLETED, task.completed)

        val taskId = db.insert(TodoListDBContract.TodoListItem.TABLE_NAME, null, values); // 3
        task.taskId = taskId

        return task
    }

在这里,我们执行以下操作:

  1. 我们首先以写模式检索数据库。

  2. 接下来,我们创建一个ContentValues的实例,并放入我们要插入的项目中字段的值键映射。

  3. 然后,我们在数据库对象上调用insert()方法,将表名和ContentValues实例传递给它。这将返回插入项的主键_id。我们更新任务对象并返回它。

打开MainActivity类。

首先,在类的顶部添加TodoListDBHelper类的一个实例作为一个新字段:

private var dbHelper: TodoListDBHelper = TodoListDBHelper(this)

并重写AppCompatActivityonDestroy()方法:

override fun onDestroy() {
    dbHelper.close()
    super.onDestroy()
}

当 Activity 的onDestroy()方法被调用时,这将关闭数据库连接。

然后,在onDialogPositiveClick()方法中,找到这行代码:

todoListItems.add(Task(taskDetails, ""))

用以下代码替换它:

val addNewTask = dbHelper.addNewTask(Task(taskDetails, ""))
todoListItems.add(addNewTask)

调用dbHelper.addNewTask()将新任务保存到数据库,而不仅仅是将其添加到todoListItems字段中。

构建并运行应用程序:

既然我们已经能够保存到数据库,我们需要在应用程序启动时能够查看数据。

从数据库中检索数据

打开TodoListDBHelper,并添加如下所示的方法:

fun retrieveTaskList(): ArrayList<Task> {
    val db = this.readableDatabase  // 1

    val projection = arrayOf<String>(BaseColumns._ID,
            TodoListDBContract.TodoListItem.COLUMN_NAME_TASK,
            TodoListDBContract.TodoListItem.COLUMN_NAME_DEADLINE,
            TodoListDBContract.TodoListItem.COLUMN_NAME_COMPLETED) // 2

    val cursor = db.query(TodoListDBContract.TodoListItem.TABLE_NAME, projection, 
            null, null, null, null, null) // 3

    val taskList = ArrayList<Task>()
// 4
    while (cursor.moveToNext()) {
        val task = Task(cursor.getLong(cursor.getColumnIndexOrThrow(BaseColumns._ID)),
                cursor.getString(cursor.getColumnIndexOrThrow(TodoListDBContract.TodoListItem.COLUMN_NAME_TASK)),
                cursor.getString(cursor.getColumnIndexOrThrow(TodoListDBContract.TodoListItem.COLUMN_NAME_DEADLINE)),
                cursor.getInt(cursor.getColumnIndexOrThrow(TodoListDBContract.TodoListItem.COLUMN_NAME_COMPLETED)) == 1)
        taskList.add(task)
    }
    cursor.close() // 5

    return taskList
}

retrieveTaskList方法中,我们执行以下操作:

  1. 我们首先以读模式检索数据库。

  2. 接下来,我们创建一个列出我们需要检索的表的所有列的数组。在这里,如果我们不需要特定列的值,我们就不添加它。

  3. 然后,我们将表名和列列表传递给数据库对象上的query()方法。这将返回一个Cursor对象。

  4. 接下来,我们循环遍历Cursor对象中的项目,并使用每个项目的属性创建Task类的实例。

  5. 我们关闭游标并返回检索到的数据

现在,打开MainActivity,并在populateListView()方法的开头添加以下代码行:

    todoListItems = dbHelper.retrieveTaskList();

您的populateListView()方法现在应该如下所示:

private fun populateListView() {
    todoListItems = dbHelper.retrieveTaskList();
    listAdapter = TaskListAdapter(this, todoListItems)
    listView?.adapter = listAdapter
}

现在,重新构建并运行。您会注意到,与上一章不同的是,当您重新启动应用程序时,您之前保存的任务会被保留:

更新任务

在本节中,我们将学习如何更新数据库中已保存任务的详细信息。打开TodoListDBHelper,并添加如下所示的方法:

    fun updateTask(task: Task) {
        val db = this.writableDatabase // 1

        // 2
        val values = ContentValues()
        values.put(TodoListDBContract.TodoListItem.COLUMN_NAME_TASK, task.taskDetails)
        values.put(TodoListDBContract.TodoListItem.COLUMN_NAME_DEADLINE, task.taskDeadline)
        values.put(TodoListDBContract.TodoListItem.COLUMN_NAME_COMPLETED, task.completed)

        val selection = BaseColumns._ID + " = ?" // 3
        val selectionArgs = arrayOf(task.taskId.toString()) // 4

        db.update(TodoListDBContract.TodoListItem.TABLE_NAME, values, selection, selectionArgs) // 5

    }

updateTask()方法中,我们执行以下操作:

  1. 我们首先以写模式检索数据库。

  2. 接下来,我们创建一个ContentValues的实例,并放入我们要更新的字段的值键映射。对于我们正在处理的内容,我们将假定更新所有列。

  3. 我们为选择要更新的数据库条目指定一个查询。我们的选择查询使用_id列。

  4. 然后,我们为选择查询指定参数,这里,我们选择的是所选TasktaskId

  5. 然后,我们在数据库对象上调用update()方法,传递表名、ContentValues实例、选择查询和选择值。

MainActivity类的onDialogPositiveClick()方法中,找到这行代码:

dbHelper.updateTask(todoListItems[selectedItem])

并将其放在以下代码行之后:

todoListItems[selectedItem].taskDetails = taskDetails

onDialogPositiveClick()方法现在应该如下所示:

override fun onDialogPositiveClick(dialog: DialogFragment, taskDetails:String) {
        if("newtask" == dialog.tag) {
            val addNewTask = dbHelper.addNewTask(Task(taskDetails, ""))
            todoListItems.add(addNewTask)
            listAdapter?.notifyDataSetChanged()

            Snackbar.make(fab, "Task Added Successfully", Snackbar.LENGTH_LONG).setAction("Action", null).show()

        } else if ("updatetask" == dialog.tag) {
            todoListItems[selectedItem].taskDetails = taskDetails
            dbHelper.updateTask(todoListItems[selectedItem])

            listAdapter?.notifyDataSetChanged()

            selectedItem = -1

            Snackbar.make(fab, "Task Updated Successfully", Snackbar.LENGTH_LONG).setAction("Action", null).show()
        }
    }

接下来,在onOptionsItemSelected()中,找到以下代码行:

dbHelper.updateTask(todoListItems[selectedItem])

然后,在此代码行之后放置:

todoListItems[selectedItem].completed = true

构建并运行。当您点击标记为完成菜单项时,所选任务将被更新为已完成,并相应地更新 listView:

删除任务

在本节中,我们将学习如何从数据库中删除已保存的任务。打开TodoListDBHelper,并添加以下方法:

    fun deleteTask(task:Task) {
        val db = this.writableDatabase // 1
        val selection = BaseColumns._ID + " = ?" // 2
        val selectionArgs = arrayOf(task.taskId.toString()) // 3
        db.delete(TodoListDBContract.TodoListItem.TABLE_NAME, selection, selectionArgs) // 4
    }

删除的过程类似于更新的过程:

  1. 首先,以写模式检索数据库

  2. 接下来,为选择要删除的数据库条目指定一个查询。我们的selection查询使用_id

  3. 然后,指定selection查询的参数,在我们的情况下是所选TasktaskId

  4. 然后,我们在数据库对象上调用delete()方法,将表名、选择查询和选择值传递给它

MainActivity类中的方法中,找到以下代码行:

todoListItems.removeAt(selectedItem)

用以下代码替换它:

val selectedTask = todoListItems[selectedItem]
todoListItems.removeAt(selectedItem)
dbHelper.deleteTask(selectedTask)

构建并运行。当您添加一个新项目时,该条目不仅会添加到ListView中,还会保存在数据库中:

编写自己的 SQL 查询可能会出错,特别是如果您正在构建一个严重依赖于数据库或需要非常复杂查询的应用程序。这也需要大量的努力和 SQL 查询知识。为了帮助解决这个问题,您可以使用 ORM 库。

ORM 库

ORM(对象关系映射)库提供了一种更好的方式,让您将对象持久化到数据库中,而不用太担心 SQL 查询,以及打开和关闭数据库连接。

注意:您仍然需要一定水平的 SQL 查询知识

有许多适用于 Android 的 ORM 库:

  • ORMLite

  • GreenDAO

  • DbFlow

  • Room

但是,在本书中,我们将专注于 Room,这是 Google 推出的 ORM。

要使用 Room,我们首先必须将其依赖项添加到项目中。

打开build.gradle,并在依赖项部分添加以下代码行:

implementation 'android.arch.persistence.room:runtime:1.0.0'
annotationProcessor 'android.arch.persistence.room:compiler:1.0.0'
kapt "android.arch.persistence.room:compiler:1.0.0"

点击立即同步。为了让 Room 能够将任务保存到数据库中,我们需要指定哪个类表示一个表。这是通过将类注释为Entity来完成的。打开Task类,并用以下代码替换其内容:

@Entity(tableName = TodoListDBContract.TodoListItem.TABLE_NAME)
class Task() {

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = BaseColumns._ID)
    var taskId: Long? = null

    @ColumnInfo(name = TodoListDBContract.TodoListItem.COLUMN_NAME_TASK)
    var taskDetails: String? = null

    @ColumnInfo(name = TodoListDBContract.TodoListItem.COLUMN_NAME_DEADLINE)
    var taskDeadline: String? = null

    @ColumnInfo(name = TodoListDBContract.TodoListItem.COLUMN_NAME_COMPLETED)
    var completed: Boolean? = false

    @Ignore
    constructor(taskDetails: String?, taskDeadline: String?): this() {
        this.taskDetails = taskDetails
        this.taskDeadline = taskDeadline
    }

    constructor(taskId:Long, taskDetails: String?, taskDeadline: String?, completed: Boolean) : this(taskDetails, taskDeadline) {
        this.taskId = taskId
        this.completed = completed
    }

}

在这里,以下内容适用:

  • @Entity指定Task表示数据库中的一个表

  • @ColumnInfo将字段映射到数据库列

  • @PrimaryKey指定该字段是表的主键

接下来是创建一个DAO(数据访问对象)。创建一个名为TaskDAO的新的 Kotlin 接口,并用以下代码替换其内容:

@Dao
interface TaskDAO {

    @Query("SELECT * FROM " + TodoListDBContract.TodoListItem.TABLE_NAME)
    fun retrieveTaskList(): List<Task> 

    @Insert
    fun addNewTask(task: Task): Long   

    @Update
    fun updateTask(task: Task)  

     @Delete
     fun deleteTask(task: Task)  

}

如前面的代码所示,以下内容适用:

  • Room 提供了InsertUpdateDelete注释,因此您不必为这些编写查询

  • 对于选择操作,您必须使用查询注释方法

接下来,我们需要创建一个数据库类,将我们的应用程序连接到数据库。创建一个名为AppDatabase的新的 Kotlin 类,并用以下代码替换其内容:

@Database(entities = arrayOf(Task::class), version = TodoListDBContract.DATABASE_VERSION)
abstract class AppDatabase : RoomDatabase() {
    abstract fun taskDao(): TaskDAO
}

这就是连接到数据库所需的所有设置。

要使用数据库,打开MainActivity。首先,创建一个AppDatabase类型的字段:

private var database: AppDatabase? = null

接下来,在onCreate()方法中实例化字段:

database = Room.databaseBuilder(applicationContext, AppDatabase::class.java, DATABASE_NAME).build()

在这里,您指定了您的数据库类和数据库的名称。

从数据库中检索数据

Room 不允许您在主线程上运行数据库操作,因此我们将使用AsyncTask来执行调用。将此私有类添加到MainActivity类中,如前面的代码所示:

private class RetrieveTasksAsyncTask(private val database: AppDatabase?) : AsyncTask<Void, Void, List<Task>>() {

    override fun doInBackground(vararg params: Void): List<Task>? {
        return database?.taskDao()?.retrieveTaskList()
    }
}

在这里,我们在doInBackground()方法中调用taskDao来从数据库中检索任务列表。

接下来,在populateListView()方法中,找到以下代码行:

todoListItems = dbHelper.retrieveTaskList();

然后,用这个替换它:

todoListItems = RetrieveTasksAsyncTask(database).execute().get() as ArrayList<Task>

Room 创建并管理一个主表,用于跟踪数据库的版本。因此,即使我们需要对数据库进行迁移以保留当前数据库中的数据。

打开TodoListDBContract类,并将DATABASE_VERSION常量增加到2

然后,用以下代码替换MainActivity中的数据库实例化:

database = Room.databaseBuilder(applicationContext, AppDatabase::class.java, DATABASE_NAME)
        .addMigrations(object : Migration(TodoListDBContract.DATABASE_VERSION - 1, TodoListDBContract.DATABASE_VERSION) {
            override fun migrate(database: SupportSQLiteDatabase) {
            }
        }).build()

在这里,我们向databaseBuilder添加一个新的Migration对象,同时指定数据库的当前版本和新版本。

现在,构建并运行。您的应用程序将启动,并显示先前保存的Tasks

将数据插入数据库

要添加新任务,在MainActivity中创建一个新的AsyncTask

private class AddTaskAsyncTask(private val database: AppDatabase?, private val newTask: Task) : AsyncTask<Void, Void, Long>() {

    override fun doInBackground(vararg params: Void): Long? {
        return database?.taskDao()?.addNewTask(newTask)
    }
}

在这里,我们在doInBackground()方法中调用taskDao来将新任务插入数据库。

接下来,在onDialogPositiveClick()方法中,找到以下代码行:

val addNewTask = dbHelper.addNewTask(Task(taskDetails, ""))

并用以下代码替换它:

var addNewTask = Task(taskDetails, "")

addNewTask.taskId = AddTaskAsyncTask(database, addNewTask).execute().get()

现在,构建并运行。就像在上一节中一样,当您添加新项目时,该条目不仅会添加到ListView中,还会保存到数据库中:

更新任务

要更新任务,在MainActivity中创建一个新的AsyncTask

private class UpdateTaskAsyncTask(private val database: AppDatabase?, private val selectedTask: Task) : AsyncTask<Void, Void, Unit>() {

    override fun doInBackground(vararg params: Void): Unit? {
        return database?.taskDao()?.updateTask(selectedTask)
    }
}

在这里,我们在doInBackground()方法中调用taskDao来将新任务插入数据库。

接下来,在onDialogPositiveClick()方法中,找到以下代码行:

dbHelper.updateTask(todoListItems[selectedItem])

用这行代码替换它:

UpdateTaskAsyncTask(database, todoListItems[selectedItem]).execute()

此外,在onOptionsItemSelected()中,找到以下代码行:

dbHelper.updateTask(todoListItems[selectedItem])

并用这行代码替换它:

UpdateTaskAsyncTask(database, todoListItems[selectedItem]).execute()

现在,构建并运行。就像在上一章中一样,选择一个任务,然后单击编辑菜单项。在弹出的编辑任务对话框中,更改任务详细信息,然后单击“保存”按钮。

这将关闭对话框,保存对数据库的更改,更新您的 ListView 以显示更新后的任务,并在屏幕底部显示一个消息提示,显示任务已成功更新:

删除任务

要删除任务,在MainActivity中创建一个新的AsyncTask

private class DeleteTaskAsyncTask(private val database: AppDatabase?, private val selectedTask: Task) : AsyncTask<Void, Void, Unit>() {

    override fun doInBackground(vararg params: Void): Unit? {
        return database?.taskDao()?.deleteTask(selectedTask)
    }
}

接下来,在onOptionsItemSelected()中,找到以下代码行:

dbHelper.deleteTask(selectedTask)

用这行代码替换它:

DeleteTaskAsyncTask(database, selectedTask).execute()

构建并运行。选择一个任务,然后单击删除菜单项。这将从 ListView 中删除所选任务,并从数据库中删除它,并在屏幕底部显示一个消息提示,显示任务已成功删除:

就是这样。正如您所看到的,使用 ORM 可以让您编写更少的代码并减少 SQL 错误。

非关系数据库

非关系型数据库,或者 NoSQL 数据库,是一种不基于关系组织数据的数据库。与关系数据库不同,不同的非关系数据库存储和管理数据的方式各不相同。一些将数据存储为键值对,而其他一些将数据存储为对象。其中许多选项支持 Android。在大多数情况下,这些数据库具有将数据同步到在线服务器的能力。最流行的两种 No-SQL 移动数据库是:

  • CouchBase Mobile

  • Realm

CouchBase 是文档数据库的一个例子,Realm 是对象数据库的一个例子。

文档数据库是无模式的,这意味着它们是非结构化的,因此对文档中可以放入什么没有限制。它们将数据存储为键值对。

另一方面,对象数据库将数据存储为对象。

总结

在本章中,我们添加了将任务存储到数据库的功能。我们还了解了可以使用的不同类型的数据库。在 Android 开发人员中使用最多的数据库是 SQLite,但这并不妨碍您探索其他选项。还有一些数据库服务,如 Firebase,提供后端作为服务的功能。

在选择数据库时,您应该考虑应用程序的数据需求。是否需要将数据存储在在线服务器上?还是,这些数据仅在应用程序的实例中本地使用?您是否想要或有能力设置和管理自定义数据服务器,还是您更愿意选择一个为您完成这项工作的服务?这些都是在为您的 Android 应用程序选择数据库时需要考虑的一些因素。

在下一章中,我们将致力于为我们的待办事项列表应用程序添加提醒功能。

第十二章:为任务设置提醒

在许多现实世界的应用程序中,有必要在某个时候提醒用户,比如说,采取一些行动或提供一些信息。例如,健身应用程序可能会提醒用户开始一些锻炼课程。

在这里,您将通过为任务设置提醒,然后在提醒到期时弹出通知,来构建上一章中的 ToDoList 应用程序。在实现这些功能时,您将学到很多,使用诸如IntentServiceBroadcastReceiverNotification等类。

在本章中,您将创建一个允许用户为任务设置提醒的功能。

在本章结束时,您将学到以下内容:

  • 为设置的提醒创建和显示通知

  • 推送通知简介

  • 如何使用云服务(如 Firebase 和 Amazon SNS)发送推送通知,以及

  • 如何设置您的应用程序以接收和显示推送通知给用户

总的来说,本章涵盖的主题包括:

  • 服务

  • 广播接收器

  • 应用内通知

  • 推送通知

AlarmManager

Android 中的提醒最好通过使用AlarmManager来实现。为什么?看看官方文档对此的解释:

这些允许您安排应用程序在将来的某个时间运行。

另外:

闹钟管理器适用于您希望在特定时间运行应用程序代码的情况,即使您的应用程序当前未运行。对于正常的定时操作(滴答声、超时等),使用 Handler 更容易、更有效率。

这意味着如果您想要实现提醒这样的功能,您来对地方了。用于处理这种任务的替代类Handler最适合在应用程序仍在使用时完成的任务。您的应用程序肯定会有跨天的提醒,可能会持续几周甚至几个月,因此最好使用AlarmManager类。

它的工作原理是这样的,您的应用程序将启动一个后台服务来启动提醒的计时器,然后在到期时向应用程序发送广播。继续看如何实现这一点。

创建闹钟

基本上,有四种类型的闹钟:

  • 经过的实时:这会根据设备启动以来经过的时间触发挂起的意图,但不会唤醒设备。经过的时间包括设备休眠期间的任何时间。

  • 经过的实时唤醒:这会唤醒设备,并在自设备启动以来经过的指定时间后触发挂起的意图。

  • RTC:这在指定时间触发挂起的意图,但不会唤醒设备。

  • RTC 唤醒:这会唤醒设备,以便在指定时间触发挂起的意图。

您将使用 RTC 唤醒闹钟类型来唤醒设备,在用户设置的精确时间触发闹钟。

首先,为用户选择闹钟应该响起的时间创建一个对话框。创建一个名为TimePickerFragment的新类。然后,使用此处显示的代码进行更新:

import android.app.AlarmManager
import android.app.Dialog
import android.app.PendingIntent
import android.app.TimePickerDialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.text.format.DateFormat
import android.util.Log
import android.widget.TimePicker
import android.widget.Toast
import java.util.Calendar

class TimePickerFragment : DialogFragment(), TimePickerDialog.OnTimeSetListener {

 override fun onCreateDialog(savedInstanceState: Bundle): Dialog {
 val c = Calendar.getInstance()
 val hour = c.get(Calendar.HOUR_OF_DAY)
 val minute = c.get(Calendar.MINUTE)

 return TimePickerDialog(activity, this, hour, minute,
 DateFormat.is24HourFormat(activity))
 }

 override fun onTimeSet(view: TimePicker, hourOfDay: Int, minute: Int) {
        Log.d("onTimeSet", "hourOfDay: $hourOfDay minute:$minute")

        Toast.makeText(activity, "Reminder set successfully", Toast.LENGTH_LONG).show()

        val intent = Intent(activity, AlarmReceiver::class.java)
        intent.putExtra(ARG_TASK_DESCRIPTION, taskDescription)

        val alarmIntent = PendingIntent.getBroadcast(activity, 0, intent, 0)
        val alarmMgr = activity.getSystemService(Context.ALARM_SERVICE) as AlarmManager

        val calendar = Calendar.getInstance()
        calendar.set(Calendar.HOUR_OF_DAY, hourOfDay)
        calendar.set(Calendar.MINUTE, minute)

        alarmMgr.set(AlarmManager.RTC_WAKEUP, calendar.timeInMillis, alarmIntent)
    }
}

companion object {
     val ARG_TASK_DESCRIPTION = "task-description"

    fun newInstance(taskDescription: String): TimePickerFragment {
        val fragment = TimePickerFragment()
        val args = Bundle()
        args.putString(ARG_TASK_DESCRIPTION, taskDescription)
        fragment.arguments = args
        return fragment
    }
}

onCreateDialog方法中,您创建了一个TimePickerDialog的实例,并将默认时间设置为当前时间。因此,当时间选择器启动时,它将显示当前时间。

然后,您重写了onTimeSet方法来处理用户设置的时间。您首先记录了时间,然后显示了一个提示,说明时间已成功设置并记录。

然后,您创建了一个意图来执行AlarmReceiver(您很快将创建它)。接下来是一个PendingIntent,在闹钟响起时触发。然后,您(终于)创建了传入用户时间的闹钟。这个闹钟将在用户设置的确切时间触发。而且,它只会运行一次。

启动提醒对话框

打开MainActivity文件,进行一些快速更新,以便您可以显示对话框。

onCreateOptionsMenu中,进行以下更改:

override fun onCreateOptionsMenu(menu: Menu): Boolean {
    ...
    val reminderItem = menu.findItem(R.id.reminder_item)

    if (showMenuItems) {
        ...
        reminderItem.isVisible = true
    }

    return true
}

你刚刚添加了一个提醒菜单项,当用户点击任务时会显示。现在,转到onOptionsItemSelected,以便在选择此菜单项时启动时间选择器。使用以下代码来实现:

} else if (R.id.delete_item == item?.itemId) {
    ...
} else if (R.id.reminder_item == item?.itemId) {
    TimePickerFragment.newInstance("Time picker argument")
            .show(fragmentManager, "MainActivity")
}

接下来,使用以下代码更新to_do_list_menu.xml中的菜单项:

<item
    android:id="@+id/reminder_item"
    android:title="@string/reminder"
    android:icon="@android:drawable/ic_menu_agenda"
    android:visible="false"
    app:showAsAction="ifRoom"/>

现在,使用以下代码在你的strings.xml文件中添加"reminder"字符串资源:

<resources>
    ...
    <string name="reminder">Reminder</string>
</resources>

好的,做得很好。现在,记得上面的AlarmReceiver类吗?它是做什么的?继续了解一下。

BroadcastReceiver

这是你学习BroadcastReceiver类的地方。根据官方文档,它是接收和处理由sendBroadcast(Intent)发送的广播意图的代码的基类。

基本上,它负责在你的应用中接收广播事件。有两种注册这个接收器的方法:

  • 动态地,使用Context.registerReceiver()的这个类的实例,或者

  • 静态地,使用 AndroidManifest.xml 中的<receiver>标签

文档中的一个重要说明:

从 Android 8.0(API 级别 26)开始,系统对在清单中声明的接收器施加了额外的限制。如果你的应用目标是 API 级别 26 或更高,你不能使用清单来声明大多数隐式广播的接收器(不特定地针对你的应用)。

发送广播

你将使用LocalBroadcastManager在闹钟响起时向用户发送通知。这是文档中的一个提示,说明为什么最好使用这种广播方法:

“如果你不需要跨应用发送广播,请使用本地广播。实现方式更加高效(不需要进程间通信),而且你不需要担心其他应用能够接收或发送你的广播所涉及的任何安全问题。”

而且,这告诉我们为什么它是高效的:

本地广播可以作为应用中的通用发布/订阅事件总线使用,而不需要系统范围广播的任何开销。

创建广播接收器

创建一个新文件并命名为AlarmReceiver,让它扩展BroadcastReceiver。然后,使用以下代码更新它:

class AlarmReceiver: BroadcastReceiver() {

    override fun onReceive(context: Context?, p1: Intent?) {
        Log.d("onReceive", "p1$p1")
        val i = Intent(context, AlarmService::class.java)
        context?.startService(i)
    }
}

你所做的只是重写onReceive方法来启动名为AlarmServiceIntentService(这个类将负责显示通知)。嗯,日志语句只是为了帮助调试。

在继续之前,在你的AndroidManifest.xml中注册服务,就像MainActivity组件一样。在这里,你只需要name属性:

<application>
    ...
  <service android:name=".AlarmReceiver"/>
</application>

现在,继续创建由AlarmReceiver启动的AlarmService

创建 AlarmService

IntentService

首先听听官方文档的说法:

IntentService是处理异步请求(表示为 Intents)的Services的基类。客户端通过startService(Intent)调用发送请求;服务根据需要启动,使用工作线程依次处理每个 Intent,并在工作完成时停止自身。”

IntentService是一个通过Intents处理请求的Service组件。接收到Intent后,它会启动一个工作线程来运行任务,并在工作完成时停止,或者在适当的时候停止。

关键之处在于它赋予你的应用在没有任何干扰的情况下执行一些工作的能力。这与Activity组件不同,例如,后者必须在前台才能运行任务。AsyncTasks可以帮助解决这个问题,但仍然不够灵活,对于这样一个长时间运行的任务来说并不合适。继续看它的实际应用。

注意:

  • IntentService有自己的单个工作线程来处理请求

  • 一次只处理一个请求

创建一个 IntentService

创建IntentService的子类称为ReminderService。您将需要重写onHandleIntent()方法来处理Intent。然后,您将构建一个Notification实例来通知用户提醒已到期:

import android.app.IntentService
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.support.v4.app.NotificationCompat
import android.util.Log

class AlarmService : IntentService("ToDoListAppAlarmReceiver") {
 private var context: Context? = null

 override fun onCreate() {
 super.onCreate()
 context = applicationContext
 }

 override fun onHandleIntent(intent: Intent?) {
 intent?showNotification(it)

 if(null == intent){
 Log.d("AlarmService", "onHandleIntent( OH How? )")
 }
 }

 private fun showNotification(taskDescription: String) {
 Log.d("AlarmService", "showNotification($taskDescription)")
 val CHANNEL_ID = "todolist_alarm_channel_01"
 val mBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
 .setSmallIcon(R.drawable.ic_notifications_active_black_48dp)
 .setContentTitle("Time Up!")
 .setContentText(taskDescription)

 val mNotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
 mNotificationManager.notify(23, mBuilder.build())
 }
}

通过代码步骤,这就是您刚刚做的事情:

onCreate()中,您保存了applicationContext的一个实例以供以后使用。

onHandleIntent()中,您使用了 Kotlin 安全检查功能来确保在非空实例上调用showNotification()方法。

showNotification()中,您使用NotificationCompat构建器创建了一个通知实例。您还设置了通知的标题和内容。然后,使用NotificationManager,您触发了通知。notify()方法中的 ID 参数是唯一标识您的应用程序的此通知的标识。

您也需要注册服务。以下是如何操作:

<service android:name=".AlarmService"
         android:exported="false"/>

您应该熟悉这一点,除了android:exported。这只是意味着我们不允许任何外部应用程序与此服务进行交互。

以下是关于IntentService类的一些重要限制。

  • 它无法直接与您的用户界面交互。要将其结果放入 UI,您必须将它们发送到 Activity。

  • 工作请求按顺序运行。如果IntentService中正在运行操作,并且您发送另一个请求,则该请求将等待,直到第一个操作完成。

  • IntentService上运行的操作无法被中断。

现在是运行您的应用程序的时候了。闹钟应该会响起,您应该会看到通知指示。

还有其他发送通知到您的应用程序的方法。继续阅读以了解有关推送通知的信息。

Firebase Cloud Messaging

"Firebase Cloud Messaging(FCM)是一个跨平台的消息传递解决方案,可以让您免费可靠地传递消息。"我相信这是对这项服务的最好简要描述。实际上,它实际上是谷歌创建和运行的 Firebase 平台上许多其他服务套件的一部分。

您已经集成了应用内通知,现在您将看到如何使用 FCM 实现推送通知。

应用内通知基本上意味着通知是由应用程序内部触发和发送的。另一方面,推送通知是由外部来源发送的。

集成 FCM

  1. 设置 FCM SDK

您首先必须将SDK(软件开发工具包)添加到您的应用程序中。您应该确保您的目标至少是 Android 4.0(冰淇淋三明治)。它应该安装有 Google Play 商店应用程序,或者运行 Android 4.0 和 Google API 的模拟器。您的 Android Studio 版本应至少为 2.2。您将在 Android Studio 中使用 Firebase 助手窗口进行集成。

还要确保您已安装了 Google 存储库版本 26 或更高版本,方法如下:

  1. 单击工具|Android|SDK 管理器

  2. 单击SDK 工具选项卡

  3. 检查Google 存储库复选框,然后单击确定

  4. 单击确定进行安装

  5. 单击后台以在后台完成安装,或者等待安装完成后单击完成

现在,您可以按照以下步骤在 Android Studio 中打开并使用助手窗口:

  1. 单击工具|Firebase打开助手窗口:

  1. 单击展开并选择 Cloud Messaging,然后单击设置 Firebase Cloud Messaging教程以连接到 Firebase 并向您的应用程序添加必要的代码:

助手的外观如下:

如果您成功完成了 Firebase 助手的操作指南,您将完成以下操作:

  • 在 Firebase 上注册您的应用程序

  • 通过对根级build.gradle文件进行以下更新,将 SDK 添加到您的应用程序

buildscript {
    // ...
    dependencies {
        // ...
        classpath 'com.google.gms:google-services:3.1.1' // google-services plugin
    }
}

allprojects {
    // ...
    repositories {
        // ...
        maven {
            url "https://maven.google.com" // Google's Maven repository
        }
    }
}

然后,在您模块的build.gradle文件中,它将在文件底部添加apply plugin行,如下所示:

apply plugin: 'com.android.application'

android {
  // ...
}
dependencies {
  // ...
  compile 'com.google.firebase:firebase-core:11.8.0'
}
// ADD THIS AT THE BOTTOM
apply plugin: 'com.google.gms.google-services'

使用以下内容更新您的清单:

<service
    android:name=".MyFirebaseMessagingService">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT"/>
    </intent-filter>
</service>

如果您想在应用程序运行时手动处理从 FCM 接收到的消息,则需要这样做。但是,由于现在有一种方法可以在没有您干预的情况下显示通知,因此您现在不需要这样做。

对于该功能,您需要以下内容:

<service
    android:name=".MyFirebaseInstanceIDService">
    <intent-filter>
        <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
    </intent-filter>
</service>

现在,您将创建MyFirebaseInstanceIDService类以扩展FirebaseInstanceIdService

如果由于某种原因,这些步骤中的任何一个未完成,您可以手动登录到 Firebase 网站,并按照以下步骤创建 Firebase 上的项目并更新应用程序的构建文件。

使用 Firebase 网站,在登录后的第一件事是添加您的项目:

然后,您将被要求输入项目的名称。为项目名称输入ToDoList。它将自动生成一个全局唯一的项目 ID。然后,选择您的居住国家,并点击创建项目按钮:

之后,选择所需的平台。请注意,Firebase 不仅用于 Android,还用于 iOS 和 Web。因此,请选择将 Firebase 添加到您的 Android 应用选项:

现在您将通过一个三步过程:

  1. 第一步是通过提供您的包名称注册您的应用程序:

  1. 在此步骤中,您只需下载google-services.json文件:

  1. 然后,在最后一步中,您将向应用程序添加 SDK。请注意,如果您已经这样做,则无需此操作:

就是这样。您已经在 Firebase 上添加了您的应用程序。现在,您将看到新创建项目的页面。在这里,您将看到所有可用于您的应用程序的服务。选择通知服务,然后单击开始

现在,您将看到以下页面。单击发送您的第一条消息按钮:

然后,选择撰写消息。在这里,输入要在消息文本框中发送的消息。选择单个设备作为目标。在输入FCM 注册令牌后,您将点击发送消息按钮以发送通知。继续阅读以了解如何获取注册令牌:

注册令牌

在设置 FCM 后首次运行应用程序时,FCM SDK 将为您的应用程序生成一个令牌。在以下情况下,此令牌将更改,并相应地生成一个新的令牌:

  • 应用程序删除实例 ID

  • 应用程序在新设备上恢复

  • 用户卸载/重新安装应用程序

  • 用户清除应用程序数据

此令牌必须保持私密。要访问此令牌,您将其记录到您的Logcat控制台中。首先,打开MyFirebaseInstanceIDservice并使用以下代码进行更新:

override fun onTokenRefresh() {
    // Get updated InstanceID token.
    val refreshedToken = FirebaseInstanceId.getInstance().getToken()
    Log.d(FragmentActivity.TAG, "Refreshed token: " + refreshedToken)

    // If you want to send messages to this application instance or
    // manage this apps subscriptions on the server side, send the
    // Instance ID token to your app server.
    sendRegistrationToServer(refreshedToken)
}

现在您已经有了密钥,请将其粘贴到上面的撰写消息框中,然后点击发送消息按钮。之后不久,您应该会在手机上看到通知。

摘要

在本章中,您学习了如何使用 Firebase 创建后台服务,发送广播消息,显示应用内通知和推送通知。有一些事情您可以自己做来加深对这些主题的理解:

  • 而不是使用某些静态消息通知用户,请使用设置提醒的任务的描述

  • 使用 Firebase,您还可以尝试向一组人发送推送通知,而不是单个设备

第十三章:测试和持续集成

在本章中,您将了解持续集成CI)的概念和测试的重要性。从未听说过 CI?那测试呢?

在本章中,我们将:

  • 了解编写测试

  • 了解 Android 测试支持库

  • 学习如何使用 Crashlytics 来跟踪崩溃报告

  • 了解 beta 测试

  • 介绍 CI 的概念

  • 了解 Jenkins、Bamboo 和 Fastlane 等工具以及如何将它们用于构建自动化和部署

测试

软件测试是评估软件或其部分以确保其按预期工作的过程。产品必须满足其构建的给定要求。因此,测试报告给出了软件质量的指示。测试的另一个主要原因是找到错误并修复它们。

有时,有诱惑将测试视为事后思考。这主要是由于时间限制等问题,但考虑到测试的重要性,它应该成为开发过程的一部分。在软件生命周期的后期编写测试可能是非常糟糕的经历。您可能不得不花费大量时间重构它,使其可测试,然后才能编写测试。所有这些因素涉及的挫折使大多数软件难以进行适当的测试。

测试的重要性

测试是一个非常广泛的话题,你可以很容易地写一本书。测试的重要性无法过分强调。以下是一些软件需要测试的原因:

  • 它使企业能够欣赏和理解软件实施的风险

  • 它确保编写了质量程序

  • 它有助于生产无 bug 的产品

  • 它降低了维护成本

  • 这是验证和验证软件的一种可靠方式

  • 它提高了性能

  • 它确认了所有声明的功能要求都已经实施

  • 它给客户带来信心

  • 它更快地暴露错误

  • 这是为了保持业务的需要

  • 它确保产品可以在其预期环境中安装和运行

Android 测试支持库

Android 测试支持库ATSL)是一组专门为测试 Android 应用程序而构建的库。它就像您在 Android 应用程序开发中使用的通常支持库一样,只是这个库是专门用于测试的。

Model-View-Presenter 架构

如前所述,软件需要可测试。只有这样,我们才能为其编写有效的测试。因此,您将使用Model-View-PresenterMVP)架构设计您的应用程序。这种架构采用了一些设计最佳实践,如控制反转和依赖注入,因此非常适合测试。为了使应用程序可测试,其各个部分必须尽可能解耦。

查看以下图表中 MVP 架构的高级图解:

非常简单地说,这些各部分的含义是:

  • Model:它提供并存储应用程序的数据

  • View:它处理模型数据的显示

  • Presenter:它协调 UI 和数据

您还可以轻松地替换其他部分并在测试期间模拟它们。在软件测试中,模拟是模仿真实对象的对象。您将提供其行为,而不是依赖于代码的实际实现。这样,您就可以专注于正在进行预期操作的测试类。您将在以下部分中看到它们的实际应用。

测试驱动开发

您将使用一种称为测试驱动开发TDD)的软件开发类型构建一个 Notes 应用程序。看一下下面的图表和下面的解释:

TDD是一种软件开发方法,其中测试是在实际程序代码之前编写的。

红:红是 TDD 过程的第一阶段。在这里,您编写测试。由于这是第一个测试,这意味着您基本上没有什么可以测试的。因此,您必须编写最少量的代码来进行测试。现在,由于这是编写测试所需的最少量代码,当您编写代码时它很可能会失败。但这完全没关系。在 TDD 中,您的测试必须在发生任何其他事情之前失败!当您的测试失败时,这是 TDD 周期的第一阶段-红色阶段。

绿色:现在,您必须编写通过测试所需的最少量代码。当测试通过时,那很好,您已经完成了 TDD 周期的第二阶段。通过测试意味着您的程序的一部分正如您期望的那样工作。随着您以这种方式构建应用程序,任何时候您都将测试代码的每个部分。您能看到这是如何运作的吗?当您完成一个功能时,您将有足够的测试来测试该功能的各个部分。

重构:TDD 过程的最后阶段是重构您早期编写的代码以通过测试。在这里,您删除冗余代码,清理代码,并为模拟编写完整的实现。之后再次运行测试。它们可能会失败。在 TDD 中,测试失败是件好事。当您编写测试并且它们通过时,您可以确信特定的需求或期望已经得到满足。

还有其他围绕测试构建的开发模型,例如行为驱动测试、黑盒测试和冒烟测试。但是,它们基本上可以归类为功能测试和非功能测试。

功能与非功能测试

通过功能测试,您将根据给定的业务需求测试应用程序。它们不需要应用程序完全运行。这些包括:

  • 单元测试

  • 集成测试

  • 验收测试

对于非功能测试,您将测试应用程序与其操作环境的交互。例如,应用程序将连接到真实数据源并使用 HTTP 连接。这些包括:

  • 安全测试

  • 可用性测试

  • 兼容性测试

笔记应用程序

要开始构建我们的笔记应用程序,请创建一个新应用程序并将其命名为 notes-app。使用 Android Studio 左上角的选项卡切换到项目视图。此视图允许您查看项目结构,就像它在文件系统上存在的那样。它应该看起来像以下截图:

单元测试测试代码的小部分,而不涉及产品的其他部分。在这种情况下,这意味着您的单元测试不需要物理设备,也不需要 Android jar、数据库或网络;只需要您编写的源代码。这些是应该在test目录中编写的测试。

另一方面,集成测试包括运行应用程序所需的所有组件,这些测试将进入androidTest目录。

测试依赖项

目前,只有一个测试库Junit,您将用它进行单元测试。但是,由于您的代码将与其他组件交互,即使它们不是被测试的组件,您也必须对它们进行模拟。Junit仍然不足以编写测试用例。因此,您还需要添加Hamcrest来帮助创建断言匹配等。让我们继续添加我们需要的库。

打开模块的构建文件,更新依赖项以匹配以下代码,并同步项目:

dependencies {
  implementation fileTree(dir: 'libs', include: ['*.jar'])
  implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
  implementation 'com.android.support:appcompat-v7:26.1.0'
  implementation 'com.android.support.constraint:constraint-layout:1.0.2'
  testImplementation 'junit:junit:4.12'
  testImplementation "org.mockito:mockito-all:1.10.19"
  testImplementation "org.hamcrest:hamcrest-all:1.3"
  testImplementation "org.powermock:powermock-module-junit4:1.6.2"
  testImplementation "org.powermock:powermock-api-mockito:1.6.2"
  androidTestImplementation 'com.android.support.test:runner:1.0.1'
  androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}

目前,请使用与前面代码中显示的确切库版本相同的库版本。这意味着您将不得不忽略 IDE 提升库版本的建议。

稍后,您可以更新为彼此兼容的较新稳定版本。

您的第一个测试

您将首先开始向用户显示笔记。笔记演示者将提供显示进度指示器的逻辑,显示笔记和其他与笔记相关的视图。

由于PresenterModelView之间协调,因此您必须对它们进行模拟,以便您可以专注于正在测试的类。

在这个测试中,您将验证要求NotesPresenter添加新笔记将触发调用View来显示添加笔记屏幕。让我们实现should display note when button is clicked测试方法。

首先,您将添加对 presenter 的addNewNote()方法的调用。然后,您将验证 View 的showAddNote()被调用。因此,您调用一个方法并验证它反过来调用另一个方法(回想一下 MVP 模式的工作原理;presenter 与视图协调)。

目前,我们不需要担心第二个调用方法做什么;这是单元测试,您一次测试一个小东西(单元)。因此,您必须模拟出 View,并且现在不需要实现它。一些接口可以实现这一点;也就是说,一个 API 或契约而不一定要实现它们。请参阅以下代码的最终部分:

import com.packtpub.eunice.notesapp.notes.NotesContract
import com.packtpub.eunice.notesapp.notes.NotesPresenter
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@Mock
private lateinit var notesView: NotesContract.View
private lateinit var notesPresenter: NotesPresenter

@Before
fun setUp() {
 MockitoAnnotations.initMocks(this)

 // The class under test
 notesPresenter = NotesPresenter()
}

@Test
fun `should display note view when button is clicked`() {
 // When adding a new note
 notesPresenter.addNewNote()

 // Then show add note UI
 verify(notesView)?.showAddNote()
}

现在,创建NotesContract,它是 MVP 架构中的View部分。它将是一个只需要方法以使测试通过的接口:

interface NotesContract {
    interface View {
        fun showAddNote()
    }

    interface UserActionsListener {

        fun loadNotes(forceUpdate: Boolean)

        fun addNewNote()

        fun openNoteDetails(requestedNote: Note)
    }
}

接下来,创建Note类。它代表 MVP 架构中的Model。它定义了您正在构建的笔记应用程序的笔记结构:

import java.util.UUID

data class Note(val title: String?,
 val description: String?,
 val imageUrl: String? = null) {

 val id: String = UUID.randomUUID().toString()
}

创建NotesPresenter,它代表 MVP 架构中的Presenter。让它实现NotesContract类中的UserActionsListener

class NotesPresenter: NotesContract.UserActionsListener {
    override fun loadNotes(forceUpdate: Boolean) {
    }

    override fun addNewNote() {
    }

    override fun openNoteDetails(requestedNote: Note) {
    }
}

这对于第一个测试来说已经足够了。您准备好了吗?好的,现在点击测试方法所在数字旁边的右箭头。或者,您也可以右键单击NotesPresenterTest文件中的位置或文件并选择运行:

您的测试应该失败:

它失败了,因为我们期望调用NotesView类的showAddNote()方法,但实际上没有。这是因为您只在Presenter类中实现了接口,但从未在NotesView类中调用预期的方法。

现在让我们继续并修复它。

首先,更新NotesPresenter以在其主要构造函数中接受NotesContract.View对象。然后,在addNewNote()方法中调用预期的方法showAddNote()

您应该始终更喜欢构造函数注入而不是字段注入。这样更容易处理,也更容易阅读和维护。

您的NotesPresenter类现在应该如下所示:

class NotesPresenter(notesView: NotesContract.View): NotesContract.UserActionsListener {
    private var notesView: NotesContract.View = checkNotNull(notesView) {
        "notesView cannot be null"
    }

    override fun loadNotes(forceUpdate: Boolean) {
    }

    override fun addNewNote() = notesView.showAddNote()

    override fun openNoteDetails(requestedNote: Note) {
    }
}

checkNotNull是一个内置的Kotlin实用程序函数,用于验证对象是否为 null。它的第二个参数接受一个 lambda 函数,如果对象为 null,则应返回默认消息。

由于NotesPresenter现在在其主要构造函数中需要NotesContract.View,因此您必须更新测试以适应这一点:

@Before
fun setUp() {
    MockitoAnnotations.initMocks(this)

// Get a reference to the class under test
    notesPresenter = NotesPresenter(notesView)
}

代码已经重构。现在重新运行测试:

万岁!测试现在通过了;太棒了。干得好。

这是使用TDD的一个完整循环。现在,您需要继续前进,在功能完全实现之前还有一些测试要完成。

您的下一个测试是验证 presenter 是否按预期显示笔记。在此过程中,您将首先从存储库中检索笔记,然后更新视图。

您将使用先前测试的类似测试 API。但是,这里有一个新的测试 API,称为ArgumentCaptor。正如您可能已经猜到的那样,它捕获传递给方法的参数。您将使用这些参数调用另一个方法,并将它们作为参数传递。让我们看一下:

@Mock
private lateinit var notesRepository: NotesRepository

    @Captor
    private var loadNotesCallbackCaptor: ArgumentCaptor<NotesRepository.LoadNotesCallback>? = null

private val NOTES = arrayListOf(Note("Title A", "Description A"),
 Note("Title A", "Description B"))
...

@Test
fun `should load notes from repository into view`() {
 // When loading of Notes is requested
 notesPresenter.loadNotes(true)

 // Then capture callback and invoked with stubbed notes
 verify(notesRepository)?.getNotes(loadNotesCallbackCaptor?.capture())
 loadNotesCallbackCaptor!!.value.onNotesLoaded(NOTES)

 // Then hide progress indicator and display notes
 verify(notesView).setProgressIndicator(false)
 verify(notesView).showNotes(NOTES)
}

让我们再简要地回顾一下。

您首先调用了要测试的方法,即loadNotes()。然后,您验证了该操作反过来使用NotesRepository实例获取笔记(getNotes()),就像之前的测试一样。然后,您验证了传递给getNotes()方法的实例,该实例再次用于加载笔记(onNotesLoaded())。之后,您验证了notesView隐藏了进度指示器(setProgressIndicator(false))并显示了笔记(showNotes())。

尽可能利用 Kotlin 中的空安全功能。不要为模拟使用可空类型,而是使用 Kotlin 的lateinit修饰符。

这将导致代码更加清晰,因为您不必在任何地方进行空值检查,也不必使用elvis运算符。

现在,按照以下方式创建NotesRepository

interface NotesRepository {

    interface LoadNotesCallback {

        fun onNotesLoaded(notes: List<Note>)
    }

    fun getNotes(callback: LoadNotesCallback?)
    fun refreshData()
}

接下来,更新NotesContract

interface NotesContract {
    interface View {
        fun setProgressIndicator(active: Boolean)

        fun showNotes(notes: List<Note>)

        ...
    }

  ...
}

您现在已准备好测试第二个测试用例。继续并运行它:

好的,它失败了。再次,使用 TDD,这很完美!您意识到这确切地告诉我们缺少什么,因此需要做什么。您只实现了合同(接口),但没有进一步的操作。

打开NotesPresenter并重构代码以使此测试通过。您将首先将NotesRepository添加为构造函数参数的一部分,然后在适当的方法中进行调用。请参阅以下代码以获取完整实现:

import com.packtpub.eunice.notesapp.data.NotesRepository
import com.packtpub.eunice.notesapp.util.EspressoIdlingResource

class NotesPresenter(notesView: NotesContract.View, notesRepository: NotesRepository) :
 NotesContract.UserActionsListener {

 private var notesRepository: NotesRepository = checkNotNull(notesRepository) {
 "notesRepository cannot be null"
 }

 override fun loadNotes(forceUpdate: Boolean) {
 notesView.setProgressIndicator(true)
 if (forceUpdate) {
 notesRepository.refreshData()
 }

 EspressoIdlingResource.increment()

 notesRepository.getNotes(object : NotesRepository.LoadNotesCallback {
 override fun onNotesLoaded(notes: List<Note>) {
 EspressoIdlingResource.decrement()
 notesView.setProgressIndicator(false)
 notesView.showNotes(notes)
 }
 })
 }
 ...
}

您使用构造函数注入将NotesRepository实例注入NotesPresenter。您检查了它的可空性,就像您对NotesContract.View所做的那样。

loadNotes()方法中,您显示了进度指示器,并根据forceUpdate字段刷新了数据。

然后,您使用了一个实用类EspressoIdlingResource,基本上是为了提醒 Espresso 可能存在异步请求。在获取笔记时,您隐藏了进度指示器并显示了笔记。

创建一个 util 包,其中包含EspressoIdlingResourceSimpleCountingIdlingResource

import android.support.test.espresso.IdlingResource

object EspressoIdlingResource {

    private const val RESOURCE = "GLOBAL"

    private val countingIdlingResource = SimpleCountingIdlingResource(RESOURCE)

    val idlingResource = countingIdlingResource

    fun increment() = countingIdlingResource.increment()

    fun decrement() = countingIdlingResource.decrement()
}

以及SimpleCountingIdlingResource

package com.packtpub.eunice.notesapp.util

import android.support.test.espresso.IdlingResource
import java.util.concurrent.atomic.AtomicInteger

class SimpleCountingIdlingResource

(resourceName: String) : IdlingResource {

    private val mResourceName: String = checkNotNull(resourceName)

    private val counter = AtomicInteger(0)

    @Volatile
    private var resourceCallback: IdlingResource.ResourceCallback? =  
    null

    override fun getName() = mResourceName

    override fun isIdleNow() = counter.get() == 0

    override fun registerIdleTransitionCallback(resourceCallback: 
    IdlingResource.ResourceCallback) {
        this.resourceCallback = resourceCallback
    }

    fun increment() = counter.getAndIncrement()

    fun decrement() {
        val counterVal = counter.decrementAndGet()
        if (counterVal == 0) {
            // we've gone from non-zero to zero. That means we're idle 
            now! Tell espresso.
            resourceCallback?.onTransitionToIdle()
        }

        if (counterVal < 0) {
            throw IllegalArgumentException("Counter has been 
            corrupted!")
        }
    }
}

确保使用EspressoIdlingResource库更新应用程序的构建依赖项:

dependencies {
  ...
  implementation "com.android.support.test.espresso:espresso-idling-resource:3.0.1"
...
}

接下来,更新setUp方法以正确初始化NotesPresenter类:

@Before
fun setUp() {
    MockitoAnnotations.initMocks(this)

// Get a reference to the class under test
    notesPresenter = NotesPresenter(notesView)
}

现在一切都准备好了,运行测试:

太棒了!真是太棒了。您已成功使用 TDD 方法编写了 NotesApp 的业务逻辑。

Crashlytics

从官方网站:

Firebase Crashlytics 是一个轻量级的实时崩溃报告工具,可帮助您跟踪、优先处理和修复侵蚀应用程序质量的稳定性问题。 Crashlytics 通过智能分组崩溃并突出导致崩溃的情况,节省了故障排除时间。

就是这样,这基本上就是 Crashlytics 的全部内容。它适用于 iOS 和 Android。以下是其主要功能:

  • 崩溃报告:其主要目的是报告崩溃,并且它确实做得很好。它可以定制以满足您的需求。例如,您可能不希望它报告某些类型的崩溃,还有其他定制选项。

  • 分析:它提供有关崩溃的报告,包括受影响的用户、其设备、崩溃发生的时间,包括干净的堆栈跟踪和日志,以帮助调试和修复。

  • 实时警报:您将自动收到有关新问题和重复问题的警报。实时警报是必要的,因为它们可以帮助您非常快速地解决问题。

Crashlytics 用于查找特定崩溃是否影响了大量用户。当问题突然严重性增加时,您还会收到警报,并且它允许您找出哪些代码行导致崩溃。

实施步骤如下:

  • 连接

  • 整合

  • 检查控制台

连接

您将首先向您的应用程序添加 Firebase。Firebase 是一个为移动和 Web 应用程序开发的平台。它有很多工具,其中之一就是 Crashlytics。

最低要求是:

  • 运行 Android 4.0(冰淇淋三明治)或更新版本的设备,并且 Google Play 服务 12.0.1 或更高版本

  • Android Studio 2.2 或更高版本

您将使用 Android Studio 2.2+中的 Firebase 助手工具将您的应用连接到 Firebase。助手工具将更新您现有的项目或创建一个带有所有必要的 Gradle 依赖项的新项目。它提供了一个非常好的直观的 UI 指南,您可以按照它进行操作:

查看完整指南,了解如何将您的项目添加到 Firebase 中的第十二章,为任务设置提醒。完成后,从浏览器登录到 Firebase 控制台。在侧边菜单中,从STABILITY部分选择Crashlytics

当 Crashlytics 页面打开时,您将被问及应用程序是否是 Crashlytics 的新应用程序。选择是,这个应用程序是 Crashlytics 的新应用程序(它没有任何版本的 SDK):

然后第二步会给您一个链接到文档页面,以设置您的应用的 Crashlytics。要将 Crashlytics 添加到应用中,请更新项目级别的build.gradle

buildscript {
    repositories {
        // ...
        maven {
           url 'https://maven.fabric.io/public'
        }
    }
    dependencies {
        // ...
        classpath 'io.fabric.tools:gradle:1.25.1'
    }
}

allprojects {
    // ...
    repositories {
       // ...
       maven {
           url 'https://maven.google.com/'
       }
    }
}

然后,使用 Crashlytics 插件和依赖项更新您的应用程序模块的build.gradle文件:

apply plugin: 'com.android.application'
apply plugin: 'io.fabric'

dependencies {
    // ...
    implementation 'com.crashlytics.sdk.android:crashlytics:2.9.1'
}

就是这样,Crashlytics 已经准备好监听您的应用程序中的崩溃。这是它的默认行为,但是如果您想自己控制初始化,您将不得不在清单文件中禁用它:

<application
...
 <meta-data android:name="firebase_crashlytics_collection_enabled" android:value="false" />
</application>

然后,在您的 Activity 类中,您可以启用它,即使使用调试器也可以:

val fabric = Fabric.Builder(this)
        .kits(Crashlytics())
        .debuggable(true)
        .build()
Fabric.with(fabric)

确保您的 Gradle Wrapper 版本至少为 4.4:

distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

由于您的应用程序需要向控制台发送报告,请在清单文件中添加互联网权限:

<manifest ...>

  <uses-permission android:name="android.permission.INTERNET" />

  <application ...

像往常一样,同步 Gradle 以使用您刚刚进行的依赖项更新您的项目。

之后,您应该看到 Fabric 插件已集成到 Android Studio 中。使用您的电子邮件和密码注册:

确认您的帐户后,Fabric API 密钥将为您生成。它应该看起来像这样:

<meta-data
    android:name="io.fabric.ApiKey"
    android:value="xxYYxx6afd23n6XYf9ff6000383b4ddxxx2220faspi0x"/>

现在,您将强制在您的应用程序中崩溃以进行测试。创建一个新的空白活动,并只添加一个按钮。然后,将其clicklistener设置为强制崩溃。Crashlytics SDK 有一个简单的 API 可以做到这一点:

import kotlinx.android.synthetic.main.activity_main.*

...

override fun onCreate(savedInstanceState: Bundle?) {
 crash_btn.setOnClickListener {
  Crashlytics.getInstance().crash()
 }
}

由于您正在测试,崩溃后重新打开应用程序,以便报告可以发送到您的控制台。

继续运行应用程序。您的测试活动应该是这样的:

点击 CRASH!按钮来强制崩溃。您的应用程序将崩溃。点击确定,然后重新打开应用程序:

检查您的收件箱,也就是您在 Crashlytics 上注册的那个:

点击“了解更多”按钮。它将打开 Crashlytics 控制台。从那里,您可以找到有关崩溃的更多详细信息。从那里,您可以解决它。

测试阶段

测试有两个主要阶段:alpha 测试和 beta 测试。主要思想是在应用程序开发的阶段让一组人测试应用程序。通常是在应用程序开始成形之后,以便可以利用反馈使应用程序更加稳定。稳定性在这里是关键。区分各种测试阶段的一个关键因素是参与测试的人数。

Alpha 测试

Alpha 测试被认为是测试软件的第一阶段。这个测试通常涉及很少数量的测试人员。在这个阶段,应用程序非常不稳定,因此只有与开发人员密切相关的少数人参与测试并提供建设性反馈。应用程序稳定后,就可以进入 beta 测试阶段。

Beta 测试

Beta 测试是软件测试的一个阶段,其中有一个更大的人群测试应用程序。这可能涉及 10、100 或 1000 人或更多,这取决于应用程序的性质和团队的规模。如果一个应用程序在全球拥有大量用户,那么它很可能有一个庞大的团队在开发,并且因此可以承担许多人参与测试该应用程序的 beta 测试。

为 beta 测试设置

您可以从Google Pay 控制台设置和管理 beta 测试。您可以选择将您的应用程序提供给特定的 Google 组,或者您可以通过电子邮件发送邀请。

用户必须拥有 Google(@gmail.com)或 G Suite 帐户才能加入。发布后,您的链接可能需要一段时间才能对测试人员可用。

创建 beta 测试轨道

现在,您将需要在 Google Play 控制台内创建所谓的轨道。这基本上是一个用于管理测试流程的设置。

在这里,您可以上传您的 APK,将其分发给选定的一组人,并在他们测试时跟踪反馈。您还可以管理 alpha 和 beta 测试阶段。

按照以下步骤设置 beta 测试:

  1. 登录到您的 Play 控制台并选择您的应用程序。

  2. 发布管理下找到应用发布,并在Beta 轨道下选择管理

  3. Artifacts部分上传您的 APK,然后展开管理测试人员部分。

  4. 选择测试方法下,选择公开 Beta 测试

  5. 复制Opt-in URL并与您的测试人员分享。

  6. 反馈渠道旁边提供电子邮件地址或 URL,以便从测试人员收集反馈。然后,点击保存来保存它。

Opt-in URL

创建测试后,发布它。然后,您将获得测试链接。其格式如下:play.google.com/apps/testing/com.yourpackage.name。现在,您必须与您的测试人员分享此链接。有了这个链接,他们可以选择测试您的应用程序。

持续集成

通常,一个应用可能有多个人(团队)在进行工作。例如,A 可能负责 UI,B 负责功能 1,C 负责业务逻辑中的功能 2。这样的项目仍然会有一个代码库以及其测试和其他一切。所有提交者可能会在推送代码之前在本地运行测试以确保自己的工作正常。具有不同提交者的共享存储库中的代码必须统一并构建为一个完整的应用程序(集成)。还必须对整个应用程序运行测试。这必须定期进行,在 CI 的情况下,每次提交都要进行。因此,在一天内,共享存储库中的代码将被构建和测试多次。这就是持续集成的概念。以下是一个非常简单的图表,显示了 CI 过程的流程。它从左边(开发)开始:

定义

CI 是一种软件开发实践,其中设置了一个自动化系统,用于在代码检入版本控制后构建、测试和报告软件的情况。集成发生是因为各种分支合并到主分支中。这意味着主分支中的任何内容都有效地代表了整个应用程序的当前状态,而且由于这每次代码进入主存储库时都会发生,所以它是持续的;因此,持续集成

在 CI 中,每当提交代码时,自动构建系统会自动从共享存储库(主分支)中获取最新代码并构建、测试和验证整个分支。通过定期执行此操作,可以快速检测到错误,从而可以快速修复。知道您的提交可能会导致不稳定的构建,因此只能提交小的更改。这也使得易于识别和修复错误。

这非常重要,因为尽管应用程序的不同部分经过了单独测试和构建,但在它们合并到共享存储库后可能并不是必要的。然后,每次检入都会由自动构建进行验证,允许团队及早发现问题。

同样,还有持续部署以及持续交付。

工具

有许多可用于 CI 的工具。有些是开源的,有些是自托管的,有些更适合于 Web 前端,有些更适合于 Web 后端,有些更适合于移动开发。

示例包括 Jenkins、Bamboo 和 Fastlane。您将使用 Fastlane 来集成您的应用程序并运行测试。Fastlane 是自托管的,这意味着您在开发机器上运行它。理想情况下,您应该将其安装在 CI 服务器上,即专用于 CI 任务的服务器。

首先,让我们在本地安装它,并使用它来运行 Notes 应用程序的测试。

在撰写本书时,Fastlane 仅在 MacOS 上运行。目前正在进行工作,以使其在 Linux 和 Windows 上运行。一些 CI 服务包括 Jenkins、Bamboo、GitLab CI、Circle CI 和 Travis。

安装 fastlane

要安装 fastlane,请按照以下步骤进行操作:

  1. 您应该已经在终端中的路径上有gem,因为 x-code 使用 Ruby 并且捆绑在 Mac OS X 中。运行以下命令进行安装:
brew cask install fastlane

根据您的用户帐户权限,您可能需要使用sudo

  1. 成功安装后,将bin目录的路径导出到您的PATH环境变量中:
export PATH="$HOME/.fastlane/bin:$PATH"
  1. 在此期间,还添加以下区域设置:
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
  1. 在终端中打开一个新会话。这个新会话将加载您刚刚对环境变量所做的更改。首先,确保您已安装bundler。如果尚未安装,请使用以下命令:
[sudo] gem install bundler
  1. 然后,切换到您的工作目录的根目录。然后,使用以下命令初始化fastlane
fastlane init

作为过程的一部分,您将被问及一些问题。首先是您的包名称。当您留空时,将提供默认值,因此请输入您的包名称:

  1. 接下来,您将被要求提供某个服务操作 JSON 秘密文件的路径。只需按Enter,因为我们暂时不需要它;稍后可以提供。最后,您将被问及是否要上传一些元数据等内容。请谦逊地拒绝;您可以稍后使用以下命令进行设置:
fastlane supply init

还会有一些其他提示,您只需按Enter键即可。

  1. 完成后,使用以下命令运行您的测试:
fastlane test

一切顺利时,您应该会看到以下结果:

总结

在本章中,您已经了解了 CI 和测试的概念。您已经学会了如何使用 ATSL 编写测试。

您了解了测试中最流行的两个阶段以及如何在 Google Play 控制台中设置它们。您尝试了 Crashlytics,并体验了其崩溃报告功能等。然后您了解了 CI,并且作为示例,您使用了名为 Fastlane 的 CI 工具之一。

哇,这一章真的很充实,您已经到达了结尾。在下一章中,您将学习如何“让您的应用程序面向全球”。有趣,对吧?好吧,让我们继续吧;我们下一章再见。

第十四章:使您的应用程序面向全球

在经过多个小时的工作和学习许多新知识后构建应用程序,开发人员的最终满足感是看到用户轻松下载并享受使用应用程序的体验,从中获得最大的使用价值。

在本章中,我们将学习通过 Google Play 商店和亚马逊应用商店分发我们的应用程序所涉及的各种步骤。我们还将了解数字签名我们的应用程序以验证其真实性。

在本章中,重点将是学习以下内容:

  • 通过 Android Studio 和命令行生成密钥库

  • 通过 Google Play 商店发布应用程序

  • 通过亚马逊应用商店发布应用程序

密钥库生成

Android 的最重要的安全功能之一是允许安装 APK,但只能来自受信任的来源,如 Google Play 商店或亚马逊应用商店。这些分发渠道要求开发人员对应用程序进行身份验证,声明这确实是他或她打算分发的应用程序。

应用程序的所有者,即开发人员,将拥有私钥,并且分发渠道将使用相应的公钥对其进行签名。公钥和私钥的组合意味着数字签名存储在keyStore文件中。keyStore是一个二进制文件,其中存储了用于对应用程序进行签名的数字密钥。

在将应用程序发布到 Google Play 商店进行分发之前,必须对 APK 进行数字签名。数字签名用作开发人员的身份验证,并确保只能通过受信任的来源进行应用程序更新。

保持密钥库文件的安全并记住密钥密码非常重要。一旦使用密钥库文件对应用程序进行签名并发布,任何对应用程序的进一步更新只能使用相同的密钥进行。

KeyStore可以通过几种方式生成:

  • Android Studio

  • 命令行

让我们详细讨论生成密钥库所涉及的步骤。

通过 Android Studio 生成密钥库

这些是我们需要遵循的通过 Android 生成密钥库的步骤:

  1. 一旦打开我们希望为其生成 APK 的项目,点击“构建|生成已签名 APK”这将导致“生成已签名 APK”屏幕显示。 Android Studio 期望用户选择密钥库路径:

  1. 由于我们将生成一个新的密钥库,点击“创建新”按钮。这将显示如下的“新密钥库”窗口:

  1. 选择密钥库路径并为.jks(Java 密钥库)文件提供名称:

  1. 确认密钥库路径后,我们需要填写密钥库密码、密钥别名、密钥别名密码、名字和姓氏、组织单位、组织、城市或地点、州或省和国家代码(XX):

  1. 一旦填写了所需的详细信息并点击“确定”按钮,我们应该能够继续进行“生成已签名 APK”屏幕。点击下一步:

  1. 在下一个屏幕上,我们将有选择 APK 目标文件夹和构建类型的选项。然后,点击“完成”按钮:

  1. 完成后,控制台中显示已签名 APK 的生成确认以及定位或分析 APK 的选项:

  1. 已签名 APK 经过数字签名,可以通过 Google Play 商店和其他发布平台发布,并且可以在目标文件夹中找到:

  1. 现在我们已经生成了密钥库,以后每当我们更新应用程序时,Android Studio 都会提供我们生成已签名 APK 的屏幕,并期望填写密码:

按照新密钥库 生成中描述的相同过程,用户应该能够生成已签名的 APK。

通过 Android Studio 自动签署 APK

我们有选项,可以在对应用程序进行更改时自动生成已签名的 APK。这可以通过在 Android Studio 中执行以下操作来实现:

  1. 右键单击App | 项目结构

  2. 选择签名标签。在此标签中,我们需要提供应用程序签名配置的详细信息。在这里,我们将其命名为config,并存储密钥别名、密码和存储文件的路径:

添加签名config将导致签名详细信息以纯文本形式添加到build.gradle文件中:

android {
     signingConfigs {
         config {
             keyAlias 'packtkey' keyPassword 'vasantham' storeFile file('G:/newkey/dreamindia.jks')
             storePassword 'vasantham' } 

将此信息移出构建文件以确保敏感信息不易被他人访问是明智的。在项目的根目录中,我们应该创建一个名为keystore.properties的文件。该文件将包含以下信息:

storePassword = OurStorePassword
KeyPassword = ourKeyPassword
keyAlias = ourKeyAlias
storeFile = ourStoreFileLocation

由于我们已经将密钥库详细信息移动到单独的文件中,现在我们应该在build.gradle文件中添加以下代码,以确保签名配置可用于自动签署 APK。我们应该在android{}块之前加载keystore.properties文件。

在此代码中,我们创建了一个名为keystorePropertiesFile的变量,并将其初始化为我们创建的keystore.properties文件。此外,我们初始化了一个名为keyStoreProperties的新Properties()对象。keystorePropertiesFile的详细信息被加载到keystoreProperties对象中:

def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))

android {
.......
}

通过添加上述代码,我们可以使用keystoreProperties['propertyName']的语法引用存储在keystoreProperties中的所有属性我们应该能够修改build.gradle文件中的签名配置,如下面的代码所示:

android {
     signingConfigs {
         config {
             keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile file(keystoreProperties['storeFile'])
             storePassword keystoreProperties['storePassword'] } 
}
..........
}

构建文件现在是安全的,不包含任何敏感信息。如果我们要使用任何源代码控制或共享代码库,我们需要确保删除并保护keystore.properties文件。

构建类型和风味

开发人员可以通过build.gradle文件维护构建类型中的变体,并且可以通过配置来配置这些变体。该配置使开发人员能够在同一应用程序中维护调试代码和发布版本的代码:

  • 调试:打开调试选项并且也可以使用调试密钥签署应用程序

  • 发布:关闭调试选项,使用发布密钥签署应用程序,并且还会减少或删除最终 APK 中与调试相关的代码

我们可以在 Android Studio 中定义调试或发布类型的构建:

  1. 右键单击 app | 项目结构。

  2. 在构建类型标签中,我们需要添加一个新的构建变体类型。我们有两种构建类型,调试和发布,如下截图所示。在创建构建类型时,我们将有选项选择构建变体的签名配置:

这将在build.gradle文件中添加以下代码:

    buildTypes {
         release {
             minifyEnabled false proguardFiles getDefaultProguardFile('proguard-
             android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.config }
     }

在应用程序世界中,为免费应用程序提供基本功能,并为付费应用程序提供高级功能是一种通用规范。Android 提供了将它们定义为productFlavors的选项。

免费和付费是开发人员常用的产品风味。通过定义各种产品风味,开发人员将有选择地维护不同的代码,从而为同一应用程序提供不同或额外的功能。免费和付费版本提供的共同功能的代码基础可以相同,而付费产品风味版本可以启用高级功能。

要定义产品口味,右键单击app |** Project Structure**,在 Flavors 选项卡中,可以定义产品口味-免费或付费。签名配置也可以自定义以匹配productFlavors

build.gradle文件将包含以下信息:

android {
........
     productFlavors {
         paid {
             signingConfig signingConfigs.paidconfig }
         free {
             signingConfig signingConfigs.freeconfig }
     }
 }

通过命令行生成密钥库

密钥库也可以通过使用 keytool 命令行生成。keytool 可在 jdk 的bin目录中找到:

启动命令提示符并运行以下命令:

keytool -genkey -v -keystore dreamindiacmd.jks -keyalg RSA -keysize 2048 -validity 10000 -alias packtcmdkey

上述命令需要一个密钥库路径,用于密钥签名的安全算法类型,密钥大小,有效期和密钥别名。执行上述命令后,我们需要提供密码和一些其他额外的细节,如下面的屏幕截图所示:

成功执行命令后,我们可以在 keytool 的相同位置找到生成的keystore文件:

在 Google Play 商店发布应用

现在我们有签名发布版本的 APK 可用,是时候通过 Google Play 商店进行全球分发了。

注册 Google Play 商店开发者帐户需要一次性费用 25 美元。登录play.google.com/apps/publish/并完成注册过程。

Google Play 商店提供了一个名为 Google Play 控制台的优秀控制台,该控制台包含了管理 Android 应用程序发布生命周期所需的所有功能。我们将看一下使我们能够发布应用的重要功能。

发布应用的第一步是在 Google Play 控制台中创建应用程序。控制台提供了创建应用程序的选项,从而启动了发布流程:

一旦我们点击“创建应用程序”,就会提示我们输入默认语言和应用程序的标题。点击创建按钮将为我们创建应用程序:

开发者控制台提供了许多选项供开发者在菜单中填写。但是,有四个重要且必填的部分需要填写,以确保应用程序可以发布。

这四个部分分别是应用发布、商店列表、内容评级和定价与分发:

现在,让我们专注于这些必填部分需要填写的细节。

应用发布部分

应用发布部分使开发者能够管理 APK 发布的整个生命周期。开发者可以在将 APK 移至公共分发之前,将其应用于内部测试、alpha 和 beta 发布。发布的各个阶段帮助开发者收集有关应用的反馈,通过限制应用,使其仅对特定用户可用:

我们需要上传为构建类型发布生成的签名 APK,以便进行生产。可以浏览 APK 文件并将其上传到 Play 商店:

一旦 APK 上传完成,可以在发布部分找到相同的版本代码和删除 APK 的选项。上传签名的 APK 完成了应用发布部分所需的详细信息:

商店列表部分

商店列表部分是接下来要关注的部分。这是一个重要的部分,因为用户将在这里看到应用的各种截图、简短和详细描述。开发人员可以选择保存草稿,并随时返回继续填写详细信息:

在 Google Play 商店中,商店列表部分要求以下内容:

  • 应用的两个截图

  • 高分辨率图标 - 512 * 512

  • 特色图形 - 1,024 W x 500 H:

可以使用免费的图像编辑器(如gimp)创建图形资产。遵循图形规范的指南非常重要且强制性。

开发人员需要提供应用程序的类型和类别以及联系方式和隐私政策(如果有的话)。一旦提供了所有详细信息,商店列表部分将完成。

内容评级部分

开发人员应该对应用中提供的内容进行自我声明。内容评级部分有一个问卷,要求开发人员提供具体答案。回答问卷是一项简单的任务:

开发人员提供有关应用内容的正确信息非常重要,因为提供错误信息可能会影响商店列表。

定价和分发部分

最后一个强制性部分,定价和分发,要求开发人员提供与其应用定价相关的信息 - 免费或付费,应用分发的国家列表,应用是否主要面向儿童,应用是否包含广告,内容指南,以及开发人员承诺遵守美国出口法的确认:

一旦开发人员在定价和分发部分提供了所有必要的详细信息,将出现“准备发布”的消息。还要注意,所有四个强制性部分都标记为绿色,表示已完成:

一旦应用提交发布,将在几小时内进行审核并提供下载。如果有任何疑问,开发人员需要解决并重新提交应用以发布。

在亚马逊应用商店发布应用

亚马逊应用商店为开发人员提供了一个免费的市场来分发他们的 Android 应用。开发人员可以登录并在以下网址创建他们的免费账户:developer.amazon.com/apps-and-games/app-submission/android.

一旦我们登录应用商店,我们需要点击亚马逊应用商店中的“添加 Android 应用”按钮:

亚马逊应用商店要求填写以下部分:常规信息、可用性和定价、描述、图像和多媒体、内容评级和二进制文件。

让我们详细看看这些部分。

常规信息

在常规信息部分,开发人员需要提供有关应用标题、包名称、应用 ID、发布 ID、应用类别以及开发人员的联系方式的信息:

可用性和定价部分

在这一部分,开发人员需要提供以下信息:

  • 应用的定价 - 免费或付费

  • 国家列表

  • 应用发布的日期和时间:

描述部分

在描述部分,开发人员需要填写有关标题、简短描述和长描述的详细信息:

该部分还使开发人员能够提供产品特色项目和识别应用的特定关键字。用户还可以选择添加本地化描述:

图像和多媒体部分

在图像和多媒体部分,开发人员需要输入与应用相关的图形资产。用户需要提供:

  • 图标:512 * 512 PNG 和 114 * 114 PNG

  • 屏幕截图:3 到 10 个 PNG 或 JPG

还有一个选项可以提供与平板电脑和手机等形态因素相关的图形:

内容评级部分

在内容评级部分,开发人员需要回答一系列与应用中显示的内容性质相关的问题。这些问题属于主题:

开发人员需要回答关于使用基于位置的服务、应用中的广告、提供隐私政策(如果有的话)以及披露应用是否面向 13 岁以下儿童的问题:

二进制文件部分

在此部分,开发人员应上传从 Android Studio 或命令行生成的已签名 APK:

开发人员还可以决定设备支持、语言支持、出口合规性和使用亚马逊地图重定向的选项:

非亚马逊 Android 设备的设备支持默认情况下是未启用的。开发人员需要通过单击“编辑设备支持”并进行所需更改来显式启用此功能。

填写完所有必需信息后,现在是时候在亚马逊应用商店中实际发布应用了。开发人员将有一个选项来审查他们输入的所有信息:

摘要

商店列表、关键字、描述等在应用识别和最终应用及开发人员的成功方面起着重要作用。

在本章中,我们讨论了使用 Android Studio 生成密钥库文件、自动签名 APK、从命令行生成密钥库文件以及通过 Google Play 商店和亚马逊应用商店发布应用所涉及的各种步骤。

在下一章中,我们将学习如何使用我们可以使用的最有趣和重要的 API 之一——Google Faces API。Google Faces API 使开发人员能够提供诸如面部检测、照片中人物的识别等酷功能。

第十五章:使用 Google Faces API 构建应用程序

计算机执行识别对象等任务的能力一直是软件和所需架构的巨大任务。自从谷歌、亚马逊和其他一些公司已经完成了所有艰苦的工作,提供了基础架构,并将其作为云服务提供,这种情况已经不再存在。应该注意的是,它们可以像进行 REST API 调用一样容易访问。

在本章中,您将学习如何使用谷歌移动视觉 API 的人脸检测 API 来检测人脸,并添加有趣的功能,比如给用户的图片添加兔子耳朵。

在本章中,将涵盖以下主题:

  • 在图像中识别人脸

  • 从摄像头源跟踪人脸

  • 识别面部的特定部位(例如眼睛、耳朵、鼻子和嘴巴)

  • 在图像中的特定部位上绘制图形(例如,在用户的耳朵上添加兔子耳朵)

移动视觉简介

移动视觉 API 提供了一个框架,用于在照片和视频中查找对象。该框架可以定位和描述图像或视频帧中的视觉对象,并具有一个事件驱动的 API,跟踪这些对象的位置。

目前,Mobile Vision API 包括人脸条形码文本检测器。

人脸 API 概念

在深入编码功能之前,有必要了解人脸检测 API 的基本概念。

来自官方文档:

人脸检测是自动在视觉媒体(数字图像或视频)中定位人脸的过程。检测到的人脸将以位置、大小和方向进行报告。一旦检测到人脸,就可以搜索眼睛和鼻子等地标。

需要注意的一个关键点是,只有在检测到人脸后,才会搜索眼睛和鼻子等地标。作为 API 的一部分,您可以选择不检测这些地标。

请注意人脸检测和人脸识别之间的区别。前者能够从图像或视频中识别人脸,而后者则可以做到同样,并且还能够告诉人脸之前是否已经被识别过。前者对其之前检测到的人脸没有记忆。

在本节中,我们将使用一些术语,所以在我们进一步之前,让我给您概述一下每个术语:

人脸跟踪将人脸检测扩展到视频序列。当视频中出现人脸时,可以将其识别为同一个人并进行跟踪。

需要注意的是,您正在跟踪的人脸必须出现在同一个视频中。此外,这不是一种人脸识别形式;这种机制只是根据视频序列中面部的位置和运动进行推断。

地标是面部内的一个感兴趣的点。左眼、右眼和鼻子底部都是地标的例子。人脸 API 提供了在检测到的人脸上找到地标的能力。

分类是确定某种面部特征是否存在。例如,可以根据面部是否睁着眼睛、闭着眼睛或微笑来对面部进行分类。

入门-检测人脸

您将首先学习如何在照片中检测人脸及其相关的地标。

为了追求这一目标,我们需要一些要求。

在 Google Play 服务 7.8 及以上版本中,您可以使用 Mobile Vision API 提供的人脸检测 API。请确保您从 SDK 管理器中更新您的 Google Play 服务,以满足此要求。

获取运行 Android 4.2.2 或更高版本的 Android 设备或配置好的 Android 模拟器。最新版本的 Android SDK 包括 SDK 工具组件。

创建 FunyFace 项目

创建一个名为 FunyFace 的新项目。打开应用程序模块的build.gradle文件,并更新依赖项以包括 Mobile Vision API:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.google.android.gms:play-services-vision:11.0.4'
    ...
}

好了,让我们开始吧——这就是你将看到所有这些如何发挥作用的地方。

<meta-data
 android:name="com.google.android.gms.vision.DEPENDENCIES"
 android:value="face" />

在你的detectFace()方法中,你将首先从 drawable 文件夹中将图像加载到内存中,并从中创建一个位图图像。由于当检测到面部时,你将更新这个位图来绘制在上面,所以你需要将它设置为可变的。这就是使你的位图可变的方法。

为了简化操作,对于这个实验,你只需要处理应用程序中已经存在的图像。将以下图像添加到你的res/drawable文件夹中。

现在,这就是你将如何进行面部检测的方法。

现在,更新你的AndroidManifest.xml,包括面部 API 的元数据。

首先将图像加载到内存中,获取一个Paint实例,并基于原始图像创建一个临时位图,然后创建一个画布。使用位图创建一个帧,然后在FaceDetector上调用 detect 方法,使用这个帧来获取面部对象的SparseArray

创建一个 Paint 实例。

查看以下代码:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    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"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context="com.packtpub.eunice.funyface.MainActivity">

  <ImageView
      android:id="@+id/imageView"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:src="img/ic_launcher_round"
      app:layout_constraintBottom_toTopOf="parent"
      android:scaleType="fitCenter"/>

  <Button
      android:id="@+id/button"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_gravity="bottom|center"
      android:text="Detect Face"/>

</FrameLayout>

这就是你在这里需要做的一切,这样你就有了一个带有ImageView和一个按钮的FrameLayout。现在,打开MainActivity.kt并添加以下导入语句。这只是为了确保你在移动过程中从正确的包中导入。在你的onCreate()方法中,将点击监听器附加到MainActivity布局文件中的按钮。

package com.packtpub.eunice.funface

import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity
import com.google.android.gms.vision.Frame
import com.google.android.gms.vision.face.FaceDetector
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 {
            detectFace()
        }
    }
}

加载图像。

使用Paint API 获取Paint类的实例。你只会在面部周围绘制,而不是整个面部。为此,设置一个细线,给它一个颜色,在我们的例子中是红色,并将绘画样式设置为STROKE

options.inMutable=true

查看以下实现:

private fun detectFace() {
    // Load the image
    val bitmapOptions = BitmapFactory.Options()
    bitmapOptions.inMutable = true
    val myBitmap = BitmapFactory.decodeResource(
            applicationContext.resources,
            R.drawable.children_group_picture,
            bitmapOptions)
}

现在,你的应用程序已经准备好使用面部检测 API。

现在,你将使用faceDetector实例的detect()方法来获取面部及其元数据。结果将是SparseArrayFace对象。

// Get a Paint instance
val myRectPaint = Paint()
myRectPaint.strokeWidth = 5F
myRectPaint.color = Color.RED
myRectPaint.style = Paint.Style.STROKE 

Paint类保存与文本、位图和各种形状相关的样式颜色的信息。

创建一个画布。

要获得画布,首先使用之前创建的位图的尺寸创建一个位图。有了这个画布,你将在位图上绘制面部的轮廓。

// Create a canvas using the dimensions from the image's bitmap
val tempBitmap = Bitmap.createBitmap(myBitmap.width, myBitmap.height, Bitmap.Config.RGB_565)
val tempCanvas = Canvas(tempBitmap)
tempCanvas.drawBitmap(myBitmap, 0F, 0F, null)

Canvas类用于保存绘制的调用。画布是一个绘图表面,它提供了各种方法来绘制到位图上。

创建面部检测器。

到目前为止,你所做的基本上是一些前期工作。现在你将通过 FaceDetector API 访问面部检测,你将在这个阶段禁用跟踪,因为你只想检测图像中的面部。

请注意,在第一次运行时,Play 服务 SDK 将需要一些时间来初始化 Faces API。在你打算使用它的时候,它可能已经完成了这个过程,也可能没有。因此,作为一个安全检查,你需要确保在使用它之前它是可用的。在这种情况下,如果FaceDetector在应用程序运行时还没有准备好,你将向用户显示一个简单的对话框。

还要注意,由于 SDK 的初始化,你可能需要互联网连接。你还需要确保有足够的空间,因为初始化可能会下载一些本地库到设备上。

// Create a FaceDetector
val faceDetector = FaceDetector.Builder(applicationContext).setTrackingEnabled(false)
        .build()
if (!faceDetector.isOperational) {
    AlertDialog.Builder(this)
            .setMessage("Could not set up the face detector!")
            .show()
    return
}

检测面部。

首先,打开你的activity_main.xml文件,并更新布局,使其包含一个图像视图和一个按钮。

// Detect the faces
val frame = Frame.Builder().setBitmap(myBitmap).build()
val faces = faceDetector.detect(frame)

在面部上绘制矩形。

现在你有了面部,你将遍历这个数组,以获取面部边界矩形的坐标。矩形需要左上角和右下角的xy,但可用的信息只给出了左上角的位置,所以你需要使用左上角、宽度和高度来计算右下角。然后,你需要释放faceDetector以释放资源。

// Mark out the identified face
for (i in 0 until faces.size()) {
    val thisFace = faces.valueAt(i)
    val left = thisFace.position.x
    val top = thisFace.position.y
    val right = left + thisFace.width
    val bottom = top + thisFace.height
    tempCanvas.drawRoundRect(RectF(left, top, right, bottom), 2F, 2F, myRectPaint)
}

imageView.setImageDrawable(BitmapDrawable(resources, tempBitmap))

// Release the FaceDetector
faceDetector.release()

结果。

一切准备就绪。运行应用程序,点击“检测面部”按钮,然后等一会儿...

该应用程序应该能够检测到人脸,并在人脸周围出现一个方框,完成:

好的,让我们继续为他们的脸部添加一些乐趣。要做到这一点,您需要确定您想要的特定地标的位置,然后在其上绘制。

要找出地标的表示,这次您要对它们进行标记,然后在所需位置绘制您的滤镜。

要进行标记,请更新绘制人脸周围矩形的 for 循环:

// Mark out the identified face
for (i in 0 until faces.size()) {
    ...

    for (landmark in thisFace.landmarks) {
        val x = landmark.position.x
        val y = landmark.position.y

        when (landmark.type) {
            NOSE_BASE -> {
                val scaledWidth = 
                       eyePatchBitmap.getScaledWidth(tempCanvas)
                val scaledHeight = 
                       eyePatchBitmap.getScaledHeight(tempCanvas)
                tempCanvas.drawBitmap(eyePatchBitmap,
                        x - scaledWidth / 2,
                        y - scaledHeight / 2,
                        null)
            }
        }
    }
}

运行应用程序并注意各个地标的标签:

就是这样!很有趣,对吧?

摘要

在本章中,您学习了如何使用移动视觉 API,这里使用的是 Faces API。这里有几件事情需要注意。该程序并非针对生产进行优化。您可以自行加载图像并在后台线程中进行处理。您还可以提供功能,允许用户从除静态源之外的不同来源选择图像。您还可以更有创意地使用滤镜以及它们的应用方式。此外,您还可以在 FaceDetector 实例上启用跟踪功能,并输入视频以尝试人脸跟踪。

第十六章:您可能会喜欢的其他书籍

如果您喜欢这本书,您可能会对 Packt 的其他书感兴趣:

使用 Kotlin 实践微服务

Juan Antonio Medina Iglesias

ISBN:9781788471459

  • 了解微服务架构和原则

  • 使用 Spring Boot 2.0 和 Spring Framework 5.0 在 Kotlin 中构建微服务

  • 使用 Spring WebFlux 创建执行非阻塞操作的响应式微服务

  • 使用 Spring Data 从 MongoDB 响应式获取数据

  • 使用 JUnit 和 Kotlin 进行有效测试

  • 使用 Spring Cloud 创建云原生微服务

  • 构建和发布微服务的 Docker 镜像

  • 使用 Docker Swarm 扩展微服务

  • 使用 JMX 监控微服务

  • 在 OpenShift Online 中部署微服务

使用 Spring 5 和 Kotlin 构建应用程序

Miloš Vasić

ISBN:9781788394802

  • 使用 Kotlin 探索 Spring 5 的概念

  • 学习依赖注入和复杂配置

  • 在您的应用程序中利用 Spring Data,Spring Cloud 和 Spring Security

  • 使用 Project Reactor 创建高效的响应式系统

  • 为您的 Spring/Kotlin 应用编写单元测试

  • 在 AWS 等云平台上部署应用程序

留下评论-让其他读者知道您的想法

请在购买书籍的网站上留下您对本书的想法。如果您从亚马逊购买了这本书,请在该书的亚马逊页面上留下诚实的评论。这对其他潜在读者来说非常重要,他们可以看到并使用您的公正意见来做出购买决策,我们可以了解我们的客户对我们的产品的看法,我们的作者可以看到您与 Packt 合作创建的标题的反馈。这只需要您几分钟的时间,但对其他潜在客户,我们的作者和 Packt 都是有价值的。谢谢!

标签:val,Kotlin,安卓,应用程序,构建,fun,Android,我们
From: https://www.cnblogs.com/apachecn/p/18206329

相关文章

  • Unity安卓IOS一键打包
    添加菜单构建按钮,使用下面API进行构建,注意设置和配置等usingSystem;usingSystem.IO;usingAssetBundles;usingLiXiaoQian.Common.Editor.Tools;usingUnityEditor;usingUnityEngine;///打包工具publicclassBuildTool{[MenuItem("Tools/构建/Android平台")]......
  • 德邦快递携手火山引擎,构建“数据飞轮”实现精准营销
     在快递行业中,数据的复杂性和多样性一直是企业面临的一大挑战。 在近日的采访中,德邦快递谈到通过引入火山引擎数智平台VeDI旗下系列数据产品,解决了长期困扰其营销活动的数据“黑盒”问题,显著提升了用户识别和营销效率,实现了月活用户和下单用户数的跃升。 德邦快递数字......
  • 使用-Danfo-js-构建数据驱动应用-全-
    使用Danfo.js构建数据驱动应用(全)原文:zh.annas-archive.org/md5/074CFA285BE35C0386726A8DBACE1A4F译者:飞龙协议:CCBY-NC-SA4.0前言大多数数据分析师使用Python和pandas进行数据处理和操作,这得益于这些库提供的便利性和性能。然而,JavaScript开发人员一直希望浏览器......
  • 整合LlamaIndex与LangChain构建高级的查询处理系统
    构建大型语言模型应用程序可能会颇具挑战,尤其是当我们在不同的框架(如Langchain和LlamaIndex)之间进行选择时。LlamaIndex在智能搜索和数据检索方面的性能令人瞩目,而LangChain则作为一个更加通用的应用程序框架,提供了更好的与各种平台的兼容性。本篇文章将介绍如何将LlamaIndex和La......
  • 高德地图安卓sdk,在uniapp中实现,地图上多个坐标点,点击坐标点,显示坐标信息
     <template><viewclass="content"><mapid="map":style="{width:'100%',height:'50vh'}":markers="markers":longitude="longitude":latitude=......
  • 《构建之法》阅读笔记之二
    第二部分:实践指南主题: 构建的实际应用内容概要:构建过程: 详细介绍了构建过程中的各个阶段,包括需求分析、设计、开发、测试等。对每个阶段的任务和方法进行了具体的描述,例如需求分析阶段可以采用用户故事、用例分析等方法;设计阶段可以采用面向对象设计、设计模式等方法。构......
  • 《构建之法》阅读笔记之一
    第一部分:理论基础主题: 构建的概念与原理内容概要:构建的本质: 构建是指通过组合和搭建各种元素来创造新的事物或系统。作者解释了构建的概念,强调了它在现代技术和工程领域中的重要性。构建不仅仅是建造物理产品,也包括软件、服务、组织结构等方面的构建。构建的原理: 介绍了......
  • 《构建之法》阅读笔记之三
    第三部分:进阶探索主题: 构建的未来发展内容概要:新技术趋势: 探讨了当前和未来构建领域的新技术趋势,如人工智能、区块链、物联网等。分析了这些新技术对构建方式和方法的影响,以及如何利用这些技术来创新和优化构建过程。构建文化: 强调了构建文化的重要性,包括团队协作、创新......
  • uniApp生成的h5页面禁止浏览器上缩放页面(支持安卓,ios)
    项目场景:uniapph5内嵌原生appios样式问题:1.双击和双指滑动,内嵌的h5页面均会被放大缩小2.修改ios底部的安全距离的背景色,默认是白色问题描述1.双击和双指滑动,内嵌的h5页面均会被放大缩小2.解决ios底部的安全距离和修改背景色,默认是白色解决方案:安卓只需要在h5.template.h......
  • 突破边界:基于Windows 11的高效渗透测试系统构建
    在这篇文章中,我将向大家推荐一款基于Windows11的渗透测试系统,由一位行业内大佬封装而成。这个名为Windows11PenetrationSuiteToolkit的项目旨在提供一个开箱即用的Windows渗透测试环境,方便安全专家和爱好者进行渗透测试工作。项目地址你可以在GitHub上找到该项目:W......