引言
Kotlin 是一个基于 JVM 的新的编程语言,由 JetBrains 开发。JetBrains,作为目前广受欢迎的 Java IDE IntelliJ 的提供商,在 Apache 许可下已经开源其 Kotlin 编程语言。与 Java 相比,Kotlin 的语法更简洁、更具表达性,而且提供了更多的特性,比如,高阶函数、操作符重载、字符串模板。它与 Java 高度可互操作,可以同时用在一个项目中。
Kotlin 的定位非常有特点,它并不像 Scala 那样另起炉灶,Scala 是一切尽量自己来,将类库,尤其是集合类都自己来了一遍。实在不够用了再用 java 的;而 Kotlin 是对现有 Java 的增强,尽量用 Java 的,不够用了再扩展,尤其体现在二者的容器库上,但同时始终保持对 java 的兼容。这种特点导致 Kotlin 的学习曲线极低。这是 Kotlin 官网首页重点强调的:“100% interoperable with Java™”。这意味着什么呢?或者换个问法:我什么时候可以开始在我的项目中引入 Kotlin 呢?我的回答是:现在就可以视你对 kotlin 的掌握程度,逐步引入 kotlin 的代码。
Dima Kovalenko 在博客中分享了他们团队使用 Kotlin 开发商业应用程序的心得和经验,并提供了一些参考资料。希望本文能对广大 Android 开发程序员有所启发。
几个月前,我们的团队决定开始新的尝试:完全应用 Kotlin 编程语言开发一个商业应用程序,这是 JetBrains 公司设计并开源的一种新编程语言。以前,我们有过 Kotlin 的经验,但那只是小规模应用:将应用程序的一部分转换到一种新的语言,或者应用在花里胡哨的项目。然而,用新的编程语言来开发商业应用程序,我们遇到了一些困难:
- 我们深深扎根于基于 Java 的 Android 开发。切换到 Kotlin 相当困难,对于以前没有函数式编程经验的人员而言,尤为困难。
- 有些东西只是不工作。 Dagger 也没有立即很好的使用。
所有这些问题,都可能会导致项目无法按期交付、并带来应用程序的稳定性问题。
一个人应该有强烈的转型动力。我们的激励是:相信Kotlin 将是 Android 平台开发的颠覆者——这只是一句玩笑话。
让我们打开 Kotlin 的参考书,开始开发 Voter 应用程序。Kotlin 是一种与 Java 具有 100%互操作性的 JVM 语言,如果您熟悉 Java,那么学习 Kotlin 就会很容易。然而,如果想充分利用这个编程语言,理解函数式编程概念是至关重要的。
学习函数式编程需要一段时间。所以要有耐心。
至少在学习之初,函数式编程并不容易。我强烈建议使用 Martin Ordersky 的“Scala 中的函数式编程”的课程来学习。Scala 有时势不可挡,但它提供了一个极好的函数式编程思维的概述。你可以把 Kotlin 看成 Scala 的一个更简化的版本。
为什么我们转向 Kotlin 阵营
函数式编程风格
Kotlin 与 Java 是 100%可互操作的。此外,Kotlin 是一种函数式语言。后者允许将表达性的代码编写得更优雅。
1. 纯函数
纯函数(没有副作用的函数)是最重要的函数概念,它允许我们大大降低代码复杂性并消除大多数可变状态。
在 JavaScript、Java 和 C#这些命令式编程语言中,副作用无处不在。这使得调试非常困难,因为变量可以在程序中的任何位置更改。所以当出现一个错误时,由于变量可以在错误的时间更改为错误的值,那么你到哪里去寻找错误呢?到处寻找错误吗?这可不好玩啊!
请注意我们是如何操作数据而不更改其内容的。
fun flatTree(tree: TreeNode): List<TreeNode> = listOf(tree, *tree.children.flatMap(::flatTree).toTypedArray())
2. 高阶函数
高阶函数将函数用作参数,返回函数或将函数作为返回值的函数。
高阶函数无处不在。你只需将函数传递给集合,就能使代码更容易阅读。比如, titles.map {it.toUpperCase()}
读取简单的英语,是不是很棒?
让我们设想一种情况,假设要计算不同类型的未读消息的数量。典型的方法是:
private fun getUnreadCountFromUsers() { val conversations = datasource.getConversations() var count = 0 for (conversation in conversations) { if (conversation.recipientId != null) { for (message in conversation.messages) { if (message.unread) { count += 1 } } } } } private fun getNumberOfUnreadAttachmentsInGroupConversations() { val conversations = datasource.getConversations() var count = 0 for (conversation in conversations) { if (conversation.groupId != null) { for (message in conversation.messages) { if (message.unread && message.type == MessageType.ATTACHMENT) { count += 1 } } } } }
正如你所看到的,当引入新的需求时,代码变得难以理解、不可收拾。让我们看看如何使用高阶函数来解决这个问题:
private fun getNumberOfAttachmentsInGroupConvesationsFun() { return getCount({conv -> conv.groupId != null}, {it -> it.type == MessageType.ATTACHMENT && it.unread}) } private fun getUnreadCountFromUsersFun() { return getCount({conv -> conv.recipientId != null}, {message -> message.unread}) } private fun getTotalNumberOfMessages() = getCount({true}, {true}) private fun getCount(convFilter: (Conversation) -> Boolean, messageFilter: (Message) -> Boolean) { datasource.getConversations() .filter(convFilter) .flatMap { it.messages } .filter(messageFilter) .fold(0, { count, message -> count + 1}) }
我们还可以想象一下用例,假设想将fold
函数变量参数化。比方说,计算未读消息的乘积。
使用高阶函数的另一个例子是用简单的高阶函数代替多个监听器:
BillingView : LinearLayout {
var billingChangeListener: (() -> Unit)? = null
…}
… // in an activity far, far away
billingView.billingChangeListener { updateUI() }
3. 不变性
不变性使得代码更容易编写,使用和推理代码(类不变性一次建立,然后不变——一劳永逸)。应用程序组件的内部状态将更加一致。Kotlin 通过引入val
关键字以及 Kotlin 集合来强制不变性,Kotlin 集合在默认情况下是不可变的。 一旦val
或者一个集合被初始化,你就可以确定它的有效性。(有关val
关键字的更精确的定义,请参阅文末的更新)。
data class Address(val line1: String, val city: String)
val items = listOf(Address(“242 5th St”, “Los Angeles”), Address(“Dovzhenka St. 5”, “Kiev”))
空安全(Null-safety)
这个语言特性使我们仔细考虑了模型类中字段的可空性。以前,当不确定 DTO 中的字段是否已初始化时,@Nullable 和 @NotNull 的注释就能提供帮助,但也很有限。现在,使用 Kotlin,就能让你准确知道什么字段可以为 null,什么字段被初始化(例如,Dagger 注入的字段),并且你可以对这些字段有严格的控制。结果?几乎没有NullPointerExceptions
。(在内部我们管?.
叫做“鹅”操作符,因为它看起来像一个鹅的脖子。)
brand?.let { badge.enabled = brand.isNewBadge }
// Can also be written as
badge.enabled = brand?.isNewBadge?:false
Anko
Anko DSL 是一个很了不起的的库,它大大简化了工作视图、线程和 Android 生命周期。据 Github 的描述,Anko 是“令人愉快的 Android 应用程序开发”,事实证明,的确如此。
selector(items = listOf("Like", "Dislike") { when (it) { 0 -> if (!liked) likePost() else -> if (!disLiked) disLikePost() } } doAsync { // Long background task uiThread { alert(R.string.could_not_log_in) { yesButton { dismiss() } cancellable = false }.show() } }
注意,当uiThread
在 Activity 内调用时,如果isFinishing
为true
,块将不会执行。我们实际上不使用这个功能,因为 RxJava 会处理应用程序中的所有线程,但它是一个很好的功能。
使用 Anko 而不是 XML。虽然 Anko 还没有做好准备取代标准的 Android UI 构建,但有的时候,它非常方便。
verticalLayout() { friendsPanel = friendsPanel.with(friendsData).lparams(width = matchParent) politicalMapCardView { setMarker(quizManager.getMarker()) }.lparams(width = matchParent) { topMargin = dip(10) } cardView() { verticalLayout() { topPadding = dip(5) textView(getString(R.string.register_question)) blueButtonView(text="Register here") { onClick { browse("https://www.uptech.team") } } } }.lparams(width = matchParent) { topMargin = dip(10) bottomMargin = dip(20) } }
如您所见,Anko DSL 允许您在 Android 内置视图中使用自定义视图。这一点与标准 XML 相比有很大的优势。
Kotlin Android 扩展:删除 ButterKnife 依赖
@Bind(R.id.first_name) protected EditText firstName; @Bind(R.id.last_name) protected EditText lastName; @Bind(R.id.address_line1) protected EditText addressLine1; @Bind(R.id.address_line2) protected EditText addressLine2; @Bind(R.id.zip_code) protected EditText zipCode; @Bind(R.id.state) protected TextView state; @Bind(R.id.state_spinner) protected HintSpinner stateSpinner; @Bind(R.id.city) protected EditText city; @Bind(R.id.frag_shipping_address_save_btn) protected Button saveBtn; @Bind(R.id.agreement) protected TextView agreement; @Bind(R.id.email) protected EditText email; @Bind(R.id.password) protected EditText password; @Bind(R.id.create_account_container) protected LinearLayout accountContainer; @Bind(R.id.member_container) protected LinearLayout memberContainer; @Bind(R.id.logged_in_title) protected TextView loggedInTitle; @Bind(R.id.user_email) protected TextView userEmail; @Bind(R.id.sign_out) protected TextView signOut; @Bind(R.id.scrollview) protected ScrollView scrollView; @Bind(R.id.dummy) protected EditText dummyView;
上面那段代码读起来无聊吗?我敢打赌你一直滚动而没有阅读。在 Kotlin,你并不需要任何这些东西。您可以通过其 @id XML 参数引用视图属性,这些属性将与 XML 文件中声明的名称相同。更多信息可以在官方文档中找到。
其他整洁的功能
1. 扩展功能和构建器
items = StoreInfo().apply { storeItems = fetchItems() }.let { manager.process(it) } container.apply { removeAllViews() items.forEach { addView(ShopItemView(context).withData(it)) } } fun ShopItemView.withData(item: StoreItem): ShopItemView { title = item.title image = item.image Brand.findById(item.id)?.let { brandName = it.name } }
apply
、let
和扩展功能可以轻松地用于创建简洁的构建器。
2. 为初学者快速破解
在最初的前几天,你经常被一个问题难倒:你不知道如何在 Kotlin 中写一个相当简单的 Java 表达式。这里有一个简单的诀窍,就是是在 Java 中编写一段代码,然后将其粘贴到 Kotlin 文件中。感谢 JetBrains 的工程师们,它会自动转换为 Kotlin。 黑客的工作方式就像一个魔术!
3. 摆脱不必要的依赖
Kotlin 替换了许多第三方库,如 ButterKnife、Google Autovalue、Retrolambda、Lombok 和一些 RxJava 代码。
总结
作为一个软件开发团队,我们面临的主要挑战是提供优秀的产品,并有效地完成工作。虽说开始用 Kotlin 有效的开发软件,需要你有函数式编程的背景,但是,投入精力去学习是值得的,能给你巨大的回报。我相信,Kotlin 是常规 Android 开发的一个重大改进,能让我们及时提供错误更少的、更加优秀的应用程序。
更新:val
实际上并不意味着“不可变的”,而是“只读”。有关详细信息,请参阅此文章。
参考文献
- 《Kotlin 参考手册》
- 《所以,你要成为一名函数式程序员》
- 《为什么函数式编程很重要》
- 《Java 使用不可变对象编程的 6 大好处》
- 《Anko DSL 对 Android XML-First》
- 《将应用转换为纯 Kotlin 的经验教训》
- 《结果:应用程序投票选举:99.8% 无故障用户》
- 《Kotlin:val 不意味着不可变,而意味着只读》
感谢徐川对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论