由DataArt强壮的Android开发人员Sergey Yeshin发布自Google宣布对Android中的Kotlin正式支持以来,已经过去了一年半,经验最丰富的开发人员开始在战斗中进行试验,而三年前还没有进行过此类项目。
这种新语言在Android社区中受到热烈欢迎,并且绝大多数新的Android项目将从Kotlin入手。 Kotlin编译为JVM字节码也很重要,因此,它必须与Java完全兼容。 因此,在用Java编写的现有Android项目中,也有机会(此外,有必要)使用Kotlin的所有功能,这使他获得了众多粉丝。
在本文中,我将讨论将Android应用程序从Java迁移到Kotlin的经验,在此过程中必须克服的困难,并说明为什么所有这些都没有白费。 本文主要针对刚开始学习Kotlin的Android开发人员,除个人经验外,还依赖于其他社区成员的资料。
为什么选择科特林?
简要描述Kotlin的功能,由于这个原因,我在项目中切换到它,离开了“舒适而痛苦的熟悉” Java世界:
- 全面的Java兼容性
- 空安全
- 类型推断
- 扩展方式
- 用作一流和lambda对象
- 泛型
- 协程
- 无检查异常
DISCO应用
这是用于交换折扣卡的小型应用程序,由10个屏幕组成。 使用他的示例,我们将考虑迁移。
简要介绍建筑
该应用程序将MVVM体系结构与Google体系结构组件一起使用:ViewModel,LiveData,Room。
另外,根据Bob叔叔的“干净架构”原则,我在应用程序中选择了3个层:数据,域和表示。
从哪里开始? 因此,我们设想了Kotlin的主要功能,并对需要迁移的项目有了一个最小的想法。 自然的问题是“从哪里开始?”。
官方的
Kotlin Android
入门文档页面说,如果要将现有应用程序移植到Kotlin,则只需开始编写单元测试。 当您对这种语言有一点了解后,可以用Kotlin编写新代码,您只需要转换现有的Java代码即可。
但是只有一个“但是”。 确实,通过简单的转换通常(尽管并非总是如此)可以使您在Kotlin上获得有效的代码,但是,它的成语还有很多不足之处。 此外,我将告诉您如何消除Kotlin语言所提到的(不仅是)功能所带来的差距。
层迁移
由于应用程序已经分层,因此从顶部开始逐层迁移是有意义的。
下图显示了迁移期间的层顺序:
我们恰好从上层开始迁移并非偶然。 因此,我们避免了自己在Java代码中使用Kotlin代码。 相反,我们使上层的Kotlin代码使用下层的Java类。 事实是,Kotlin最初的设计考虑了与Java交互的需求。 现有的Java代码可以自然地从Kotlin调用。 我们可以轻松地从现有Java类继承,访问它们并将Java注释应用于Kotlin类和方法。 Kotlin代码也可以在Java中使用而不会带来太多麻烦,但是通常需要花费额外的精力,例如添加JVM注释。 又为什么在Java代码中进行不必要的转换(如果最终仍将用Kotlin重写)呢?
例如,让我们看一下过载的产生。
通常,如果您使用默认参数值编写Kotlin函数,则该函数仅在Java中作为具有所有参数的完整签名可见。 如果要为Java调用提供多个重载,则可以使用@JvmOverloads批注:
class Foo @JvmOverloads constructor(x: Int, y: Double = 0.0) { @JvmOverloads fun f(a: String, b: Int = 0, c: String = "abc") { ... } }
对于具有默认值的每个参数,这将创建一个额外的重载,该重载在远程参数列表中具有该参数及其右侧的所有参数。 在此示例中,将创建以下内容:
有许多使用JVM注释来正确运行Kotlin的示例。
该文档页面详细介绍了从Java到Kotlin的调用。
现在我们逐层描述迁移过程。
表示层
这是一个用户界面层,其中包含带有视图的屏幕和一个ViewModel,而后者又包含LiveData形式的属性以及来自模型的数据。 接下来,我们来看一下在迁移此应用程序层时有用的技巧和工具。
1. Kapt注释处理器
与任何MVVM一样,View通过数据绑定绑定到ViewModel属性。 对于Android,我们正在处理使用注解处理的Android Databind库。 因此Kotlin有
其自己的注释处理器 ,如果您不对相应的build.gradle文件进行更改,则该项目将停止构建。 因此,我们将进行以下更改:
apply plugin: 'kotlin-kapt' android { dataBinding { enabled = true } } dependencies { api fileTree(dir: 'libs', include: ['*.jar'])
重要的是要记住,必须用kapt完全替换build.gradle中所有出现的commentProcessor配置。
例如,如果您在项目中使用Dagger或Room库,并且它们也在后台使用注释处理器进行代码生成,则必须将kapt指定为注释处理器。
2.内联函数
将功能标记为内联时,我们要求编译器将其放置在使用位置。 函数的主体被嵌入,换言之,它代替了函数的常规用法。 因此,我们可以规避类型擦除的限制,即擦除类型。 使用内联函数时,我们可以在运行时获取类型(类)。
我的代码中使用Kotlin的此功能来“提取”已启动的Activity的类。
inline fun <reified T : Activity> Context?.startActivity(args: Bundle) { this?.let { val intent = Intent(this, T::class.java) intent.putExtras(args) it.startActivity(intent) } }
修饰-指定修饰类型。
在上述示例中,我们还谈到了Kotlin语言的扩展功能。
3.扩展
它们是扩展。 实用程序方法是在扩展中删除的,这有助于避免过分庞大的类实用程序。
我将举例说明该应用程序涉及的扩展:
fun Context.inflate(res: Int, parent: ViewGroup? = null): View { return LayoutInflater.from(this).inflate(res, parent, false) } fun <T> Collection<T>?.isNotNullOrEmpty(): Boolean { return this != null && isNotEmpty(); } fun Fragment.hideKeyboard() { view?.let { hideKeyboard(activity, it.windowToken) } }
Kotlin开发人员通过提供Kotlin Android Extensions插件预先考虑了有用的Android扩展。 他提供的功能包括View绑定和Parcelable支持。 有关此插件功能的详细信息,请参见
此处 。
4. Lambda函数和高阶函数
使用Android代码中的lambda函数,您可以摆脱笨拙的ClickListener和回调,这在Java中是通过自写接口实现的。
使用lambda代替onClickListener的示例:
button.setOnClickListener({ doSomething() })
Lambda也用于高阶函数,例如,用于收集函数。
以
地图为例:
fun <T, R> List<T>.map(transform: (T) -> R): List<R> {...}
我的代码中有一个地方需要“映射”卡的ID,以便随后将其删除。
使用传递给map的lambda表达式,我得到了所需的id数组:
val ids = cards.map { it.id }.toIntArray() cardDao.deleteCardsByIds(ids)
请注意,如果lambda是唯一的参数,而关键字关键字是唯一的参数的隐式名称,则在调用函数时可以完全省略括号。
5.平台类型
您将不可避免地需要使用Java编写的SDK(实际上包括Android SDK)。 这意味着您应该始终对Kotlin和Java Interop(例如平台类型)保持警惕。
平台类型是Kotlin找不到空有效性信息的类型。 事实是,默认情况下,Java代码不包含有关null有效性的信息,并且并非总是使用
NotNull和@ Nullable批注。 如果Java中没有相应的注释,则该类型将成为平台。 您既可以将其用作允许null的类型,也可以将其用作不允许null的类型。
这意味着就像在Java中一样,开发人员完全负责这种类型的操作。 编译器不会添加空检查运行时,并允许您执行所有操作。
在以下示例中,我们在Activity中覆盖onActivityResult:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent{ super.onActivityResult(requestCode, resultCode, data) val randomString = data.getStringExtra("some_string") }
在这种情况下,数据是可能包含null的平台类型。 但是,从Kotlin代码的角度来看,数据在任何情况下都不能为空,并且无论您是否将Intent类型指定为可为空,都不会从编译器收到警告或错误,因为两种版本的签名均有效。 但是由于不能保证接收非空数据,因为在使用SDK的情况下您无法控制它,因此在这种情况下获取null会导致NPE。
另外,作为示例,我们可以列出以下可能出现的平台类型:
- Service.onStartCommand(),其中Intent可以为null。
- BroadcastReceiver.onReceive()。
- Activity.onCreate(),Fragment.onViewCreate()和其他类似方法。
此外,碰巧该方法的参数都带有注释,但是由于某些原因,工作室在生成替代项时会失去Nullability。
域层
该层包括所有业务逻辑;它负责数据层和表示层之间的交互。 这里的关键角色是由存储库扮演的。 在存储库中,我们执行服务器端和本地的必要数据操作。 楼上,在Presentation层,我们仅提供Repository接口方法,该方法隐藏了数据操作的复杂性。
如上所述,RxJava用于实现。
1. RxJava
Kotlin与RxJava完全兼容,并且与Java相比更加简洁。 但是,即使在这里,我也不得不面对一个不愉快的问题。 听起来像是这样:如果将lambda作为
andThen方法的参数传递,则该lambda将不起作用!
要验证这一点,只需编写一个简单的测试:
Completable .fromCallable { cardRepository.uploadDataToServer() } .andThen { cardRepository.markLocalDataAsSynced() } .subscribe()
然后内容
将失败。 对于大多数运算符(例如
flatMap ,
defer ,
fromAction和许多其他运算符)就是这种情况,
实际上应将 lambda作为参数。 有了
andThen这样的记录,
可以预期Completable / Observable / SingleSource 。 通过使用普通括号()而不是卷曲{}可以解决此问题。
“ Kotlin和Rx2。 我如何因为括号错误而浪费了5个小时 。
“2.重组
我们还谈到了有趣的Kotlin语法,例如解构或
解构赋值 。 它允许您一次将一个对象分配给多个变量,将其分成多个部分。
假设我们在API中有一个方法可以一次返回多个实体:
@GET("/foo/api/sync") fun getBrandsAndCards(): Single<BrandAndCardResponse> data class BrandAndCardResponse(@SerializedName("cards") val cards: List<Card>?, @SerializedName("brands") val brands: List<Brand>?)
从此方法返回结果的一种紧凑方法是解构,如以下示例所示:
syncRepository.getBrandsAndCards() .flatMapCompletable {it-> Completable.fromAction{ val (cards, brands) = it syncCards(cards) syncBrands(brands) } } }
值得一提的是,多重声明是基于约定的:应该被销毁的类必须包含componentN()函数,其中N是相应的组件号-该类的成员。 也就是说,上面的示例转换为以下代码:
val cards = it.component1() val brands = it.component2()
我们的示例使用一个数据类,该数据类自动声明componentN()函数。 因此,多条声明可以和他一起工作。
在下一部分中,我们将专门讨论数据层,以进一步讨论数据类。
资料层
该层包括POJO,用于处理来自服务器和基础的数据,以及用于处理本地数据和从服务器接收的数据的接口。
为了处理本地数据,使用了Room,它为我们提供了一个方便的包装器来处理SQLite数据库。
移植本身的第一个目标就是POJO,它在标准Java代码中是具有许多字段的批量类及其相应的get / set方法。 您可以借助数据类使POJO更加简洁。 一行代码足以描述具有多个字段的实体:
data class Card(val id:String, val cardNumber:String, val brandId:String,val barCode:String)
除了简洁之外,我们还获得:
- 在幕后重写equals() , hashCode()和toString()方法。 在为RecyclerView生成视图的适配器中使用DiffUtil时,为数据类的所有属性生成均等值非常方便。 事实是DiffUtil比较两个数据集,两个列表:旧列表和新列表,找出发生了什么更改,并使用notify方法最佳地更新适配器。 通常,列表项使用等于进行比较。
因此,在将新字段添加到类之后,我们无需将其添加到equals,以便DiffUtil将新字段考虑在内。 - 不变的阶级
- 支持默认值,可以使用生成器模式替换默认值。
一个例子:
data class Card(val id : Long = 0L, val cardNumber: String="99", val barcode: String = "", var brandId: String="1") val newCard = Card(id =1L,cardNumber = "123")
另一个好消息是:配置了kapt(如上所述)后,数据类可以与Room注释配合使用,这使您可以将所有数据库实体转换为数据类。 Room还支持可为空的属性。 的确,Room还不支持Kotlin的默认值,但是已经为此设置了相应的错误。
结论
我们仅研究了从Java迁移到Kotlin期间可能出现的一些陷阱。 重要的是,尽管出现问题,尤其是缺乏理论知识或实践经验,但所有问题都可以解决。
但是,用Kotlin编写简洁的表达性和安全的代码所带来的乐趣将远远超过过渡路径上出现的所有困难。 我可以有信心地说,DISCO项目的例子肯定证实了这一点。
书籍,有用的链接,资源
- 语言知识的理论基础将使语言Svetlana Isakova和Dmitry Zhemerov的创建者可以编写《行动中的科特林》 。
保守主义,信息量大,主题广泛,专注于Java开发人员以及俄语版本的可用性使其成为语言学习开始时最好的手册。 我从她开始。 - Kotlin 来源与developer.android。
- 科特林俄语指南
- 来自Genesis的Android开发人员Konstantin Mikhailovsky 的精彩文章 ,介绍了切换到Kotlin的经验。