Android开发必备知识:为什么说Kotlin值得一试

chengxl 9年前

来自: http://www.jcodecraeer.com//a/anzhuokaifa/androidkaifa/2016/0226/4000.html


原文出处:腾讯Bugly 

1、Hello, Kotlin

1.1 Kotlin的身世

  • 写了许久 Java,有没有发现其实你写了太多冗余的代码?

  • 后来你体验了一下 Python,有没有觉得不写分号的感觉真是超级爽?

  • 你虽然勤勤恳恳,可到头来却被 NullPointerException 折磨的死去活来,难道就没有受够这种日子么?

  • 直到有一天你发现自己已经写了好几十万行代码,发现居然全是 getter 和 setter!

blob.png

哈哈,实际上你完全可以不用这么痛苦,用 Kotlin 替代 Java 开发你的程序,无论是 Android 还是 Server,你都能像之前写 Java 一样思考,同时又能享受到新一代编程语言的特性,说到这里你是不是开始心动了呢?下面我就通过这篇文章来给大家介绍一下 Kotlin 究竟是何方神圣。

话说,Kotlin 是 JetBrain 公司搞出来的,运行在 JVM 上的一门静态类型语言,它是用波罗的海的一个小岛的名字命名的。从外观上,乍一看还以为是 Scala,我曾经琢磨着把 Scala 作为我的下一门语言,不过想想用 Scala 来干嘛呢,我又不做大数据,而它又太复杂了o(�s□�t)o

用Kotlin创建一个数据类

blob.png

最初是在 intelliJ 的源码中看到 Kotlin 的,那时候 Kotlin 的版本还不太稳定,所以源码总是编译不过,真是要抓狂啊,还骂『什么破玩意儿!为什么又出来新语言了?Groovy 还没怎么学会,又来个 Kotlin!』话说,Kotlin,难道是『靠它灵』的意思??

其实经过一年多的发展,Kotlin 1.0已经 release,feature 基本完善,api 也趋于稳定,这时候尝试也不会有那种被坑的感觉了。过年期间也算清闲,于是用 Kotlin 做了个 app,简单来说,就是几个感觉:

  • 思路与写 Java 时一样,不过更简洁清爽

  • 少了冗余代码的烦恼,更容易专注于功能的开发,整个过程轻松愉快

  • 扩展功能使得代码写起来更有趣

  • 空安全和不可变类型使得开发中对变量的定义和初始化倾注了更多关注

  • 啊啊,我再也不用写那个 findViewById 了,真的爽爆有木有!

1.2 第一个Kotlin程序

Kotlin 开发当然使用 JetBrain 系列的 IDE,实际上 intelliJ idea 15 发布时就已经内置了 Kotlin 插件,更早的版本则需要到插件仓库中下载安装 Kotlin 插件――在安装时你还会看到有个 Kotlin Extensions for Android,不要管他,已经过时了。安装好以后,我们就可以使用 Kotlin 进行开发了。

接下来我们用 Android Studio 创建一个 Android 工程,比如叫做 HelloKotlin,在 app 目录下面的 build.gradle 文件中添加下面的配置:

blob.png

这里添加了 Kotlin 对 Android 的扩展,同时也添加了 Kotlin 的 Gradle 插件。

接下来就可以编写 Kotlin 代码了――等等,Android Studio 会帮我们生成一个MainActivity,你可以直接在菜单

Code -> Convert Java file to Kotlin file

将这个 Java 代码转换为 Kotlin 代码。截止到现在,你什么都不用做,程序就已经可以跑起来了。

2、完美为Java开发者打造

2.1 通用的集合框架

我们都知道 Jvm 上面的语言,像什么 Java、Groovy、Jython 啥的,都是要编成虚拟机的字节码的,一旦编成字节码,在一定程度上大家就都平等了。

英雄不问出身啊

有人做过一个非常形象的比喻:Java 虚拟机语言就是打群架。Kotlin 正是充分利用了这一点,它自己的标准库只是基于 Java 的语言框架做了许多扩展,你在Kotlin 当中使用的集合框架仍然跟你在Java当中一样。

举个例子,如果你想要在 Kotlin 中使用 ArrayList,很简单,Java 的 ArrayList 你可以随意使用,这个感觉跟使用 Java 没有任何区别,请看:

blob.png

然,Kotlin 标准库也对这些做了扩展,我们在享用 Java 世界的一切资源的同时,还能比原生 Java 代码更滋润,真是爽爆有木有:

blob.png

2.2 与Java交互

Kotlin 的标准库更多的是对 Java 库的扩展,基于这个设计思路,你丝毫不需要担心 Kotlin 对 Java 代码的引用,你甚至可以在 Kotlin 当中使用 Java 反射,反正只要是 Java 有的,Kotlin 都有,于是有人做出这样的评价:

Kotlin 就是 Java 的一个扩展

这样说 Kotlin 显然是不公平的,但就像微信刚面世那会儿要为 QQ 接收离线消息一样,总得抱几天大腿嘛。

有关从 Kotlin 中调用Java的官方文档在此Calling Java code from Kotlin (https://kotlinlang.org/docs/reference/java-interop.html#static-methods-and-fields),其中最常见的就是 Getter / Setter 方法对应到 Kotlin 属性的调用,举个例子:

准备一个Java类

blob.png

下面是Kotlin代码

blob.png

所以我们在 Android 开发时,就可以这样:

view.background = ...  textView.text = ...

反过来在 Java 中调用 Kotlin 也毫无压力,官方文档C alling Kotlin from Java 对于常见的情况作了比较详细的阐述,这里就不再赘述。

3、简洁,可靠,有趣

3.1 数据类

最初学 Java 的时候,学到一个概念叫 JavaBean,当时就要被这个概念给折磨死了。明明很简单的一个东西,结果搞得很复杂的样子,而且由于当时对于这些数据类的设计概念不是很清晰,因而也并不懂得去覆写诸如 equals 和 hashcode 这样重要的方法,一旦用到 HashMap 这样的集合框架,总是出了问题都不知道找谁。

Kotlin 提供了一种非常简单的方式来创建这样的数据类,例如:

data class Coordinate(val x: Double, val y: Double)

仅仅一行代码,Kotlin 就会创建出一个完整的数据类,并自动生成相应的 equals、hashcode、toString 方法。是不是早就受够了 getter和setter?反正我是受够了。

3.2 空安全与属性代理

第一次见到空类型安全的设计是在 Swift 当中,那时候还觉得这个东西有点儿意思哈,一旦要求变量不能为空以后,因它而导致的空指针异常的可能性就直接没有了。想想每次 QA 提的 bug 吧,说少了都得有三分之一是空指针吧。

Kotlin 的空安全设计,主要是在类型后面加?表示可空,否则就不能为 null。

val anInt: Int = null // 错误  val anotherInt: Int? = null // 正确

使用时,则:

blob.png

而对于 Java 代码,比如我们在覆写 Activity 的 onCreate 方法时,有个参数 savedInstanceState:

override fun onCreate(savedInstanceState: Bundle!)

这表示编译器不再强制 savedInstanceState 是否可 null,开发者在覆写时可以自己决定是否可 null。当然,对于本例,onCreate 的参数是可能为 null 的,因此覆写以后的方法应为:

override fun onCreate(savedInstanceState: Bundle?)

通常来讲,教科书式的讲法,到这里就该结束了。然而直到我真正用 Kotlin 开始写代码时,发现,有些需求实现起来真的有些奇怪。

还是举个例子,我需要在 Activity 当中创建一个 View 的引用,通常我们在 Java 代码中这么写:

blob.png

在 Kotlin 当中呢?

blob.png

每次用 aTextView 都要加俩!,不然编译器不能确定它究竟是不是 null,于是不让你使用。。这尼玛。。。到底是为了方便还是为了麻烦??

所以后来我又决定这么写:

blob.png

这可如何是好??

其实 Kotlin 肯定是有办法解决这个问题哒!比如上面的场景,我们这么写就可以咯:

blob.png

lazy 是 Kotlin 的属性代理的一个实例,它提供了延迟加载的机制。换句话说,这里的 lazy 提供了初始化 aTextView 的方法,不过真正初始化这个动作发生的时机却是在 aTextView 第一次被使用时了。lazy 默认是线程安全的,你当然也可以关掉这个配置,只需要加个参数即可:

blob.png

好,这时候肯定有人要扔西红柿过来了(再扔点儿鸡蛋呗),你这 lazy 只能初始化 val 啊,万一我要定义一个 var 成员,又需要延迟初始化,关键还不为 null,怎么办??

blob.png

lateinit 的使用还是有很多限制的,比如只能在不可 null 的对象上使用,比须为var,不能为 primitives(Int、Float之类)等等,不过这样逼迫你一定要初始化这个变量的做法,确实能减少我们在开发中的遗漏,从而提高开发效率。

至于 lazy 技术,实际上是 Delegate Properties 的一个应用,也就是属性代理了。在 Kotlin 当中,声明成员属性,除了直接赋值,还可以用 Delegate 的方式来声明,这个 Delegate 需要根据成员的类型(val 或者 var)来提供相应的 getValue 和 setValue 方法,比如一个可读写的 Delegate,需要提供下面的方法:

blob.png

好嘴皮不如来个栗子,下面我们就看一个自定义 Delegate,用来访问 SharedPreference:

blob.png

需要说明的是,这段代码是我从《Kotlin for Android Developer》的示例中摘出来的。有了这个 Delegate 类,我们就可以完全不需要关心 SharedPreference了,下面给出使用的示例代码:

blob.png

于是我们再也不需要重复写那些 getSharedPreference,也不用 edit、commit,再见那些 edit 之后忘了 commit 的日子。有没有觉得非常赞!

3.3 扩展类

扩展类,就是在现有类的基础上,添加一些属性或者方法,当然扩展的这些成员需要导入当前扩展成员所在的包才可以访问到。下面给出一个例子:

blob.png

我们已经介绍过 data class,Coordinate 有两个成员分别是 x 和 y,我们知道通常表示一个二维平面,有这俩够了;然而我们在图形学当中经常会需要求得其极坐标,所以我们扩展了 Coordinate,增加了一个属性 theta 表示角度(反正切的值域为 -π/2 ~ π/2,所以这个式子不适用于二三象限,不过这不是重点了),增加了一个 R 方法来获得点的半径,于是我们在 main 方法中就可以这么用:

blob.png

那么这个扩展有什么限制呢?

  • 在扩展成员当中,只能访问被扩展类在当前作用域内可见的成员,本例中的x 和 y 都是 public 的(Kotlin 默认 public,这个我们后面会提到),所以可以在扩展方法和属性中直接访问。

  • 扩展成员与被扩展类的内部成员名称相同时,扩展成员将无法被访问到

好的,基本知识就是这些了,下面我们再给出一个实际的例子。

通常我们在 Java 中会自定义一些 LogUtils 类来打日志,或者直接用 android.util.log 来输出日志,不知道大家是什么感受,我反正每次因为要输入 Log.d 还要输入个 tag 简直烦的要死,而且有时候恰好这个类还没有 tag 这个成员,实践中我们通常会把当前类名作为 TAG,但每个类都要做这么个工作,是在是没有什么趣味可言(之前我是用 LiveTemplates 帮我的,即便如此也没有那种流畅的感觉)。

有了 Kotlin 的这个扩展功能,日子就会好过得多了,下面我创建的一个打日志的方法:

blob.png

有了这个方法,你可以在任何类的方法体中直接写:

debug(whatever)

然后就会输出以这个类名为 TAG 的日志。

嗯,这里需要简单介绍 Kotlin 在泛型中的一个比较重要的增强,这个在 Java 中无论如何也是做不到的:inline、reified。我们再来回头看一下 debug 这个方法,我们发现它可以通过泛型参数 T 来获取到T的具体类型,并且拿到它的类名――当然,如果你愿意,你甚至可以调用它的构造方法来构造一个对象出来――为什么 Kotlin 可以做到呢?因为这段代码是 inline 的,最终编译时是要编译到调用它的代码块中,这时候T的类型实际上是确定的,因而 Kotlin 通过 reified 这个关键字告诉编译器,T 这个参数可不只是个摆设,我要把它当实际类型来用呢。

为了让大家印象深刻,我下面给出类似功能的 Java 的代码实现:

blob.png

而你如果说希望在 Java 中也希望像下面这样拿到这个泛型参数的类型,是不可以的:

blob.png

就算我们在调用处会写道 debug < Date >(“blabla”),但这个 Date 在编译之后还是会被擦除。

3.4 函数式支持(Lambdas)

Java 8 已经开始可以支持 Lambda 表达式了,这种东西对于 Java 这样一个『根红苗正』的面向对象编程语言来说还真是显得不自然,不过对于 Kotlin 来说,就没那么多顾忌了。

通常我们需要执行一段异步的代码,我们会构造一个 Runnable 对象,然后交给 executor,比如这段 java 代码:

blob.png

用 Kotlin 怎么写呢?

executor.submit({    //todo})

一下子省了很多代码。

那么实际当中我们可能更常见到下面的例子,这是一段很常见的 Java 代码,在  Android 的 UI 初始化会见到:

blob.png

那么我们用 Kotlin 怎么写呢?

textView.setOnClickListener{ /*todo*/ }  handler.post{ /*todo*/ }

在 Anko 这个 Android 库的帮助下,我们甚至可以继续简化 OnClickListener 的设置方式:

textView.onClick{ /*todo*/ }

当然,好玩的不止这些,如果结合上一节我们提到的扩展方法,我们就很容易看到 Kotlin 的标准库提供的类似 with 和 apply 这样的方法是怎么工作的了:

blob.png

我们通常会在某个方法体内创建一个对象并返回它,可我们除了调用它的构造方法之外还需要做一些其他的操作,于是就要创建一个局部变量。。。有了 apply 这个扩展方法,我们就可以这么写:

blob.png

这样返回的 StringBuilder 对象实际上是包 "whatever" 这个字符串的。

至于说 Kotlin 对于 RxJava 的友好性,使得我突然有点儿相信缘分这种东西了:

blob.png

3.5 Pattern Matching

记得之前在浏览 Scala 的特性时,看到:

object HelloScala{    // do something  }

觉得很新鲜,这时候有个朋友不屑的说了句,Scala 的模式匹配才真正犀利――Kotlin 当中也有这样的特性,我们下面就来看个例子:

blob.png

咋一看感觉 when 表达式就是一个增强版的 switch――Java 7 以前的 switch 实际上支持的类型非常有限,Java 7 当中增加的对 String 的支持也是基于 int 类型的――我们可以看到 when 不再像 switch 那样只匹配一个数值,它的子式可以是各种返回 Boolean 的表达式。

when 表达式还有一种写法更革命:

blob.png

只要是返回 Boolean 的表达式就可以作为 when 的子式,这样 when 表达式的灵活性可见一斑。当然,与 Scala 相比,Kotlin 还是要保守一些的,下面给出一个 Scala 类似的例子,大家感受一下,这实际上也可以体现出 Kotlin 在增加 Java 的同时也尽量保持简单的设计哲学(大家都知道,毕竟 Scala 需要智商o(�s□�t)o)。

blob.png

运行结果如下:

a tuple with : 1 , 3  [I@2d554825  3.0, 4.0

3.6 如果你是一个SDK开发者

我曾经做过一段时间的 SDK 开发,SDK 的内部有很多类其实是需要互相有访问权限的,但一旦类及其成员是 public 的,那么调用方也就可以看到它们了;而 protected 或者 default 这样的可见性对于子包却是不可见的。

用了这么久 Java,这简直是我唯一强烈感到不满的地方了,甚至于我突然明白了 C++ 的 friend 是多么的有用。

Kotlin 虽然没有提供对于子包可见的修饰符,不过它提供了i nternal:即模块内可见。换句话说,internal 在模块内相当于 public,而对于模块外就是 private 了――于是乎我们如果开发 SDK,那么可以减少 api 层的编写,那些用户不可见的部分直接用 internal 岂不更好。当然有人会说我们应当有 proguard 做混淆,我想说的是,proguard 自然是要用到的,不过那是 SDK 这个产品加工的下一个环节了,我们为什么不能在代码级别把这个事情做好呢?

关于Kotlin的默认可见性究竟是哪个还有人做出过讨论,有兴趣的可以参考这里:Kotlin’s default visibility should be internal (https://discuss.kotlinlang.org/t/kotlins-default-visibility-should-be-internal/1400)。

3.7 DSL

其实我们对 DSL 肯定不会陌生,gradle 的脚本就是基于 groovy 的 DSL,而 Kotlin 的函数特性显然也是可以支持 DSL 的。比如,我们最终要生成下面的 xml 数据:

blob.png

我们可以构建下面的类:

blob.png

blob.png

我们看到在 main 方法当中,我们用 Kotlin 定义的 dsl 写出了一个 Project 对象,它有这与 xml 描述的一致的结构和含义,如果你愿意,可以构造相应的方法来输出这样的 xml,运行之后的结果:

blob.png

当然,这个例子做的足够的简陋,如果你有兴趣也可以抽象出 "Element",并为之添加 "Attributes",实际上这也不是很难。

3.7 Kotlin与Android的另一些有趣的东西

写了很多代码,却发现它们干不了多少事情,终究还是会苦恼的。比如我一直比较痛苦的一件事儿就是:

Button button = (Button) findViewById(R.id.btn);

如果我需要很多个按钮和图片,那么我们要写一大片这样的 findViewById。。妈呀。。。这活我干不了啦。。

不过用 Kotlin 的 Android 扩展插件,我们就可以这样:

先上布局文件:

main.xml

blob.png

在 Activity 中:

blob.png

注意到:

import kotlinx.android.synthetic.main.load_activity.*

导入这一句之后,我们就可以直接在代码中使用 start、textView,他们分别对应于 main.xml 中的 id 为 start 的按钮和 id 为 textView 的 TextView。

于是你就发现你再也不用 findViewById 了,多么愉快的一件事!!!当然,你还会发现 Toast 的调用也变得简单了,那其实就是一个扩展方法 toast();而 startActivity 呢,其实就是一个 inline加reified 的应用――这我们前面都提到过了。

还有一个恶心的东西就是 UI 线程和非 UI 线程的切换问题。也许你会用 handler 不断的 post,不过说真的,用 handler 的时候难道你不颤抖么,那可是一个很容易内存泄露的魔鬼呀~哈哈,好吧其实我不是说这个,主要是用 handler 写出来的代码 实在 太 丑 了 !!

原来在 Java 当中,我们这么写:

blob.png

而在 Kotlin 当中呢,我们只需要这么写:

blob.png

自己感受一下吧。

下面我们再来提一个有意思的东西,我们从做 Android 开发一开始就要编写 xml,印象中这个对于我来说真的是一件痛苦的事情,因为它的工作机制并不如代码那样直接(以至于我现在很多时候居然喜欢用 Java 代码直接写布局)――当然,最主要的问题并不是这个,而是解析 xml 需要耗费 CPU。Kotlin 有办法可以解决这个问题,那就是 DSL 了。下面给出一个例子:

blob.png

一个 LinearLayou t包含了一个 Button,这段代码你可以直接写到你的代码中灵活复用,就像这样:

blob.png

这样做的好处真是不少:

  • 比起 xml 的繁琐来,这真是要清爽很多

  • 布局本身也是代码,可以灵活复用

  • 再也不用 findViewById 了,难道你不觉得在这个上面浪费的生命已经足够多吗

  • 事件监听很方便的嵌到布局当中

  • DSL 方式的布局没有运行时的解析的负担,你的逻辑代码怎么运行它就怎么运行

  • Anko还增加了更多好玩的特性,有兴趣的可以参考:Anko@Github (https://github.com/Kotlin/anko)

3.8 方法数之痛

我曾经尝试用 Scala 写了个 Android 的 HelloWorld,一切都配置好以后,仅仅引入了 Scala 常见的几个库,加上 support-v4 以及 appcompat 这样常见的库,结果还是报错了。是的,65K。。。而且用 Scala 开发 Android 的话,基于 gradle 的构建会让整个 app 的 build 过程异常漫长,有时候你会觉得自己悟出了广义相对论的奥义,哦不,你一定是晕了,时间并没有变慢。

相比之下,Kotlin 的标准库只有 7000 个方法,比 support-v4 还要小,这正反映了 Kotlin 的设计理念:100% interoperable with Java。其实我们之前就提到,Java 有的 Kotlin 就直接拿来用,而 Scala 的标准库要有 5W 多个方法,想想就还是想想算了。

4、小结

目前 Kotlin 1.0 已经 release,尽管像 0xffffffff 识别成 Long 类型这样的 bug 仍然没有解详情 (https://youtrack.jetbrains.com/oauth?state=%2Fissue%2FKT-4749):

val int: Int = 0xffffffff // error  val anotherInt: Int = 0xffffffff.toInt() // correct

不过,Kotlin 的教学资源和社区建设也已经相对成熟,按照官方的说法,Kotlin可以作为生产工具投入开发,详情可以参考:Kotlin 1.0 Released: Pragmatic Language for JVM and Android (http://blog.jetbrains.com/kotlin/2016/02/kotlin-1-0-released-pragmatic-language-for-jvm-and-android/)。

敢于吃螃蟹,多少有些浪漫主义色彩,我们这些程序员多少可以有些浪漫主义特质,不过在生成环境中,稳定高于一切仍然是不二法则。追求新技术,一方面会给团队带来开发和维护上的学习成本,另一方面也要承担未来某些情况下因为对新技术不熟悉而产生未知问题的风险――老板们最怕风险了~~

基于这一点,毫无疑问,Kotlin 可以作为小工具、测试用例等的开发工具,这是考虑到这些代码通常体量较小,维护人数较少较集中,对项目整体的影响也较小;而对于核心代码,则视情况而定吧。

就我个人而言,长期下去,Kotlin 很大可能会成为我的主要语言,短期内则仍然采用温和的改革方式慢慢将Kotlin 渗透进来。

一句话,Kotlin 是用来提升效率的,如果在你的场景中它做不到,甚至成了拖累,请放开它。

如果您觉得我们的内容还不错,就转发到朋友圈,和小伙伴一起分享吧~