十、开发

与前几章相比,本章涉及的问题更接近于开发问题。我们将在这里讨论的主题与特定的 Android 操作系统 API 没有太紧密的联系。我们更关心的是如何使用 Kotlin 方法最好地完成技术需求。

这一章还有一节介绍了如何将 Kotlin 代码转换成可以服务于WebView小部件的 JavaScript 代码。

用 Kotlin 编写可重用库

你在网上找到的大多数教程都是关于活动、服务、广播接收器和内容供应器的。这些组件是可重用的,因为您可以或多或少地从一个项目中提取它们,并将它们复制到另一个项目中。Android 操作系统中的封装已经达到了一个复杂的阶段,这使得重用成为可能。然而,在较低的层面上,在某些情况下,Android 中提供的库或 API 可能不适合您的所有需求,因此您可能会尝试自己开发这样的库,然后在可行的情况下将源代码从一个项目复制到另一个项目。

当然,这种在源代码层面上的复制并不适合可重用库的现代方法;只需考虑引入大量样板工作的维护和版本控制问题。最好的事情是将这样的可重用库设计为专用的开发工件。然后,它们可以很容易地从不同的项目中重用。

在接下来的部分中,我们将开发一个基本的正则表达式库,作为您自己的库项目的概念基础。

启动库模块

库项目是包含一个或多个模块的项目。在 Android Studio 打开的情况下,创建一个新项目,并确保它启用了 Kotlin 支持。然后,在新项目中进入新➤新模块,选择 Android 库。

注意

Android 库不仅仅是类的集合。它还可能包含资源和配置文件。出于我们的目的,我们将只看类。从开发的角度来看,这些额外的可能性没有坏处,你可以忽略它们。然而,对于使用 Android 库类型的项目,与只使用 JAR 文件相比,这为将来的扩展提供了更多的可能性。

创建库

在库模块中,创建一个新的 Kotlin 类,并在其中编写以下内容:

package com.example.regularexpressionlib

infix operator fun String.div(re:String) :
      Array<MatchResult> =
  Regex(re).findAll(this).toList().toTypedArray()

infix operator fun String.rem(re:String) :
      MatchResult? =
  Regex(re).findAll(this).toList().firstOrNull()

operator fun MatchResult.get(i:Int) =
  this.groupValues[i]

fun String.onMatch(re:String, func: (String)-> Unit)
      : Boolean =
this.matches(Regex(re)).also { if(it) func(this) }

这四个操作符和函数的作用不亚于允许我们编写searchString/regExpString来搜索正则表达式匹配和searchString % regExpString来搜索第一个匹配。此外,我们可以使用searchString.onMatch()让一些块只有在匹配时才执行。

这个列表不同于我们在本书中看到的所有列表。首先,你可以看到我们这里没有任何类。这是可能的,因为 Kotlin 知道文件工件的概念。在幕后,它根据包名生成一个隐藏的类。通过import com.example.regularexpressionlib.*导入的库的任何客户端都可以像在 Java 中执行所有这些函数的静态导入一样工作。

infix operator fun String.div( re:String )定义字符串的除法运算符。这样的划分在标准中是不可能的,所以与 Kotlin 内置运算符没有冲突。它使用 Kotlin 库中的Regex类来查找搜索字符串中正则表达式的所有匹配项,并将其转换为数组,因此我们可以稍后使用[ ]操作符通过索引来访问结果。infix operator fun String.rem( re:String )做了几乎相同的事情,但是它为字符串定义了%操作符,执行正则表达式搜索,并且只获取第一个结果,如果没有结果,则返回null

operator fun MatchResult.get(i:Int) = ...是前面运算符返回的MatchResult的扩展。它允许通过索引访问匹配的组。比方说,如果你搜索 (el)【内侧" Hello Nelo" ,你可以写("Hello Nelo" / "(e.)") [0][1]来获得第一组的第一场比赛,在这种情况下 el 来自 Hello

测试库

我们需要一种在开发库的同时测试它的方法。不幸的是,Android Studio 3.0 不允许类似于main()的功能。我们唯一能做的就是创建一个单元测试,对于我们的例子,这样的单元测试可以如下所示:

import org.junit.Assert.*
import org.junit.Test
...

class RegularExpressionTest {
  @Test
  fun proof_of_concept() {
      assertEquals(1, ("Hello Nelo" / ".*el.*").size)
      assertEquals(2, ("Hello Nelo" / ".*?el.*?").size)

      var s1:String = ""
      ("Hello Nelo" / "e[lX]").forEach {
          s1 += it.groupValues
      }
      assertEquals("[el][el]", s1)

      var s2: String = ""
      ("Hello Nelo" / ".*el.*").firstOrNull()?.run {
          s2 += this[0]
      }
      assertEquals("Hello Nelo", s2)
      assertEquals("el",
            ("Hello Nelo" % ".*(el).*")?.let{ it[1] } )

      assertEquals("el",
            ("Hello Nelo" / ".*(el).*")[0][1])

      var match1: Boolean = false
      "Hello".onMatch(".*el.*") {
         match1 = true
      }
      assertTrue(match1)
  }
}

然后,您可以使用 Android Studio 的上下文菜单像运行任何其他单元测试一样运行这个测试。注意,在开发的早期阶段,您可以向测试添加println()语句,以便在测试运行时在测试控制台上打印一些信息。

使用图书馆

一旦你调用构建➤重建项目,你可以在模块的这个文件夹中找到 Android 库。

build/outputs/aar

要从客户端使用它,请通过“新建➤新模块”在客户端项目中创建新模块,然后选择“导入”。JAR/。AAR 包,并导航到库项目生成的.aar文件。

警告

该程序复制.aar文件。如果您有一个新版本的库,您可以删除客户端项目中的库项目并再次导入它,或者手动将.aar文件从库项目复制到客户端项目。

要在客户机中使用这个库,您只需将import com.example.regularexpressionlib.*添加到头部,此后您就可以应用新的匹配结构,如前面的测试所示。

发布图书馆

到目前为止,我们一直在本地使用这个库,这意味着您在开发机器上的某个地方有这个库项目,并且可以从同一台机器上的其他项目中使用它。您还可以发布库,这意味着如果您手头有一个企业存储库,就可以让企业环境中的其他开发人员使用它们,或者让它们对您想要提供给社区的库真正公开。

不幸的是,发布库的过程相当复杂,包括在几个地方修改构建文件,以及使用第三方插件和存储库网站。这使得发布库的过程变得复杂而脆弱,当您阅读本书时,对一个可能的发布过程的详细描述可能很容易过时。因此,我要求你自己做研究。在你最喜欢的搜索引擎中输入publishing Android libraries,你会很容易地找到对你有帮助的在线资源。如果您发现几个过程可能适合您的需要,一般的经验法则是使用一个有大的支持社区并且尽可能简单的过程。

此外,对于公司项目,如果您想使用公共存储库,请确保您有权限使用它们。如果您不能使用公共存储库,安装一个公司存储库并不是一个过于复杂的任务。要建立公司的 Maven 资源库,您可以使用软件套件 Artifactory。

使用 Kotlin 的高级监听器

无论你在为 Android 开发什么样的应用,在某个地方或者更常见的地方,你都必须为 API 函数调用提供监听器。在 Java 中,您必须创建实现监听器接口的类或匿名内部类,而在 Kotlin 中,您可以更优雅地做到这一点。

如果你有一个单一的抽象方法(SAM)类或接口,这很容易。例如,如果你想给一个按钮添加一个点击监听器,这意味着你必须提供一个接口View.OnClickListener的实现,用 Java 的方式来做就是这样的:

btn.setOnClickListener(object : View.OnClickListener {
    override fun onClick(v: View?) {
        // do s.th.
    }
})

然而,由于这个接口只有一个方法,您可以更简洁地编写它,就像这样:

btn.setOnClickListener {
  // do s.th.
}

你可以让编译器找出接口方法应该如何实现。

如果侦听器不是 SAM,这意味着如果它有不止一个方法,这种简短的符号就不再可能。例如,如果您有一个EditText视图,并且想要添加一个文本更改监听器,那么即使您只对onTextChanged()回调方法感兴趣,您也必须编写下面的代码。

val et = ... // the EditText view
et.addTextChangedListener( object : TextWatcher {
    override fun afterTextChanged(s: Editable?) {
        // ...
    }
    override fun beforeTextChanged(s: CharSequence?,
          start: Int, count: Int, after: Int) {
        // ...
    }
    override fun onTextChanged(s: CharSequence?,
          start: Int, before: Int, count: Int) {
        // ...
    }
})

然而,您可以做的是在一个实用程序文件中扩展EditText类,并增加提供一个简化的文本更改监听器的可能性。为此,从这样一个文件开始,例如,com.example包内的utility.kt,当然也可以是你的应用的任何包。添加以下内容:

fun EditText.addTextChangedListener(l:
      (CharSequence?, Int, Int, Int) -> Unit) {
  this.addTextChangedListener(object : TextWatcher {
      override fun afterTextChanged(s: Editable?) {
      }
      override fun beforeTextChanged(s: CharSequence?,
            start: Int, count: Int, after: Int) {
      }
      override fun onTextChanged(s: CharSequence?,
            start: Int, before: Int, count: Int) {
          l(s, start, before, count)
      }
  })
}

这将所需的方法动态添加到该类中。

现在,您可以在任何需要的地方使用import com.example.utility.*,然后编写下面的代码,与最初的构造相比,它看起来要简洁得多:

val et = ... // the EditText view
et.addTextChangedListener({ s: CharSequence?,
      start: Int, before: Int, count: Int ->
  // do s.th.
})

多线程操作

我们已经在第 9 章中讨论了多线程。在这一节中,我们只是指出 Kotlin 在语言层面上可以简化多线程。

Kotlin 在其标准库中包含了几个实用函数。与使用 Java API 相比,它们有助于更容易地启动线程和计时器;见表 10-1

表 10-1

科特林并发

|

名字

|

因素

|

返回

|

描述

| | --- | --- | --- | --- | | fixedRate-Timer | name: String?``daemon: Boolean``initialDelay: Long``period: Long``action: TimerTask.() -> Unit | Timer | 为固定速率调度创建并启动计时器对象。periodinitialDelay参数以毫秒为单位。 | | fixedRate-Timer | name: String?``daemon: Boolean``startAt: Date``period: Long``action: TimerTask.() -> Unit | Timer | 为固定速率调度创建并启动计时器对象。period 参数以毫秒为单位。 | | timer | name: String?``daemon: Boolean``initialDelay: Long``period: Long``action: TimerTask.() -> Unit | Timer | 为固定速率调度创建并启动计时器对象。period 参数是上一个任务结束和下一个任务开始之间的时间,以毫秒为单位。 | | timer | name: String?``daemon: Boolean``startAt: Date``period: Long``action: TimerTask.() -> Unit | Timer | 为固定速率调度创建并启动计时器对象。period 参数是上一个任务结束和下一个任务开始之间的时间,以毫秒为单位。 | | thread | start: Boolean``isDaemon: Boolean``contextClassLoader: ClassLoader?``name: String?``priority: Int``block: () -> Unit | Thread | 创建并可能启动一个线程,执行它的block。优先级较高的线程优先于优先级较低的线程执行。 |

对于定时器函数,action参数是一个闭包,其中this是对应的TimerTask对象。例如,使用它,你可以从它的执行块中取消定时器。将daemonisDaemon设置为true的线程或定时器不会阻止 JVM 在所有非守护线程退出后关闭。

凭借其通用的功能,Kotlin 在帮助我们处理并发性方面做得很好;在java.util.concurrent中,许多处理并行执行的类都将RunnableCallable作为参数,在 Kotlin 中,你总是可以通过直接的{ ... } lambda 构造来替换这样的 SAM 构造。这里有一个例子:

val es = Executors.newFixedThreadPool(10)
// ...
val future = es.submit({
      Thread.sleep(2000L)
      println("executor over")
      10
  } as ()->Int)
val res:Int = future.get()

因此,您不必像在 Java 中那样编写以下代码:

ExecutorService es = Executors.newFixedThreadPool(10);
// ...
Callable<Integer> c = new Callable<>() {
    public Integer call() {
      try {
        Thread.sleep(2000L);
      } catch(InterruptedException e { }
      System.out.println("executor over");
      return 10;
    };
Future<Integer> f = es.submit(c);
int res = f.get();

注意,在 Kotlin 代码中,()->Int的造型是必要的,即使 Android Studio 抱怨这是多余的。这样做的原因是,如果我们不这样做,另一个带有参数Runnable的方法会被调用,而执行器无法返回值。

兼容性库

框架 API 和兼容性库之间有一个重要的区别,但在开始时并不容易理解。如果你开始开发 Android 应用,你会经常看到相同名称的类出现在不同的包中。或者更糟的是,您会看到不同包中不同名称的类似乎在做同样的事情。

我们来看一个突出的例子。要创建活动,您可以子类化android.app.Activity或者子类化android.support.v7.app.AppCompatActivity。看看你在网上找到的例子和教程,在用法上似乎没有明显的区别。实际上,AppCompatActivity继承了Activity,所以哪里需要Activity,就可以用AppCompatActivity代替,它就会编译。那么,功能上有区别吗?看情况。如果你查看文档或代码,你会发现AppCompatActivity允许添加android.support.v7.app.ActionBar,而android.app.Activity不允许。相反,android.app.Activity允许添加android.app.ActionBar。而且这次android.support.v7.app.ActionBar没有继承android.app.ActionBar,所以不能把android.support.v7.app.ActionBar加到android.app.Activity上。

这基本上是说,如果你偏爱android.support.v7.app.ActionBar胜过android.app.ActionBar,你必须为一个活动使用AppCompatActivity。为什么要用android.support.v7.app.ActionBar而不是android.app.ActionBar?答案很简单:后者相当古老;从 API 级开始就有了。较新版本的android.app.ActionBar不能破坏 API 以保持与旧设备的兼容性。但是android.support.v7.app.ActionBar可以增加新的功能;它要新得多,从 API 级就存在了。

魔法现在是这样工作的:如果你使用一个说 API 等级 24 或更高的设备,你可以使用android.support.v7.app.AppCompatActivity并添加android.support.v7.app.ActionBar。你也可以使用android.app.Activity,但是你不能添加android.support.v7.app.ActionBar,而是必须使用android.app.ActionBar。因此,对于新设备,如果支持库动作栏比框架动作栏更适合您的需求,那么使用android.support.v7.app.AppCompatActivity进行活动是有意义的。

老设备呢?您仍然可以使用android.support.v7.app.AppCompatActivity,因为它是作为添加到应用中的库提供的。因此,你也可以使用现代的android.support.v7.app.ActionBar作为一个动作条,并且比老式的android.app.ActionBar拥有更多的功能。这实际上是魔术的过程。通过使用支持库,即使旧设备也可以利用后来添加的新功能!support 类的实现在内部检查设备版本,并提供合理的回退功能,以尽可能地模仿现代设备。

需要注意的是,作为开发人员,您必须同时生活在两个世界中。如果没有其他选择,您必须显式或隐式地使用框架类,并且如果您希望确保与旧设备的最大兼容性,您必须考虑使用支持库类(如果可用的话)。因此,在使用一个类之前,检查是否也有匹配的支持库类是至关重要的。你可能不喜欢 Android 中使用的这种两个世界的方法,这也意味着构建应用需要更多的思考,但这就是 Android 处理向后兼容性的方式。

如果你在你最喜欢的搜索引擎中输入 android 支持库,你将很容易找到关于支持库的详细信息。

支持库与您的应用捆绑在一起,因此它们必须在构建文件中声明为依赖项。如果你在 Android Studio 中启动一个新项目,默认情况下,它会写入模块的build.gradle文件。

dependencies {
 ...
  implementation 'com.android.support:appcompat-v7
 :26.1.0'
  implementation 'com.android.support.constraint:
   constraint-layout:1.0.2'
...
}

您可以看到默认情况下支持库版本 7 是可用的,因此您可以从一开始就使用它。

科特林最佳实践

开发不仅仅是解决与 IT 相关的问题或实现需求;你也想写出“好”的软件。然而,“好”在这个上下文中的确切含义有点模糊。很多方面在这里起作用:开发快,执行性能高,程序短,程序可读,程序稳定性高,等等。所有这些都有其优点,夸大其中任何一个都会阻碍其他方面。

事实上,你应该把它们都记在心里,但是我的经验告诉我要把重点放在以下几个方面:

  • 让程序变得全面(或有表现力)。一个没有人理解的超级优雅的解决方案可能会让你高兴,但是请记住,以后可能其他人需要理解你的软件。

  • 保持程序简单。过于复杂的解决方案容易出现不稳定性。当然,你不会某天早上醒来说,“好,今天我要写一个简单程序来解决 XYZ 需求。”编写能够可靠解决问题的简单程序是一个经验问题,需要多年的实践。但是你可以在编写简单的程序时不断尝试变得更好。一个好的起点是经常问自己,“难道不应该有一个更简单的解决方案吗?”对于软件的任何部分,在某些情况下,通过查看 API 文档和编程语言参考,您会发现更容易的解决方案与您当前拥有的解决方案一样。

  • 不要重复自己。这个原则,通常被称为干编程,怎么强调都不为过。每当你发现自己在使用 Ctrl+C 和 Ctrl+V 来复制一些程序段落时,可以考虑使用一个函数或一个 lambda 表达式来提供一个完成事情的地方。

  • 做预期的事情。你可以在 Kotlin 中覆盖类方法和操作符,你可以动态地添加函数到现有的类中,甚至是像String这样的基本类。在任何情况下,通过查看它们的名字来确保这样的扩展如预期的那样工作,因为如果它们不工作,程序就很难理解。

Kotlin 在所有这些方面都有所帮助,并且经常比古老的 Java 做得更好。在接下来的部分中,我们指出了几个 Kotlin 概念,你可以用它们来使你的程序变得简短、简单和有表现力。请注意,这些概念的总和远远不是 Kotlin 的完整文档。因此,要了解更多细节,请参阅在线文档。

函数式编程

虽然函数式编程作为一种开发范式在版本 8 中进入了 Java,但 Kotlin 从一开始就支持函数式编程风格。在函数式编程中,你更喜欢不变的值而不是变量,避免状态机,并允许函数作为函数的参数。另外,lambda 演算允许传递没有名字的函数。科特林为我们提供了这一切。

在 Java 中,你可以用final修饰符来表示一个变量在第一次初始化后不会被改变。虽然大多数 Java 开发人员使用final修饰符作为常量;我很少看到开发人员在编码中使用它们。

public class Constants {
  public final static int CALCULATION_PRECISION = 10;
  public final static int MAX_ITERATIONS = 1000;
  ...
}

这是一个遗憾,因为它提高了可读性和稳定性。为了节省几个按键而省略它的诱惑实在太大了。科特林的故事是不同的;您使用val来表示数据对象在其生命周期内保持不变,如果您需要一个实变量,则使用var,如下所示:

fun getMaxFactorial():Int = 13
fun fact(n:Int):Int {
    val maxFactorial = getMaxFactorial()
    if(n > maxFactorial)
       throw RuntimeException("Too big")
    var x = 1
    for( i in 1.. (n) ) {
         x *= i
    }
    return x
}
val x = fact(12)
System.out.println("12! = ${x}")

这个简短的代码片段使用maxFactorial作为val,意思是“这是不可更改的。”然而,x是一个var,它在初始化后被改变。

我们甚至可以在阶乘计算的代码片段中避免使用var x,用一个函数构造来代替它。这是另一个函数命令:比起一个语句或一串语句,更喜欢表达式。为此,我们使用递归并编写以下代码:

fun fact(n:Int):Int = if(n>getMaxFactorial())
  throw RuntimeException("Too big") else
  if(n > 1) n * fact(n-1) else 1
val x = fact(10)
System.out.println("10! = ${x}")

这个小阶乘计算器只是一个简短的例子。有了收藏,故事变得更加有趣。Kotlin 标准库包括许多函数构造,您可以使用它们来编写优雅的代码。为了让您对所有的可能性有所了解,我们再次重写阶乘计算器,并使用 collections 包中的fold函数。

fun fact(n:Int) = (1..n).fold(1, { acc,i -> acc * i })
System.out.println("10! = ${fact(10)}")

为了简单起见,我删除了范围检查;如果您愿意,可以将前面的if...检查添加到{...}中的 lambda 表达式。你看我们连一个val都没有留下;不过在内部,iacc被当作vals来处理。这甚至可以再缩短一步。因为我们使用的只是类型Int的“时间”功能,所以我们可以直接引用它并编写以下代码:

fun fact(n:Int) = (1..n).fold(1, Int::times)
System.out.println("10! = ${fact(10)}")

使用 collections 包中的其他函数构造,您可以对集合、列表和映射执行更有趣的转换。但是函数式编程也是将函数作为对象在代码中传递。在 Kotlin 中,您可以将功能分配给valsvars,如下所示:

val factEngine: (acc:Int,i:Int) -> Int =
      { acc,i -> acc * i }
fun fact(n:Int) = (1..n).fold(1, factEngine)
System.out.println("10! = ${fact(10)}")

或者如下,这甚至更短,因为科特林在某些情况下可以推断类型:

val factEngine = { acc:Int, i:Int -> acc * i }
fun fact(n:Int) = (1..n).fold(1, factEngine)
System.out.println("10! = ${fact(10)}")

在本书中,我们尽可能使用函数构造来提高全面性和简明性。

顶级函数和数据

虽然在 Java 世界中,拥有太多全局可用的函数和数据被认为是不好的风格,例如在一些实用程序类中使用静态范围的定义,但在 Kotlin 中,这已经经历了一次复兴,看起来也更加自然。这是因为您可以在任何类之外的文件中声明函数和变量/值。不过,要使用它们,您必须导入类似于import com.example.global.*中的元素,其中包com/example.global中的任意名称的文件不包含类,而只包含funvarval元素。

例如,在com/example/app/util中编写一个名为common.kt的文件,并在其中添加以下内容:

package com.example.app.util

val PI_SQUARED = Math.PI * Math.PI

fun logObj(o:Any?) =
      o?.let { "(" + o::class.toString() + ") " +
                o.toString() } ?: "<null>"

然后添加更多的实用函数和常量。要使用它们,请编写以下内容:

import com.example.app.util.*
...
val ps = PI_SQUARED
logObj(ps)

但是,您应该谨慎使用该功能;过度使用它很容易导致结构混乱。完全避免将可变变量放在这样的范围内!您可以也应该将实用函数和全局常量放在这样的全局文件中。

类别扩展

与 Java 语言不同,Kotlin 允许动态地向类中添加方法。为此,请编写以下内容:

fun TheClass.newFun(...){ ... }

操作符也是如此,它允许你创建像"Some Text" % "magic"(这是你的想象)这样的扩展到像String这样的普通类。您应该像这样实现这个特殊的扩展:

infix operator fun String.rem(s:String){ ... }

只要确保不要无意中覆盖现有的类方法和操作符。这使得你的程序不可读,因为它做了意想不到的事情。注意,像Double.times()这样的大多数标准操作符无论如何都不能被覆盖,因为它们在内部被标记为 final。

10-2 描述了你可以通过operator fun TheClass.<OPER- ATOR>定义的操作符。

表 10-2

科特林算子

|

标志

|

转化为

|

中缀

|

默认功能

| | --- | --- | --- | --- | | +a | a.unaryPlus() |   | 通常什么都不做。 | | -a | a.unaryMinus() |   | 对一个数字求反。 | | !a | a.not() |   | 对布尔表达式求反。 | | a++ | a.inc() |   | 增加一个数字。 | | a- - | a.dec() |   | 减少一个数字。 | | a + b | a.plus(b) | x | 加法。 | | a - b | a.minus(b) | x | 减法 | | a * b | a.times(b) | x | 乘法。 | | a / b | a.div(b) | x | 组织。 | | a % b | a.rem(b) | x | 除法后的余数。 | | a . . b | a.rangeTo(b) | x | 定义一个范围。 | | a in b | b.contains(a) | x | 密封检查。 | | a !in b | !b.contains(a) | x | 非包容检查。 | | a[i] | a.get(i) |   | 索引访问。 | | a[i,j,...] | a.get(i,j,...) |   | 索引访问,通常不使用。 | | a[i] = b | a.set(i,b) |   | 索引设置访问。 | | a[i,j,...] = b | a.set(i,j,...,b) |   | 索引设置访问,通常不使用。 | | a() | a.invoke() |   | 祈祷。 | | a(b) | a.invoke(b) |   | 祈祷。 | | a(b,c,...) | a.invoke(b,c,...) |   | 祈祷。 | | a += b | a.plusAssign(b) | x | 添加到a。不得返回值;而是必须修改this。 | | a -= b | a.minusAssign(b) | x | 从a中减去。不得返回值;而是必须修改this。 | | a *= b | a.timesAssign() | x | 乘以a。不得返回值;而是必须修改this。 | | a /= b | a.divAssign(b) | x | 将a除以b然后赋值。不得返回值;相反,你必须修改this。 | | a %= b | a.remAssign(b) | x | 将除法的余数除以b,然后赋值。不得返回值;而是必须修改this。 | | a == b | a?.equals(b) ?: (b === null) | x | 检查相等性。 | | a != b | !(a?.equals(b) ?: (b === null)) | x | 检查不平等。 | | a > b | a.compareTo(b) > 0 | x | 对比。 | | a < b | a.compareTo(b) < 0 | x | 对比。 | | a >= b | a.compareTo(b) >= 0 | x | 比较。 | | a <= b | a.compareTo(b) <= 0 | x | 对比。 |

要定义扩展,对于类型为Infix的表中的任何操作符,您需要编写以下代码:

infix operator fun TheClass.<OPERATOR>( ... ){ ... }

这里,函数参数是第二个和任何后续操作数,函数体内的this是指第一个操作数。对于非类型Infix的操作符,只需省略infix

为自己的类定义操作符当然是个好主意。通过操作符修改标准 Java 或 Kotlin 库类也可以提高代码的可读性。

命名参数

通过使用如下命名参数:

fun person(fName:String = "", lName:String = "",
      age:Int=0) {
   val p = Person().apply { ... }
   return p
}

你可以像这样打更有表现力的电话:

val p = person(age = 27, lName = "Smith")

使用参数名意味着您不必关心参数顺序,并且在许多情况下,您可以避免为各种参数组合重载构造函数。

范围函数

作用域函数允许你以一种不同于使用类和方法的方式来构建你的代码。例如,考虑以下代码:

val person = Person()
person.lastName = "Smith"
person.firstName = "John"
person.birthDay = "2011-01-23"
val company = Company("ACME")

虽然这是有效的代码,但重复的person.令人讨厌。况且前四行是在构造一个人,下一行和一个人无关。如果这能直观的表达出来就好了,也可以避免重复。这是 Kotlin 中的一个构造,内容如下:

val person = Person().apply {
  lastName = "Smith"
  firstName = "John"
  birthDay = "2011-01-23"
}
company = Company("ACME")

与原始代码相比,这看起来更有表现力。上面明明说构造一个人,用它做点什么,然后再做点别的。有五个这样的结构,尽管相似,但它们在含义和用法上不同:alsoapplyletrunwith。表 10-3 描述了它们。

表 10-3

范围函数

|

句法

|

什么是this

|

这是什么

|

返回

|

使用

| | --- | --- | --- | --- | --- | | a.also {``... } | this外部语境 | a | a | 用于一些横切关注点,例如添加日志记录。 | | a.apply {``... } | a | - | a | 用于后期构造对象成形。 | | a.let {``... } | this外部语境 | a | 最后一个表达式 | 用于转换。 | | a.run {``... } | a | - | 最后一个表达式 | 用一个对象做一些计算,只有副作用。为了 c 更清晰,不要使用它返回的内容。 | | with(a) {``... } | a | - | 最后一个表达式 | 对一个对象进行分组操作。为了更清楚,不要使用它返回的内容。 |

使用作用域函数极大地提高了代码的表达能力。我在这本书里经常用到它们。

可空性

Kotlin 在语言层面上解决了可空性问题,以避免烦人的NullPointerException抛出。对于任何变量或常量,默认情况下不允许赋值null值;您必须通过在末尾添加一个?来显式声明可空性,如下所示:

var name:String? = null

然后,编译器知道示例中的name可以是null,并采取各种预防措施来避免NullPointerException s。例如,您不能编写name.toUpperCase(),但您必须使用name?.toUpperCase()来代替,它仅在name不是null时进行大写,否则返回null本身。

使用我们之前描述的作用域函数,有一种优雅的方法可以避免像if( x != null ) { ... }这样的构造。您可以改为编写以下内容:

x?.run {
  ...
}

这样做是一样的,但是更有表现力;凭借?.,只有当x不是null时,才会执行run{}

elvis操作符?:也非常有用,因为它处理只有当接收变量是null时才需要计算表达式的情况,如下所示:

var x:String? = ...
...
var y:String = x ?: "default"

这和 Java 里的String y = (x != null) ? x : "default");是一样的。

数据类别

数据类是负责承载结构化数据的类。实际上,对数据类中的数据做一些事情通常是不必要的,或者至少是不重要的。

在 Kotlin 中声明数据类很容易;你所要做的就是写下以下内容:

data class Person(
       val fName:String,
       val lName:String,
       val age:Int)

或者,如果您想对某些参数使用默认值,请使用:

data class Person(
       val fName:String="",
       val lName:String,
       val age:Int=0)

这个简单的声明已经定义了一个构造函数、一个合适的用于比较的equals()方法、一个默认的toString()实现,以及成为析构的一部分的能力。要创建一个对象,您只需编写以下代码:

val pers = Person("John","Smith",37)

或者写一个更有表现力的版本,如下所示:

val pers = Person(fName="John", lName="Smith", age=37)

在这种情况下,如果参数声明了默认值,也可以省略参数。

这一点以及您还可以在函数中声明类和函数的事实,使得定义特定的复杂函数返回类型变得很容易,如下所示:

fun someFun() {
  ...
  data class Person(
       val fName:String,
       val lName:String,
       val age:Int)
  fun innerFun():Person = ...
  ...
  val p1:Person = innerFun()
  val fName1 = p1.fName
  ...

解构

析构声明允许你多重赋值或变量。假设您有一个数据类Person,如前一节所定义。然后,您可以编写以下内容:

val p:Person = ...
val (fName,lName,age) = p

这给了你三个不同的值。数据类的顺序是由类的成员声明的顺序定义的。一般来说,任何具有component1()component2()、...访问器可以参与析构,所以您也可以对自己的类使用析构。例如,这是默认情况下为映射条目指定的,因此您可以编写以下内容:

val m = mapOf( 1 to "John", 2 to "Greg", ... )
for( (k,v) in m) { ... }

这里,to是一个中缀操作符,它创建了一个Pair类,该类又定义了fun component1()fun component2()

作为析构声明的一个附加特性,可以对未使用的部分使用 _ 通配符,如下所示:

val p:Person = ...
val (fName,lName,_) = p

多行字符串文字

Java 中的多行字符串定义起来总是有点笨拙。

String s = "First line\n" +
    "Second line";

在 Kotlin 中,可以如下定义多行字符串文字:

val s = """
    First line
    Second Line"""

您甚至可以通过添加.trimIndent()来去掉前面的缩进空格,如下所示:

val s = """
    First line
    Second Line""".trimIndent()

这将删除每一行开头的前导换行符和公共空格。

内部函数和类

在 Kotlin 中,函数和类也可以在其他函数中声明,这进一步有助于构建代码。

fun someFun() {
  ...
  class InnerClass { ... }
  fun innerFun() = ...
  ...
}

这种内部构造的范围当然仅限于声明它们的函数。

字符串插值

在 Kotlin 中,您可以将值传递给字符串,如下所示:

val i = 7
val s = "And the value of 'i' is ${i}"

这是从 Groovy 语言借用的,您可以将它用于所有类型,因为所有类型都有一个toString()成员。唯一的要求是${}的内容计算为一个表达式,因此您甚至可以编写以下代码:

val i1 = 7
val i2 = 8
val s = "The sum is: ${i1+i2}"

或者使用方法调用和 lambda 函数编写更复杂的结构:

val s = "8 + 1 is: ${ { i: Int -> i + 1 }(8) }"

限定“这个”

如果this不是您想要的,而是您想要从外部上下文中引用this,那么在 Kotlin 中,您可以使用如下的@限定符:

class A {
    val b = 7
    init {
        val p = arrayOf(8,9).apply {
            this[0] += this@A.b
        }
        ...
    }
}

授权

Kotlin 允许轻松地遵循委托模式。这里有一个例子:

interface Printer {
    fun print()
}

class PrinterImpl(val x: Int) : Printer {
    override fun print() { print(x) }
}

class Derived(b: Printer) : Printer by b

在这里,类Derived是类型Printer的,并将其所有方法调用委托给b对象。所以,你可以这样写:

val pi = PrinterImpl(7)
Derived(pi).print()

您可以随意覆盖方法调用,因此您可以调整委托以使用新的功能。

class Derived(val b: Printer) : Printer by b {
    override fun print() {
         print("Printing:")
         b.print()
    }
}

重命名的导入

在某些情况下,导入的类可能会使用长名称,但是您经常使用它们,所以您希望它们有较短的名称。例如,假设您经常在代码中使用SimpleDateFormat类,但不想一直写完整的类名。为了帮助我们简化这一点,您可以引入导入别名并编写以下代码:

import java.text.SimpleDateFormat as SDF

此后,您可以使用SDF代替SimpleDateFormat,如下所示:

val dateStr = SDF("yyyy-MM-dd").format(Date())

但是,不要过度使用这个特性,因为否则你的开发伙伴需要记住太多的新名字,这会使你的代码难以阅读。

JavaScript 上的 Kotlin

如果你把 Android 和 Kotlin 放在一起听,很明显你会认为 Kotlin 是 Java 的替代品,解决了 Android 运行时和 Android APIs 的问题。但还有另一种可能性,虽然不那么明显,但却开启了有趣的可能性。如果只看 Kotlin,您会发现它可以创建在 Java 虚拟机上运行的字节码,或者在 Android 的情况下在有点像 Java 的 Dalvik 虚拟机上运行。或者它可以生成在浏览器中使用的 JavaScript。问题是,我们能在 Android 中也使用它吗?答案是肯定的,在接下来的部分中,我将向您展示如何做到这一点。

创建 JavaScript 模块

我们从包含 Kotlin 文件的 JavaScript 模块开始,这些文件被编译成 JavaScript 文件。当您启动一个新模块时,没有什么像 JavaScript 模块向导一样可用,但我们可以轻松地从一个标准的智能手机应用模块开始,并转换它以满足我们的需求。

在 Android Studio 项目中,选择新➤新模块,然后选择手机和平板电脑模块。给它一个像样的名字,暂时说kotlinjsSample。生成模块后,删除以下文件夹和文件,因为我们不需要它们:

src/test
src/androidTest
src/main/java
src/main/res
src/main/AndroidManifest.xml

注意

如果你想从 Android Studio 中移除,你必须首先将视图类型从 Android 切换到 Project。

相反,添加两个文件夹。

src/main/kotlinjs
src/main/web

现在将模块的build.gradle文件的内容替换如下:

buildscript {
  ext.kotlin_version = '1.2.31'
  repositories {
      mavenCentral()
  }
  dependencies {
      classpath "org.jetbrains.kotlin:" +
            "kotlin-gradle-plugin:$kotlin_version"
  }
}

apply plugin: 'kotlin2js'

sourceSets {
  main.kotlin.srcDirs += 'src/main/kotlinjs'
}
task prepareForExport(type: Jar) {
  baseName = project.name + '-all'
  from {
      configurations.compile.collect {
             it.isDirectory() ? it : zipTree(it) } +
      'src/main/web'
  }
  with jar
}

repositories {
  mavenCentral()
}

dependencies {
  implementation "org.jetbrains.kotlin:" +
        "kotlin-stdlib-js:$kotlin_version"
}

这个构建文件启用了 Kotlin ➤ JavaScript 编译器,并引入了一个新的导出任务。

你现在可以打开 Android Studio 窗口右侧的 Gradle 视图,在那里的others下,你会找到任务prepareForExport。要运行它,请双击它。之后,在build/libs里面你会发现一个新的文件kotlinjsSample-all.jar。这个文件代表 JavaScript 模块,供其他应用或模块使用。

src/main/kotlinjs中创建文件Main.kt,并向其中添加内容,如下所示:

import kotlin.browser.document

fun main(args: Array<String>) {
    val message = "Hello JavaScript!"
    document.getElementById("cont")!!.innerHTML = message
}

最后,我们将针对一个网站,所以我们需要第一个 HTML 页面。将它作为标准的登陆页面index.html,在src/main/web中创建它,并输入以下内容:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
    <title>Kotlin-JavaScript</title>
  </head>
  <body>
    <span id="cont"></span>
    <script type="text/javascript"
           src="kotlin.js"></script>
    <script type="text/javascript"
           src="kotlinjsSample.js"></script>
  </body>
</html>

再次执行任务prepareForExport,让模块输出工件反映我们刚才所做的更改。

使用 JavaScript 模块

要使用我们在上一节中构建的 JavaScript 模块,请在应用的build.gradle文件中添加几行代码,如下所示:

task syncKotlinJs(type: Copy) {
  from zipTree('../kotlinjsSample/build/libs/' +
                'kotlinjsSample-all.jar')
  into 'src/main/assets/kotlinjs'
}
preBuild.dependsOn(syncKotlinJs)

这将导入 JavaScript 模块的输出文件,并将其提取到应用的assets文件夹中。借助于dependsOn()声明,这个额外的构建任务会在正常构建过程中自动执行。

现在,在布局文件中放置一个WebView元素,可能如下所示:

<WebView
     android:id="@+id/wv"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
</WebView>

要用主活动的onCreate()回调中的网页填充视图,编写以下代码:

wv.webChromeClient = WebChromeClient()
wv.settings.javaScriptEnabled = true
wv.loadUrl("file:///android_asset/kotlinjs/index.html")

这将启用对WebView小部件的 JavaScript 支持,并从 JavaScript 模块加载主 HTML 页面。

作为扩展,您可能希望将网页中的 JavaScript 连接到应用中的 Kotlin 代码(而不是 JavaScript 模块)。这并不过分复杂;您只需添加以下内容:

class JsObject {
    @JavascriptInterface
    override fun toString(): String {
      return "Hi from injectedObject"
    }
}
wv.addJavascriptInterface(JsObject(), "injectedObject")

此后,您可以使用 JavaScript 模块中的injectedObject,如下所示:

val message = "Hello JavaScript! injected=" +
      window["injectedObject"]

使用这些技术,你可以使用 HTML、CSS、Kotlin 转换成 JavaScript,以及一些访问器对象来处理 Android APIs,从而设计出完整的应用。