迁移到任何新语言都可能是一项艰巨的任务。成功的秘诀是循序渐进、分块迁移并频繁测试,以确保团队取得成功。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,并开始用 Kotlin 编写新测试。这可以为您的团队提供时间来熟悉该语言,然后再编写计划随应用发布的代码。
将实用方法移至扩展函数
任何静态实用程序类(StringUtils
、IntegerUtils
、DateUtils
、YourCustomTypeUtils
等)都可以表示为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 ktlint 也为 Kotlin 提供了额外的规则。
持续集成
请务必遵守通用编码标准,并为您的 Kotlin 代码提供足够的测试覆盖率。将其作为自动化构建过程的一部分有助于确保一致性并遵守这些标准。
互操作性
Kotlin 在大多数情况下与 Java 无缝互操作,但请注意以下几点。
可为空性
Kotlin 依靠编译代码中的可空性注解来推断 Kotlin 侧的可空性。如果未提供注解,Kotlin 会默认为平台类型,该类型可以被视为可为空或不可为空类型。但是,如果不小心处理,这可能会导致运行时 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
。虽然这段代码按预期工作,但与简单的空值检查和智能转换相比,它的可读性较差,如下例所示
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 的更多信息,请查看以下链接
- Android 的 Kotlin 优先方法
- Kotlin 入门资源
- Java 用户学习 Kotlin 的资源
- Java 到 Kotlin 学习路径:帮助 Java 程序员学习和编写惯用 Kotlin 的资源集合。