在大型团队中采用 Kotlin

改用任何新语言都是一项艰巨的任务。成功的秘诀在于缓慢起步、循序渐进和经常测试,从而让您的团队走向成功。Kotlin 可编译成 JVM 字节码,并与 Java 完全互操作,因而有助于您轻松完成迁移。

组建团队

迁移前的第一步是让您的团队建立基本共识。下面给出了一些建议,或可助您加快团队的学习进度。

组建学习小组

学习小组是促进学习和加强记忆的一种有效方法。研究表明,以小组的形式背诵所学内容有助于巩固学到的知识。为每个小组成员分发一本 Kotlin 教材或其他学习资料,并让他们每周学习几章。每次小组学习时,各成员都应该相互比较所学内容,并讨论任何疑问或发现。

营造教学文化

虽然并不是每个人都认为自己是老师,但每个人都可以教授知识。从技术骨干或团队领导到个人贡献者,每个人都可以促成营造一种有助于确保获得成功的学习环境。其中一种方法便是定期举行报告会,从团队中指定一位成员谈谈自己所学或想要分享的内容。您可以利用学习小组,每周邀请组员自愿讲述一个新的章节,直到团队熟悉这种语言为止。

指定带头人

最后,指定一个带头人来带领其他人学习。开启采用过程时,此人可充当主题专家 (SME)。请务必让此人参加与 Kotlin 相关的所有实践会议。带头人最好热衷于 Kotlin 并且已经具备一些相关实践知识。

缓集成

缓慢起步并从战略上思考最先迁移生态系统的哪些部分是关键所在。通常,最好将此工作隔离到组织内的单个应用(但应避开旗舰应用)中。至于所选应用的迁移,尽管每种情况都不尽相同,但均可参考以下这些常见的起步方法。

数据模型

您的数据模型很可能由大量状态信息和少许方法组成。数据模型中可能还包含一些常见方法,例如 toString()equals()hashcode()。这些方法通常可以在隔离环境中轻松地进行转换和单元测试。

例如,假设有以下 Java 代码段:

public class Person {

   private String firstName;
   private String lastName;
   // ...

   public String getFirstName() {
       return firstName;
   }

   public void setFirstName(String firstName) {
       this.firstName = firstName;
   }

   public String getLastName() {
       return lastName;
   }

   public void setLastName(String lastName) {
       this.lastName = lastName;
   }

   @Override
   public boolean equals(Object o) {
       if (this == o) return true;
       if (o == null || getClass() != o.getClass()) return false;
       Person person = (Person) o;
       return Objects.equals(firstName, person.firstName) &&
               Objects.equals(lastName, person.lastName);
   }

   @Override
   public int hashCode() {
       return Objects.hash(firstName, lastName);
   }

   @Override
   public String toString() {
       return "Person{" +
               "firstName='" + firstName + '\'' +
               ", lastName='" + lastName + '\'' +
               '}';
   }
}

您可以将该 Java 类替换为一行 Kotlin 代码,如下所示:

data class Person(var firstName: String?, var lastName : String?)

随后可以针对当前测试套件对该代码进行单元测试。我们建议从小入手,一次迁移一个模型,并主要处理涉及状态而不涉及行为的类。在此过程中请务必时常进行测试。

迁移测试

另一个可供考虑的起步途径是转换现有测试并开始用 Kotlin 编写新的测试。这样能让您的团队有时间先熟悉这种语言,然后再编写您计划随应用一起提供的代码。

将实用程序方法转换为扩展函数

任何静态实用程序类(StringUtilsIntegerUtilsDateUtilsYourCustomTypeUtils 等)都可以表示为 Kotlin 扩展函数,并可供您现有的 Java 代码库使用。

例如,假设您有一个包含几个方法的 StringUtils 类:

package com.java.project;

public class StringUtils {

   public static String foo(String receiver) {
       return receiver...;  // Transform the receiver in some way
   }

   public static String bar(String receiver) {
       return receiver...;  // Transform the receiver in some way
   }

}

这些方法随后可能会在应用中的其他位置使用,如以下示例所示:

...

String myString = ...
String fooString = StringUtils.foo(myString);

...

通过使用 Kotlin 扩展函数,您可以向 Java 调用方提供相同的 Utils 接口,同时还能为不断扩大的 Kotlin 代码库提供更简洁的 API。

为此,您可以先使用 IDE 提供的自动转换功能将此 Utils 类转换为 Kotlin 代码。示例输出可能与以下代码类似:

package com.java.project

object StringUtils {

   fun foo(receiver: String): String {
       return receiver...;  // Transform the receiver in some way
   }

   fun bar(receiver: String): String {
       return receiver...;  // Transform the receiver in some way
   }

}

