在大型团队中采用 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,请查看以下链接: