在Android项目中配置Kotlin
官方文档讲了如何一步步安装Kotlin插件,并使用插件在Android项目中自动修改Gradle文件来添加对Kotlin的支持。我不建议大家这么做,因为这样自动完成的结果可能并不完美,即使修改后的Gradle文件工作正常,也会打乱Android项目中Gradle文件的一般形式。
说实话,我一直不喜欢有些Android Studio插件直接修改Android build文件,因为经常搞得很乱,我又得一点一点清理直到符合我的风格。Gradle构建文件也是源代码,而这些插件并不擅长修改已经存在的代码。所以如果你也和我一样有些挑剔,那就多花一分钟和我一起手动配置。
下面我们要分四步完成Kotlin的配置。
- 新建一个Android项目。
- 修改Gradle代码来添加Kotlin Gradle插件与标准库。
- 在IntelliJ或Android Studio中添加Kotlin插件。
- 将Java类文件转换成Kotlin。
首先,直接以默认方式新建一个Android项目,此时应该自带一个Activity。之后,要在两个build.gradle文件中添加五行重要代码,我都在其后添加了注释。下面让我们先修改最高层的build.gradle脚本,添加两行代码。
这样就会在项目构建时添加Kotlin Gradle插件。请注意上面在ext.kotlin_version中标注的kotlin版本字符串,我们一会还要在app模块的compile dependencies中用到它,而且两个地方版本必须相符。你最好使用官方文档中最新版本。
然后,在app模块自己的build.gradle文件中紧随Android plugin添加kotlin-android plugin。这样整个项目就整合了Kotlin,在build项目时会编译Kotlin文件,这样最后所有的类文件都会打包在一个app中。
惯例上,Kotlin文件存放在src/main/kotlin路径中,但也可以把他们和Java文件一起放在/src/main/java路径中。这里我们还是按照惯例,并在Gradle中标注一个新的Kotlin源路径。
不要忘了新建这个路径,一会就要用到了。最后需要添加一个Kotlin依赖,直接使用build.gradle中的kotlin版本变量。
不过这个包有多大呢?好问题!每当我们添加新的依赖时,都应该搞清楚这个包有多大。不过对于这个问题,我会在后面的文章中回答。
这就是Kotlin Gradle插件,走完这些步骤后,就可以在项目中运行Kotlin代码了。不过你还需要添加IDE对Kotlin的支持,所以如果你还没有安装IntelliJ或Android Studio的Kotlin插件,那就赶快安装。安装Kotlin插件就像安装其他任何插件一样,可以在Preferences->Plugins->Insall JetBrains plugin下找到。安装后要重启IDE,做完这一步后,准备工作就完成了。我发现IDE对Kotlin的支持甚至和Java语言一样好。这也可以理解,毕竟IDE和Kotlin都是JetBrains开发的嘛。
快速将Java转成Kotlin
IDE插件有一个很有趣的功能就是将Java文件直接转成Kotlin。这个插件可以很智能地将Java语言风格转换成Kotin风格并保持运行兼容。如果你创建了一个Android项目,那就找到自动生成的MainActivity,在左边的项目结构中选中,并触发IDE的action "Convert Java File to Kotlin File"。你可以按下快捷键Command+Shift+A(OSX)来选择action。这个插件甚至有专门针对这个action的快捷键Option+Shift+Command+K(OSX)。其实官方并不建议直接转换Java文件,但直到现在我还没遇到过什么问题。
如果你按我说的操作转换了Java文件,就会在原本.java文件的地方找到一个.kt文件。
你可以看到现在MainActivity在右下角有一个K标志(这里隐藏了.kt扩展名)。由于我们刚刚专门为Kotlin配置了一个路径,我们把Kotlin文件拖进kotlin文件夹中。注意要保留kotlin文件中类的包名,不然项目无法运行。
如果你想在项目中只用Kotlin,那可以干脆删除掉Java文件夹,把所有的Kotlin文件放在kotlin路径中。之后,项目结构就像下面这样。
从新的Activity中你可以大概知道Kotlin长什么样。我下面说几点Kotlin与Java很不一样的地方:
- 在Kotlin中你见不到"new"关键字。
- 把类名当做方法并传入参数就可以直接构造对象。
- 数据类型关键字被val(final)与var(variable)取代,Kotlin可以自己判断数据是什么类型。
中场休息
刚才我们一步一步在Android项目中添加了Kotlin支持,现在我们要开始通过代码直观感受Kotlin的语言特色以及它能如何简化Android开发。
当我第一次接触Kotlin并了解其语言特色功能时,有一点让我感触很深,那就是type-safe builders。它让你以陈述式语言风格来创建对象,其语法很类似Gradle,但Gradle和Groovy代码是动态编写的,而kotlin是静态编写的,所以编译器可以在属性的值不合法时告诉你。
type-safe builders的一个典型用法就是构建嵌套式数据结构,比如XML。在Android中有很多XML文件,如layouts和views。如果Kotlin可以以程序的方式动态编写XML,它可能很善于处理层级问题。所以我决定尝试使用Kotlin创建一个动态构建View层级的工具。如果是在Java中做这件事,代码量之大可以想象。
注意:在后面的代码中我会经常使用lambda,所以在继续阅读之前,确保你了解lambda的基本形式。简而言之,lambda是要作为参数被传入某方法或赋值给某变量的匿名方法的简化表现形式。
type-safe builders可行还要归功于Kotlin的一个特色功能:lambda with receiver。下面我们要看一个能真正有用的案例。kotlin可以在类的外面定义方法,我在这里就是这么干的。还要注意变量的名字是在类型之前的,这一点与Java语言正相反。
(点击放大图像)
简便起见,我将上面的方法命名为v,在之后的系列文章中我还会使用到这个方法。这个方法是这么调用的:
这段代码和填充下面这段XML是等效的。
OK,如果这是你第一次看Kotlin代码,相信有不少需要翻译的。下面解释一下刚才代码中涉及的几个语法。
<reified TV : View>
reify的意思是具体化。而作为Kotlin的一个方法泛型关键字,它代表你可以在方法体内访问泛型指定的JVM类对象。这段代码的意思就是v方法要使用命名为TV(意为Type of View,即View种类)的reified泛型,它指定类必须为View或其子类。必须以内联方式声明这个方法才有效。调用者要给TV指定一个具体的类型。
init: TV.() -> Unit
v方法有两个参数,一个是Context,一个是lambda风格的init。init在这里很特殊,因为它是一种lambda with receiver类型的方法引用。lambda with receiver是一个要求特定类型的对象的代码块,这里要求的对象在lambda代码中通过this关键字引用。在这里receiver对象就是reified泛型TV。
在我们这个例子中,v要创建一个类型为TV的对象,需要调用者告诉它如何创建。这个新创建的TV类型对象会成为给定的lambda的receiver,labmda通过view.init()在v中被调用,以便对view进行操作。“-> Unit”意思是lambda返回Unit类型,就如Java中的Void一样,即什么也不返回。
总结一下这里的lambda with receiver:
- v声明了一个名叫init的参数,它是TV类型的lambda with receiver。
- v创建并初始化一个TV对象,并在其上调用lambda来初始化它。
- lambda在自己代码块中通过this关键字引用TV对象。
TV::class.java
通过TV::class.java表达你可以引用reified泛型TV的Class对象。这种表达在Kotlin中专门针对reified泛型,与Java相比,可以大量减少代码。
到这里,估计你会有一些问题:
为什么v有两个参数,而调用的时候似乎只给了一个?
这个只是因为Java程序员对Kotlin语法并不熟悉。在Java中,一个方法的所有的参数都要写在圆括号内,所以当有匿名回调方法时会变得很长。但在Kotlin中,当lambda时最后一个参数时,有一个特殊语法,即lambda出现在紧随圆括号的大括号中。你可以把所有的代码都放在圆括号内,但大多数情况下这么写代码会显得更加简洁,而且可以使一组圆括号保持在一行里,更易于追踪。另外,这种语法在传入只有一个方法的匿名内部类比如Runnable时也可以派上用场。
layoutParams和text是变量吗?
lambda with receiver的一个语法特色就是当操作this的方法或属性时this关键字可以省略。但在上面的调用例子中layoutParams和text到底是什么?其实这些属性是Kotlin提供的receiver类型(TV)的属性。因为TextView有setLayoutParams()与setText()等方法,Kotlin会自动识别这些JavaBean风格的方法并为他们创建属性,好像它们是类的成员变量一样。所以这里text = "Hello"等价于this.setText("Hello")。下面是在安装了Kotlin插件的Android
Studio中的截图,里面展示了自动完成的内容。
如你所见,Kotlin插件之处text属性是从TextView(receiver对象)中JavaBean风格的getter/setter方法中派生出的。
构造器到底是怎么运作的?就不能new TV(context)吗?
因为编译器也不知道在v方法内部TV到底是什么类型,我们不能new它的对象。不过我们可以利用reified Class对象(TV::class.java)来获取仅有一个context参数的构造器。View都有这样一个构造器,不然我们也写不出这个程序。我们通过构造器对象来获得TV类型的实例,就如Java中的new关键字一样。想一想我们可以通过一个方法适配所有View,而不再针对每个View单独写方法,这一点小麻烦还是很值的。在后续系列文章中我们也会优化这个方法。
这几行代码真的很大的简化了我们的工作。如果你刚刚接触Kotlin,建议你从头再消化一遍,毕竟许多概念和Java差别很大。我自己也花了好多时间来理解这些概念。
这些都只是开始,这个方法有很多地方可以完善和升级,使之更加易用。比如我想用用一行代码构造一整个View层级。所以不要走开,我会在后续文章探索Kotlin还能做什么。