Kotlin 安卓开发学习手册(六)
十九、使用外部库
外部库是通用的接口和类的集合,因此可以在各种项目中重用。您还不会找到很多 Kotlin 库,但是因为 Kotlin 很容易与 Java 类和接口连接,所以在您的项目中,您可以使用其他开发人员和开发团队发布的一个或多个 Java 库。
外部库的示例领域包括编码和解码、压缩、CSV 文件处理、电子邮件、高级数学和统计、数据库、扩展日志记录工具、XML 和 JSON 文件处理等等。在第二十章中,你会学到更多关于 XML 和 JSON 的知识。
这一章的其余部分将讨论如何在你的 Android 项目中添加外部库,如果你添加了外部 Java 库,将深入探讨与可空性相关的特性,并描述如何构建你自己的库。
添加外部库
添加外部库的第一步是指定库的来源。可以加载或包含库的地方被称为库。一旦你开始一个新的 Android 项目,项目的build.gradle脚本包含两个地方的库,在buildscript部分和allprojects部分:
buildscript {
...
repositories {
google()
jcenter()
}
...
}
allprojects {
...
repositories {
google()
jcenter()
}
...
}
...
应用依赖项使用来自allprojects部分的存储库。来自buildscript部分的存储库转而引用构建过程的插件和依赖项。我们想要添加应用库,而不是调整构建过程,所以我们应该看看allprojects部分。这里可以指定以下存储库:
-
这是一个储存库,从这里可以加载 Android 特有的库。对于 Android 项目来说,这总是包括在内并且总是必要的,但是它通常不是您查找特定于应用的库的地方。换句话说,这不是我们用来存放外部库的地方。
-
mavenCentral():这是位于https://repo1.maven.org/maven2的原始 Maven 仓库。在谈到 Maven 构建系统时,大多数开发人员首先想到的是这个用于添加库的存储库。然而,对于 Android 来说,第一选择是使用jcenter来代替。 -
jcenter():在http://jcenter.bintray.com引用一个替代的 Maven 库。通常偏向于jcenter而不是mavenCentral不会有什么坏处,但是在很多情况下两者都可以工作,甚至有可能两者都指定。不同之处可能表现在下载库的不同性能,以及不同的“最新”库版本。jcenter人声称他们的库比mavenCentral更大更快。 -
mavenLocal():无论你如何使用 Maven 作为构建系统,在你的开发机器上,一个缓存将会被构建起来,并永久地填充你从任何 Maven 库(包括jcenter)下载的库。此外,如果您创建了一个 Maven 库项目并安装了它,那么这个库将会出现在这个缓存中,即使您从未打算将它上传到一个官方的公共存储库中。mavenLocal()存储库在缓存中查找库依赖关系。请注意,您通常会在 PC 用户的home文件夹中的.m2下找到该缓存。 -
maven { url 'http://example.com/maven'}:您可以使用它来添加一个定制的 Maven 资源库。如果您使用私人或公司的 Maven 存储库,这将非常方便。注意,google()和jcenter()库只是maven { url 'https://dl.google.com/dl/android/maven2/' }和maven { url 'https://jcenter.bintray.com/' }的快捷方式。 -
ivy { url 'http://example.com/ivy'}:您可以使用它来添加一个 Apache Ivy 存储库。
在大多数情况下,使用默认设置就可以了:如图所示使用google()和jcenter()。您可以尝试这些方法,并在必要时添加新的存储库。
随着存储库的建立,我们现在可以以依赖关系的形式添加实际的库。这在模块的build.gradle文件中效果最好。对于一个新项目,该文件的dependencies部分可能是这样的:
dependencies {
implementation
fileTree(dir: 'libs', include: ['*.jar'])
implementation
"org.jetbrains.kotlin:kotlin-stdlib-jdk7:
$kotlin_version"
implementation
"com.android.support:appcompat-v7:28.0.0"
implementation
"com.android.support.constraint:
constraint-layout:1.1.3"
testImplementation
"junit:junit:4.12"
androidTestImplementation
"com.android.support.test:runner:1.0.2"
androidTestImplementation
"com.android.support.test.espresso:
espresso-core:3.0.2"
}
(每一项占一行。)细节这里没什么意思;你需要知道的是,对于新的外部库,我们必须添加另一行以implementation开头的代码。这个新条目的精确语法遵循以下格式:
implementation "MAVEN_GROUP_ID:MAVEN_ARTIFACT_ID:VERSION"
或者
implementation 'MAVEN_GROUP_ID:MAVEN_ARTIFACT_ID:VERSION'
这个组 ID、工件 ID 和版本的三元组也被称为 Maven 坐标。
一个等效的方法是使用一个参数化的表单(写在一行上,不换行):
implementation group: "MAVEN_GROUP_ID",
name: "MAVEN_ARTIFACT_ID",
version "VERSION"
同样,你也可以使用单引号。
这最好用一个例子来解释。假设您想要添加 Apache Commons 数学库,它允许复杂的数学计算。我们首先需要确定库的 Maven 坐标。有几种方法可以得到这些坐标。
-
该库可能有自己的网站,例如,Maven 坐标可以在下载中找到。
-
直接查看存储库的网站,并使用那里提供的搜索工具。
-
使用您选择的搜索引擎,输入一个搜索字符串,比如“apache commons math maven”
在大多数情况下,您将获得形式为 XML 字符串的 Maven 坐标
<!-- https://mvnrepository.com/artifact/
org.apache.commons/commons-math3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-math3</artifactId>
<version>3.2</version>
</dependency>
在dependency元素中,您可以看到组 ID、工件 ID 和版本。因此,通过查看标记名,可以很容易地从 XML 中推导出 Gradle 等价物。这里的转录是
implementation "org.apache.commons:commons-math3:3.2"
或者
implementation group: "org.apache.commons",
name : "commons-math3",
version : "3.2"
(一行)。
注意
有时你也会得到 Gradle 语法。即使上面写着compile 'org.apache.commons:commons-math3:3.2',也不要用compile而是用implementation来代替。compile关键字属于旧版本的 Gradle。
Android Studio 随后会要求您同步您的项目。这样做,然后你就可以在你的代码中使用这个新的库了。
依赖性管理
库可以依赖于其他库。这种情况经常发生,如果我们不得不手动添加一个库公开的所有依赖项,这可能会变成一场真正的噩梦。例如,另一个名为 Apache Commons Configuration 的 Apache Commons 库在 2.4 版中依赖于另外三个库,而这三个库又可能依赖于其他库,以此类推。幸运的是,Maven 会自动解析这种依赖关系,包括所有可传递的依赖关系,因此我们不需要做任何事情。
我们在这里提到这一点,是为了让你意识到这样的依赖和传递性依赖会极大地破坏我们的应用。例如,如果您添加了一个大小为 100 kb 的可疑库,由于依赖关系,它可能很容易膨胀到几兆字节。在现代设备上,这几乎不会造成真正的问题,但知道为什么应用文件在某些情况下会变得如此之大还是有好处的。
未解析的本地依赖项
如果您使用 Android Studio 创建一个新项目,模块的build.gradle文件的dependencies部分的第一行如下所示
fileTree(dir: 'libs', include: ['*.jar'])
这意味着你放入libs文件夹的任何.jar文件都将作为库添加到你的应用中。没有自动的依赖关系解析会发生,你必须自己下载库。这与 Maven 的依赖包含方法有些冲突,所以尽量避免使用这种技术。
外部库和可空性
我们知道,在 Kotlin 中,属性的可空性在提高程序稳定性方面起着重要的作用。如果我们包括外部 Java 库,情况就不同了。作为一种语言,Java 并不像 Kotlin 那样精确区分可空变量和不可空变量。为了能够使用外部 Java 库,Kotlin 假设所有函数调用参数和函数返回值都可以为空。
如果外部库的 API 文档说函数返回值不能是null,那么告诉 Kotlin 这个事实的唯一方法就是使用 Elvis 操作符,如果返回值是null则抛出一个异常:
val res = javaObject.function() ?:
throw Exception("Cannot happen")
创建自己的图书馆
您可以用任何想要的方式创建一个库,包括使用命令行或其他 ide,比如 Eclipse。在这一节中,我们将介绍如何使用 Android Studio 创建和使用库。
在 Android Studio 中,一个库项目实际上不仅仅是一个可以包含在其他项目中的.jar文件。它几乎是一个独立的应用,因为它可以包括 Android 配置文件和描述用户界面的文件。但是,没有人阻止我们使用 Android 库来定义可以在其他项目中使用的接口和类。
作为一个例子,我们定义了一个名为 StringRegex 的库,它通过一个运算符函数来扩展String类,用于检查正则表达式匹配,因此我们可以编写
val s = "The big brown fox jumps over the creek."
val containsOnlyLetters = s % "[A-Za-z ]*"
// -> false because of the "."
为了定义这个扩展函数,我们重载了%操作符rem()。代码是这样的
package org.foo.stringregex
operator fun String.rem(re:String):Boolean =
this.matches(Regex(re))
当然,你可以使用不同的东西。
我们首先在 Android Studio 中开始一个新的库项目。为此,请转到文件➤新➤新项目,并选择添加无活动。作为项目名,输入StringRegexApp,作为包名,输入org.foo.stringregex。作为保存位置,输入您喜欢的任何内容。让你选择 Kotlin 作为语言设置,作为一个最低的 API 级别,选择任何你认为合适的。在新的 Android Studio 项目窗口中,打开文件➤新➤新模块。选择 Java 库。输入StringRegex作为库名。其他设置在这里并不重要。项目视图现在看起来类似于图 19-1 。
图 19-1
StringRegex Android 库
删除 Java 类 MyClass,因为我们不需要它。在包org.foo.stringregex中,创建一个新的 Kotlin 文件。单击鼠标右键,然后选择“新建➤·Kotlin 文件/类”。作为名称输入stringregex并作为种类选择文件。
Android Studio 可能会显示一条警告,说明 Kotlin 未配置。如果是这种情况,单击 Configure,从菜单中选择 Java with Gradle,在询问您要启用 Kotlin 的模块的对话框中,选择 All modules。
注意
对于 Android Studio 3.3,Kotlin 配置向导中有一个 bug。在 Stringregex 模块的build.gradle文件中,您可能需要注释掉plugins中的插件版本:
id 'org.jetbrains.kotlin.jvm' //version '1.3.20'
打开stringregex文件,输入前面清单中显示的扩展功能代码。您现在可以关闭窗口了,因为我们将从一个客户端项目中引用它。
从 Android Studio 中打开的任何应用中,您可以选择我们为这本书创建的应用之一,打开settings.gradle文件,并添加两条语句。
include ':StringRegex'
project(':StringRegex').projectDir =
new File('../StringRegexApp/StringRegex')
其中,File()中的字符串必须指向我们刚刚创建的库模块。我们仍然需要声明模块依赖关系。为此,打开客户端应用模块的build.gradle文件,在dependencies部分添加
implementation project(":StringRegex")
这个过程可以根据你的需要对任意多个引用这个库的应用重复。
现在,您可以导入扩展函数,并在客户端代码中使用它。
import org.foo.stringregex.rem
...
val s = "The big brown fox jumps over the creek."
val containsOnlyLetters = s % "[A-Za-z ]*"
// -> false because of the "."
要查看或使用从库中生成的.jar文件,请在操作系统的文件系统浏览器中转到StringRegexApp / StringRegex / build / libs.
二十、XML 和 JSON
在第十九章中,我们学习了如何在我们的 Android 项目中包含外部库。Kotlin 的标准库中没有专门的 XML 和 JSON 处理类,所以为了完成 XML 和 JSON 相关的任务,我们使用适当的外部库,并以扩展函数的形式添加一些方便的函数。
注意
XML 和 JSON 都是结构化数据的格式规范。如果您的 Android 应用与外部世界通信以接收或发送标准化格式的数据,您将会经常使用它们。
本章假设您有一个示例应用,可以用来测试所提供的代码片段。使用你喜欢的任何应用或我们在本书中开发的应用之一。例如,您可以添加一些示例代码,为 activity 的onCreate()函数内部的测试提供Log输出,或者您可以使用一个使用 Android 测试方法的测试类。选择最适合您需求的方法。
XML 处理
XML 文件最简单的形式如下:
<?xml version="1.0" encoding="UTF-8"?>
<ProbeMsg>
<TimeStamp>2016-10-30T19:07:07Z</TimeStamp>
<ProbeId>1A6G</ProbeId>
<ProbeValue ScaleUnit="cm">37.4</ProbeValue>
<Meta>
<Generator>045</Generator>
<Priority>-3</Priority>
<Actor>P. Rosengaard</Actor>
</Meta>
</ProbeMsg>
注意
XML 允许更复杂的结构,如模式验证和名称空间。在本章中,我们只描述 XML 标签、属性和文本内容。您可以自由扩展本章中介绍的示例和实用程序函数,以包含这些扩展功能。
对于 XML 处理,使用以下范例之一或组合。
-
*DOM 模型:完整的树处理:*在文档对象模型(DOM)中,XML 数据被视为一个整体,由内存中的树状结构表示。
-
SAX:基于事件的处理:在这里,XML 文件被解析,每个元素或属性都会触发一个适当的事件。事件由回调函数接收,回调函数必须向 SAX 处理器注册。这种“告诉我你在做什么”的处理方式通常被称为推送解析。
-
StAX:基于流的处理:在这里,您执行诸如“给我下一个 XML 元素”之类的操作。与 SAX 不同,在 SAX 中我们有一个推送解析,对于 StAX,我们告诉解析器它必须做什么:“我告诉你你做什么。”这因此被称为拉解析。
在 Android 上,你通常处理小到中等大小的 XML 文件。因此,在本章中我们使用 DOM。对于读取,我们首先解析完整的 XML 文件,并将数据存储在内存中的 DOM 树中。在这里,像删除、更改或添加元素这样的操作很容易完成,并且发生在内存中;因此它们非常快。为了编写,我们从内存中取出完整的 DOM 树,并从中生成一个 XML 字符流,也许将结果写回到一个文件中。
对于 XML 处理,我们添加了 Java 参考实现 Xerces 作为外部库。在 Android Studio 中,打开模块的build.gradle文件,在dependencies部分添加:
implementation 'xerces:xercesImpl:2.12.0'
注意
Xerces 还实现了 SAX 和 StAX APIs,尽管我们将只使用它的 DOM 实现。
读取 XML 数据
借助 Xerces 实现,我们可以使用的 DOM 实现已经包含了读取 XML 元素所需的一切。然而,我们将添加几个扩展函数,大大提高 DOM API 的可用性。为此,创建一个包com.example.domext,或者您也可以使用任何其他合适的包名。在这个包中添加一个 Kotlin 文件dom.kt,其内容如下:
package com.example.domext
import org.apache.xerces.parsers.DOMParser
import org.w3c.dom.Document
import org.w3c.dom.Node
import org.xml.sax.InputSource
import java.io.StringReader
import java.io.StringWriter
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
fun parseXmlToDOM(s:String) : Document {
val parser: DOMParser = DOMParser()
return parser.let {
it.parse(InputSource(StringReader(s)))
it.document
}
}
fun Node.fetchChildren(withText:Boolean = false) =
(0..(this.childNodes.length - 1)).
map { this.childNodes.item(it) }.
filter { withText || it.nodeType != Node.TEXT_NODE }
fun Node.childCount() = fetchChildren().count()
fun Node.forEach(withText:Boolean = false,
f:(Node) -> Unit) {
fetchChildren(withText).forEach { f(it) }
}
operator fun Node.get(i:Int) = fetchChildren()[i]
operator fun Node.invoke(s:String): Node =
if(s.startsWith("@")) {
this.attributes.getNamedItem(s.substring(1))
}else{
this.childNodes.let { nl ->
val iter = object : Iterator<Node> {
var i: Int = 0
override fun next() = nl.item(i++)
override fun hasNext() = i < nl.length
}
iter.asSequence().find { it.nodeName == s }!!
}
}
operator fun Node.invoke(vararg s:String): Node =
s.fold(this, { acc, s1 -> acc(s1) })
fun Node.text() = this.firstChild.nodeValue
fun Node.name() = this.nodeName
fun Node.type() = this.nodeType
这些都是org.w3c.dom.Node的包级函数和扩展函数,具有以下特点:
-
在 DOM API 中,树中的每个元素(例如,从本节开始的 XML 数据中的
ProbeValue)都由一个Node实例表示。 -
我们添加了一个
parseXmlToDOM(s:String)包级函数,将 XML 字符串转换为Document。 -
我们给
Node添加了一个fetchChildren()函数,它返回一个节点的所有非文本子节点,这些子节点忽略了文本元素。如果添加with- Text=true作为参数,元素的文本节点会包含在子列表中,即使它们只包含空格和换行符。例如,在本节开头的 XML 数据中,节点Meta有三个子节点:Generator、Priority和Actor。使用withText=true,它们之间的空格和换行符也将被返回。 -
我们给
Node添加了一个childCount()函数,它计算一个节点的子元素的数量,不考虑文本元素。官方的 DOM API 没有为此提供功能。 -
我们给
Node添加了一个forEach()函数,允许我们以 Kotlin 的方式遍历一个节点的子节点。最初的 DOM API 没有提供这样的迭代器,因为它只有函数和属性hasChild- Nodes()、childNodes.length和childNodes.item(index:Int)来遍历子元素。如果添加withText=true作为参数,元素的文本节点将包含在子列表中,即使它们只包含空格和换行符。 -
我们为
Node添加了一个get(i:Int)函数,以从元素中获取某个子元素,而不考虑文本节点。 -
我们重载了
Node的invoke运算符,属于括号()。带有String参数的第一个变量通过名称导航到一个孩子:node("cn")=node→名为“cn”的孩子如果参数以一个@开始,属性被寻址:node("@an")=node→属性名为“an”在后一种情况下,您仍然需要调用text()来获得字符串形式的属性值。 -
重载的
invoke操作符的第二个变体允许我们指定几个字符串,从一个子元素导航到另一个子元素,等等。 -
我们向
Node添加函数:首先,text()获取元素的文本内容,然后name()给出节点名,然后type()计算节点类型(可能的值参见Node类的常量属性)。
警告
为了简单起见,本节中显示的 DOM 处理代码片段没有以合理的方式处理异常。在将代码用于生产项目之前,必须添加适当的错误处理。
这个片段提供了如何使用 API 和扩展的例子。
import ...
import com.example.domext.*
...
val xml = """<?xml version="1.0" encoding="UTF-8"?>
<ProbeMsg>
<TimeStamp>2016-10-30T19:07:07Z</TimeStamp>
<ProbeId>1A6G</ProbeId>
<ProbeValue ScaleUnit="cm">37.4</ProbeValue>
<Meta>
<Generator>045</Generator>
<Priority>-3</Priority>
<Actor>P. Rosengaard</Actor>
</Meta>
</ProbeMsg>"""
try {
// Parse the complete XML document
val dom = parseXmlToDOM(xml)
// Access an element
val ts = dom("ProbeMsg")("TimeStamp").text()
Log.d("LOG", ts) // 2001-11-30T09:08:07Z
// Access an attribute
val uni = dom("ProbeMsg")("ProbeValue")("@ScaleUnit")
Log.d("LOG", uni.text()) // cm
// Simplified XML tree navigation
val uni2 = dom("ProbeMsg","ProbeValue","@ScaleUnit")
Log.d("LOG", uni2.text()) // cm
// Iterate through an element's children
dom("ProbeMsg")("Meta").forEach { n ->
Log.d("LOG", n.name() + ": " + n.text())
// Generator: 045
// Priority: -3
// Actor: P. Rosengaard
}
}catch(e:Exception) {
Log.e("LOG", "Cannot parse XML", e)
}
...
改变 XML 数据
一旦我们在内存中有了 XML 树的 DOM 表示,我们就可以添加元素了。虽然我们可以使用 DOM API 已经提供的函数,但是 Kotlin 允许我们提高表达能力。为此,将以下代码添加到我们的扩展文件dom.kt(我不添加新的导入;按 Alt+Enter 让 Android Studio 帮你添加必要的导入):
fun prettyFormatXml(document:Document): String {
val format = OutputFormat(document).apply { lineWidth = 65
indenting = true
indent = 2
}
val out = StringWriter()
val serializer = XMLSerializer(out, format)
serializer.serialize(document)
return out.toString()
}
fun prettyFormatXml(unformattedXml: String) =
prettyFormatXml(parseXmlToDOM(unformattedXml))
fun Node.toXmlString():String {
val transformerFact = TransformerFactory.newInstance()
val transformer = transformerFact.newTransformer()
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
val source = DOMSource(this)
val writer = StringWriter()
val result = StreamResult(writer)
transformer.transform(source, result)
return writer.toString()
}
operator fun Node.plusAssign(child:Node) {
this.appendChild(child)
}
fun Node.addText(s:String): Node {
val doc = ownerDocument
val txt = doc.createTextNode(s)
appendChild(txt)
return this
}
fun Node.removeText() {
if(hasChildNodes() && firstChild.nodeType == Node.TEXT_NODE)
removeChild(firstChild)
}
fun Node.updateText(s:String) : Node { removeText()
return addText(s)
}
fun Node.addAttribute(name:String, value:String): Node {
(this as Element).setAttribute(name, value)
return this
}
fun Node.removeAttribute(name:String) {
this.attributes.removeNamedItem(name)
}
这是我们在这种情况下的描述
-
功能
prettyFormatXml( document: Document )和prettyFormatXml( unformattedXml: String )是主要用于诊断目的的实用功能。给定一个Document或者一个无格式的 XML 字符串,它们创建一个漂亮的字符串。 -
扩展函数
Node.toXmlString()从当前节点开始创建 XML 子树的字符串表示。如果对Document这样做,整个 DOM 结构都将被转换。 -
我们重载
Node的plusAssign操作符(对应于+=)来添加一个子节点。 -
我们为
Node添加了一个addText()扩展,用于向节点添加文本内容。 -
我们为
Node添加了一个removeText()扩展,用于从节点中删除文本内容。 -
我们为
Node添加了一个updateText()扩展,用于改变节点的文本内容。 -
我们为
Node添加了一个addAttribute()扩展,用于向节点添加属性。 -
我们为
Node添加了一个removeAttribute()扩展,用于从节点中删除属性。 -
我们为
Node添加了一个updateAttribute()扩展,用于改变节点的属性。
例如,这些函数的用例包括以下代码片段。首先,我们向给定的节点添加一个元素加属性:
val xml = """<?xml version="1.0" encoding="UTF-8"?>
<ProbeMsg>
<TimeStamp>2016-10-30T19:07:07Z</TimeStamp>
<ProbeId>1A6G</ProbeId>
<ProbeValue ScaleUnit="cm">37.4</ProbeValue>
<Meta>
<Generator>045</Generator>
<Priority>-3</Priority>
<Actor>P. Rosengaard</Actor>
</Meta>
</ProbeMsg>"""
try {
val dom = parseXmlToDOM(xml)
val msg = dom("ProbeMsg")
val meta = msg("Meta")
// Add a new element to "meta".
meta += dom.createElement("NewMeta").
addText("NewValue").
addAttribute("SomeAttr", "AttrVal")
Log.d("LOG", "\n\n" + prettyFormatXml(dom))
}catch(e:Exception) { Log.e("LOG", "XML Error", e)
}
为此,我们还使用了来自Document类的createElement()函数。最后,这段代码将修改后的 XML 写入日志控制台。
以下代码示例解释了如何更改和移除属性和元素:
val xml = """<?xml version="1.0" encoding="UTF-8"?>
<ProbeMsg>
<TimeStamp>2016-10-30T19:07:07Z</TimeStamp>
<ProbeId>1A6G</ProbeId>
<ProbeValue ScaleUnit="cm">37.4</ProbeValue>
<Meta>
<Generator>045</Generator>
<Priority>-3</Priority>
<Actor>P. Rosengaard</Actor>
</Meta>
</ProbeMsg>"""
try {
val dom = parseXmlToDOM(xml)
val msg = dom("ProbeMsg")
val ts = msg("TimeStamp")
val probeValue = msg("ProbeValue")
// Update an attribute and the text contents of
// an element.
probeValue.updateAttribute("ScaleUnit", "dm")
ts.updateText("1970-01-01T00:00:00Z")
Log.d("LOG", "\n\n" + prettyFormatXml(dom))
// Remove an attribute
probeValue.removeAttribute("ScaleUnit")
Log.d("LOG", "\n\n" + prettyFormatXml(dom))
// Removing a node means removing it from
// its parent node.
msg.removeChild(probeValue)
Log.d("LOG", "\n\n" + prettyFormatXml(dom))
}catch(e:Exception) {
Log.e("LOG", "XML Error", e)
}
创建新的 DOM
如果您需要从头开始编写 XML 文档的 DOM 表示,首先创建一个Document实例。这个没有公共构造函数;相反,你应该写:
val doc = DocumentBuilderFactory.
newInstance().newDocumentBuilder().newDocument()
从这里,您可以像前面描述的那样添加元素。注意,要查看我们的prettyFormatXml()实用函数的任何输出,您必须向doc添加至少一个子元素。
练习 1
向dom.kt文件添加一个createXmlDocument()函数,以简化文档创建。
JSON 处理
JavaScript 对象表示法(JSON)是 XML 的小姐妹。与使用 XML 格式的数据相比,用 JSON 格式编写的数据需要更少的空间。此外,JSON 数据几乎自然地映射到浏览器环境中的 JavaScript 对象,因此 JSON 在最近几年获得了相当大的关注。
Kotlin 的标准库不知道如何处理 JSON 数据,所以,类似于 XML 处理,我们添加一个合适的外部库。从几种可能性来看,我们使用广泛采用的杰克逊图书馆。要将它添加到 Android 项目中,在模块的build.gradle文件中的dependencies部分添加
implementation
'com.fasterxml.jackson.core:jackson-core:2.9.8'
implementation
'com.fasterxml.jackson.core:jackson-databind:2.9.8'
(在两行上,删除换行符)。
JSON 处理有几种范例。最常用的是带有特定于 JSON 的对象的树状结构,以及带有各种半自动转换机制的 Kotlin 和 JSON 对象之间的映射。我们将映射方法留给您进一步的研究;它包含几个非常复杂的特性,主要是 JSON 集合映射。杰克逊的主页给了你更多的信息。相反,我们描述处理 JSON 数据的内存树表示的机制。
对于本节的其余部分,我们使用以下 JSON 数据来解释示例中使用的函数:
val json = """{
"id":27,
"name":"Roger Rabbit",
"permanent":true,
"address":{
"street":"El Camino Real",
"city":"New York",
"zipcode":95014
},
"phoneNumbers":[9945678, 123456781],
"role":"President"
}"""
JSON 助手函数
用于 JSON 处理的 Jackson 库包含了编写、更新和删除 JSON 元素所需的全部内容。这个库非常广泛,包含了大量的类和函数。为了简化开发并包含 Kotlin 好东西,我们使用了一些包级函数和扩展函数来提高 JSON 代码的可读性。这些最好放在某个包com.whatever.ext中的 Kotlin 文件json.kt中。
我们从导入开始,添加一个invoke操作符,这样我们可以很容易地从一个节点获取一个子节点,并添加一个remove和一个forEach函数,用于删除一个节点并遍历节点的子节点:
import com.fasterxml.jackson.core.JsonFactory
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.*
import java.io.ByteArrayOutputStream
import java.math.BigInteger
operator fun JsonNode.invoke(s:String) = this.get(s)
operator fun JsonNode.invoke(vararg s:String) =
s.fold(this, { acc, s -> acc(s) })
fun JsonNode.remove(name:String) {
val on = (this as? ObjectNode)?:
throw Exception("This is not an object node")
on.remove(name) }
fun JsonNode.forEach(iter: (JsonNode) -> Unit ) {
when(this) {
is ArrayNode -> this.forEach(iter)
is ObjectNode -> this.forEach(iter)
else -> throw Exception("Cannot iterate over " +
this::class)
}
}
接下来,我们为asText()添加简单的别名text(),以简化文本提取:
fun JsonNode.text() = this.asText()
另一个迭代器遍历对象节点的子节点。这一次,我们还会考虑孩子们的名字:
fun JsonNode.forEach(iter: (String, JsonNode) -> Unit ) {
if(this !is ObjectNode)
throw Exception(
"Cannot iterate (key,val) over " + this::class)
this.fields().forEach{
(name, value) -> iter(name, value) }
}
为了编写一个对象节点的子节点,我们定义了一个put()函数,因此我们可以编写node.put( "childName", 42 ):
// Works only if the node is an ObjectNode!
fun JsonNode.put(name:String, value:Any?) : JsonNode {
if(this !is ObjectNode)
throw Exception("Cannot put() on none-object node")
when(value) {
null -> this.putNull(name)
is Int -> this.put(name, value)
is Long -> this.put(name, value)
is Short -> this.put(name, value)
is Float -> this.put(name, value)
is Double -> this.put(name, value)
is Boolean -> this.put(name, value)
is String -> this.put(name, value)
is JsonNode -> this.put(name, value)
else -> throw Exception(
"Illegal value type: ${value::class}")
}
return this
}
为了向数组对象追加一个值,我们定义了一个add()函数,它适用于各种类型:
// Add a value to an array, works only if this is an
// ArrayNode
fun JsonNode.add(value:Any?) : JsonNode {
if(this !is ArrayNode)
throw Exception("Cannot add() on none-array node")
when(value) {
null -> this.addNull()
is Int -> this.add(value)
is Long -> this.add(value)
is Float -> this.add(value)
is Double -> this.add(value)
is Boolean -> this.add(value)
is String -> this.add(value)
is JsonNode -> this.add(value)
else -> throw Exception(
"Illegal value type: ${value::class}")
}
return this
}
对于 JSON 对象创建,我们定义了各种createSomething()样式的函数,并且我们还添加了几个类似 Kotlin 的构建函数:
// Node creators
fun createJsonTextNode(text:String) = TextNode.valueOf(text)
fun createJsonIntNode(i:Int) = IntNode.valueOf(i)
fun createJsonLongNode(l:Long) = LongNode.valueOf(l)
fun createJsonShortNode(s:Short) = ShortNode.valueOf(s)
fun createJsonFloatNode(f:Float) = FloatNode.valueOf(f)
fun createJsonDoubleNode(d:Double) = DoubleNode.valueOf(d)
fun createJsonBooleanNode(b:Boolean) = BooleanNode.valueOf(b)
fun createJsonBigIntegerNode(b: BigInteger) = BigIntegerNode.valueOf(b)
fun createJsonNullNode() = NullNode.instance
fun jsonObjectNodeOf(
children: Map<String,JsonNode> = HashMap()) :
ObjectNode {
return ObjectNode(JsonNodeFactory.instance, children)
}
fun jsonObjectNodeOf(
vararg children: Pair<String,Any?>) :
ObjectNode {
return children.fold(
ObjectNode(JsonNodeFactory.instance), { acc, v ->
acc.put(v.first, v.second)
acc
})
}
fun jsonArrayNodeOf(elements: Array<JsonNode> =
emptyArray()) : ArrayNode {
return ArrayNode(JsonNodeFactory.instance,
elements.asList())
}
fun jsonArrayNodeOf(elements: List<JsonNode> =
emptyList()) : ArrayNode {
return ArrayNode(JsonNodeFactory.instance,
elements)
}
fun jsonEmptyArrayNode() : ArrayNode {
return ArrayNode(JsonNodeFactory.instance)
}
fun jsonArrayNodeOf(vararg elements: Any?) : ArrayNode {
return elements.fold(
ArrayNode(JsonNodeFactory.instance), { acc, v ->
acc.add(v)
acc
})
}
扩展函数toPrettyString()和toJsonString()可以用来生成任何 JSON 节点的字符串表示:
// JSON output as pretty string
fun JsonNode.toPrettyString(
prettyPrinter:PrettyPrinter? =
DefaultPrettyPrinter()) : String {
var res:String? = null
ByteArrayOutputStream().use { os ->
val gen = JsonFactory().createGenerator(os).apply {
if(prettyPrinter != null) this.prettyPrinter = prettyPrinter
}
val mapper = ObjectMapper()
mapper.writeTree(gen, this)
res = String(os.toByteArray())
}
return res!!
}
// JSON output as simple string
fun JsonNode.toJsonString() : String =
toPrettyString(prettyPrinter = null)
所有这些扩展函数的主要思想是通过向基本节点类JsonNode添加 JSON 对象相关和 JSON 数组相关的函数来提高简洁性,并在运行时执行类强制转换。虽然它使 JSON 代码更小,更有表现力,但运行时出现异常的风险也增加了。
读取和写入 JSON 数据
要读入 JSON 数据,您只需编写:
val json = ... // see section beginning
val mapper = ObjectMapper()
val root = mapper.readTree(json)
从这里我们可以研究 JSON 元素,遍历并获取 JSON 对象成员,并提取 JSON 数组元素:
try {
val json = ... // see section beginning
val mapper = ObjectMapper()
val root = mapper.readTree(json)
// see what we got
Log.d("LOG", root.toPrettyString())
// type of the node
Log.d("LOG", root.nodeType.toString())
// <- OBJECT
// is it a container?
Log.d("LOG", root.isContainerNode.toString())
// <- true
root.forEach { k,v ->
Log.d("LOG",
"Key:${k} -> val:${v} (${v.nodeType})")
Log.d("LOG",
" <- " + v::class.toString())
}
val phones = root("phoneNumbers")
phones.forEach { ph ->
Log.d("LOG", "Phone: " + ph.text())
}
Log.d("LOG", "Phone[0]: " + phones[0].text())
val street = root("address")("street").text()
Log.d("LOG", "Street: " + street)
Log.d("LOG", "Zip: " + root(“address”, “zipcode”).asInt())
}catch(e:Exception) {
Log.e("LOG", "JSON error", e)
}
以下代码片段展示了如何通过添加、更改或删除节点或 JSON 对象成员来改变 JSON 树。
// add it to the "try" statements from the
// last listing
// remove an entry
root("address").remove("zipcode")
Log.d("LOG", root.toPrettyString())
// update an entry
root("address").put("street", "Fake Street 42")
Log.d("LOG", root.toPrettyString())
root("address").put("country", createJsonTextNode("Argentina"))
Log.d("LOG", root.toPrettyString())
// create a new object node
root.put("obj", jsonObjectNodeOf(
"abc1" to 23,
"abc2" to "Hallo",
"someNull" to null
))
Log.d("LOG", root.toPrettyString())
// create a new array node
root.put("arr", jsonArrayNodeOf(
23,
null,
"Hallo"
))
Log.d("LOG", root.toPrettyString())
// write without spaces or line breaks
Log.d("LOG", root.toJsonString())
创建新的 JSON 树
要在内存中创建新的 JSON 树,可以使用:
val root = jsonObjectNodeOf()
从那里,您可以像前面描述的那样添加 JSON 元素。
练习 2
创建一个 JSON 文档,对应于:
{
"firstName": "Arthur",
"lastName": "Doyle",
"dateOfBirth": "03/04/1997",
"address": {
"streetAddress": "21 3rd Street",
"city": "New York",
"state": "NY",
"postalCode": "10021-1234"
},
"phoneNumbers": [
{
"type": "home",
"number": "212 555-1234"
},
{
"type": "mobile",
"number": "123 456-7890"
}
],
"children": [],
"spouse": null
}