接下来,移除该类或对象定义,在每个函数名称前面加上应该应用此函数的类型作为前缀,并以此来引用该函数内的类型,如以下示例所示:

package com.java.project

fun String.foo(): String {
    return this...;  // Transform the receiver in some way
}

fun String.bar(): String {
    return this...;  // Transform the receiver in some way
}

最后,将 JvmName 注释添加到源文件的顶部,以使编译的名称与应用的其余部分兼容,如以下示例所示:

@file:JvmName("StringUtils")
package com.java.project
...

最终版本应与以下代码类似:

@file:JvmName("StringUtils")
package com.java.project

fun String.foo(): String {
    return this...;  // Transform `this` string in some way
}

fun String.bar(): String {
    return this...;  // Transform `this` string in some way
}

请注意,现在可以按照与每种语言匹配的惯例,使用 Java 或 Kotlin 调用这些函数。

Kotlin

...
val myString: String = ...
val fooString = myString.foo()
...

Java

...
String myString = ...
String fooString = StringUtils.foo(myString);
...

完成迁移

待您的团队已熟悉 Kotlin 并且您已迁移较小的部分后,您便可以继续处理较大的组件,如 Fragment、Activity、ViewModel 对象,以及与业务逻辑相关的其他类。

注意事项

就像 Java 有特定的样式一样,Kotlin 也有自己的惯用样式,正是这种样式促成了它的简洁。不过,一开始您可能会发现,您的团队生成的 Kotlin 代码看起来更像是要被其替换的 Java 代码。随着您的团队不断积累 Kotlin 使用经验,这种情况会逐步改变。切记,逐步改变是取得成功的关键。

随着 Kotlin 代码库不断扩大,您可以采取以下几项措施来保持一致性:

通用编码标准

请务必在采用过程中尽早制定一套标准的编码规范。只要合理,您的规范可以与 Android Kotlin 样式指南不一致。

静态分析工具

请使用 Android Lint 和其他静态分析工具来强制执行为您的团队制定的编码标准。作为第三方 Kotlin Linter 的 klint 也针对 Kotlin 提供了额外的规则。

持续集成

请确保符合通用编码标准,并为 Kotlin 代码提供足够的测试覆盖范围。将此纳入自动构建流程有助于确保一致性和遵循这些标准。

互操作性

Kotlin 在大多数情况下可与 Java 无缝互操作,但请注意以下几点。

是否可为 null

Kotlin 依靠编译后代码中的“是否可为 null”注释在 Kotlin 端推断相应值是否可为 null。如果未提供注释,则 Kotlin 默认采用平台类型(可将其视为可为 null 类型,也可将其视为不可为 null 类型)。不过,如果不小心处理,这可能会导致运行时 NullPointerException 问题。

采用新功能

Kotlin 提供了许多新库和语法糖来减少样板代码,这有助于提高开发速度。即便如此,在使用 Kotlin 的标准库函数(例如集合函数协程lambda)时,还是应该小心谨慎并且讲究条理。

下面是新手 Kotlin 开发者经常遇到的一个陷阱。假设有以下 Kotlin 代码:

val nullableFoo: Foo? = ...

// This lambda executes only if nullableFoo is not null
// and `foo` is of the non-nullable Foo type
nullableFoo?.let { foo ->
   foo.baz()
   foo.zap()
}

本例的目的是在 nullableFoo 不为 null 的情况下执行 foo.baz()foo.zap(),从而避免出现 NullPointerException。虽然这段代码可以发挥预期作用,但读起来不如简单的 null 检查和智能类型转换直观,如以下示例所示:

val nullableFoo: Foo? = null
if (nullableFoo != null) {
    nullableFoo.baz() // Using !! or ?. isn't required; the Kotlin compiler infers non-nullability
    nullableFoo.zap() // from guard condition; smart casts nullableFoo to Foo inside this block
}

测试

在 Kotlin 中,默认情况下,类及其函数处于关闭状态,不能扩展。您必须明确打开要子类化的类和函数。此行为是一种语言设计决策,旨在促进代码的编写而非继承。Kotlin 具有内置支持,可通过委托实现行为,以帮助简化代码编写。

此行为会给依靠接口实现或继承在测试期间替换行为的模拟框架(如 Mockito)带来问题。对于单元测试,您可以启用 Mockito 的 Mock Maker Inline 功能,该功能可让您模拟最终类和方法。或者,您也可以使用 All-Open 编译器插件在编译过程中打开要测试的任何 Kotlin 类及其成员。使用此插件的主要优势在于,它既支持单元测试,又支持插桩测试。

更多信息

如需详细了解如何使用 Kotlin,请查看以下链接: