在Google I/O 2017中,Google 宣布 Kotlin 成为 Android 官方开发语言。
在Google I/O 2019中,Google 宣布,Kotlin 编程语言现在是 Android 应用程序开发人员的首选语言,“Android 的开发将越来越以 Kotlin 为先。” 许多新的 Jetpack API 和特性将首先在 Kotlin 中提供。
Kotlin将成为Android开发程序员的必修课。
本文既不是全面系统的学习手册,也不是对Kotlin的简单介绍,而是讲解作为一个Android程序员,想要真正快速进入Kotlin开发,所必须学习掌握的知识要点,也可以当作是入门速查手册。
如果你是一名安卓开发的Java程序员,想快速上手转Kotlin开发,那么本文就是为你而准备的了。
如果你对Kotlin已经有一定的了解,只想直接查看如何不使用接口回调而得到异步函数执行的结果,请直接跳到 协程实战 一节。
简洁: 大大减少样板代码的数量。安全: 避免空指针异常等错误。互操作性: 充分利用 JVM、Android 的现有库。可以跟Java类互相访问,几乎没有桥接成本。
Android Studio 从 3.0(preview)版本开始将内置 Kotlin 插件。
如果你的环境还没有安装Kotlin,那么打开 Settings ( Mac 为 Preferences) 面板,在右侧找到 Plugins 选项 ,搜索框输入 "Kotlin" 查找,点击 Search in repositories(仓库中搜索),然后安装即可,安装完成之后需要重启 Android Studio。
新建工程,在工程向导中选择Kotlin语言,然后就可以直接开始Kotlin开发了。
如果你想在现有Java工程中添加Kotlin支持,也将非常简单,步骤如下: 1. 选择Project视图 2. 打开工程外层build.gradle,在buildscript中添加:
ext
在dependencies中添加:
classpath
apply
在dependencies中添加:
implementation
如图所示,Kotlin环境便成功集成进工程了。
Java工程集成Kotlin后,安装包将增大大约200~500K。
Kotlin的最大优势就是减少样板代码量,现在先来直观感受一下如何直接访问layout xml中的元素: 1. 新建一个Kotlin工程后; 2. 在content_main.xml添加一个TextView控件,并将控件id修改为textView; 3. 在MainActivity中,添加如下代码:
textView
注意以上代码,跟Java代码差不太多,Kotlin的每行代码结束不必使用分号,将几行代码写到一行中则需要用分号分隔开。 在Android Studio IDE中,会被提示该行代码错误,使用Alt+Enter,将自动添加:
import
然后刚才的代码就可以编译运行了。 不再需要使用 findViewById(http://R.id.xxx),感觉是不是轻松多了。如果你使用了xml中未定义的id来访问,将无法通过编译。
单方法接口只需要写一个闭包就行了:
textView
val
数据类型可以省略,Kotlin会自动识别类型
val
var
Java中的颜色:
int
粘贴到Kotlin会被转换为:
val
如果不转换:
var
可以使用强制转换
var
注意Java的强制转换 (int)a,在Kotlin中需要使用 a.toInt() 。
Java中的 TextView v = (TextView)aView; Kotlin中需要这样:
var
fun
函数必须使用 fun 关键字来修饰。 如果函数需要返回值,则在括号后面添加冒号,然后声明返回值类型。 Kotlin没有三元操作符,可以使用if/else来替代。
fun
函数需要使用fun关键字修饰,函数的参数声明里面,不可以使用var来修饰,因为Kotlin不允许函数体内修改参数的值。 有默认值的参数,在调用的时候可以省略,使用命名参数方式来调用函数时,参数顺序可以任意调整。因此函数不需要多态,编写一份函数就够灵活使用了。
上面的示例代码中参数类型出现了问号❓,问号的作用是告诉编译器,该变量可以为空。 指针变量默认不可以为空
var
函数参数默认不允许为空,因此这里不会出现空指针崩溃,无需做空指针检测
fun
指针变量加上问号再访问属性方法,其实就是编译器帮我们添加了if/else代码,如果textView为空,就不会去访问textView的setTextColor方法,从而避免空指针崩溃。因此有人说,kotlin不再会出现空指针崩溃。
当然了,有时候,可能你就是希望当指针为空时,抛出异常,而不要掩盖问题原因,那么你可以这样:
fun
!! 的作用是告诉编译器,这个指针不允许为空,如果遇到空指针,则直接抛出异常。
class
主构造函数已经内联到class声明中了,它会自动调用init方法,init方法不能显示调用。第二个构造函数其实有些多余,因为主构造函数的name参数其实可以写成 name: String? = null,那么第二个构造函数就没有存在的价值了,当然一旦写了,它就会优先,当我们调用如下代码
var
该行代码会执行第二个构造函数,删除第二个构造函数则会执行主构造函数。当参数很多的时候,多个构造函数会有一定的用处。 继承关系直接使用冒号即可,不可以多重继承 构造函数一般不编写具体代码,初始化工作应该全部放在init函数去做 类成员变量和函数默认是public的,需要隐藏则添加private关键字 setter和getter方法,不必显示编写,编译器会自动生成 初始化函数和构造函数不需要 fun 关键字 一个文件可以定义多个类
class
等同于
class
主构造函数的constructor可以省略。
既然Kotlin的就是为了减少样板代码量的,那么实现多重接口的意义就不大,回调都可以使用代码block来实现。保留多重接口的支持更多可能是为了Java代码能直接转换为kotlin代码吧,毕竟kotlin的代码形式基本可以全部兼容Java代码。
interface
private
这种方式的回调接口,基本上跟Java是个相似的。
Kotlin提供lambda表达式语法来精简代码量,当回调接口只有一个方法的时候,就可以使用这种方式来简化代码:
class
当我们在Kotlin中调用Java的点击事件监听的时候,就使用了lambda的方式:
fab
既然Kotlin的就是为了减少样板代码量,那么就不应该再定义有很多方法的接口,尽量将接口拆分到多个接口,一个接口只有一个方法。这样接口的实现方就再也不会出现很多空方法的窘境了,再加上使用协程来处理异步耗时操作,不在异步中定义回调接口,Kotlin的优势才能得到最大发挥。
fun
var
Array,数组,长度不可变,但内容可以修改,例如如下:
var
数组的遍历方法:
val
ArrayList,可变,内容和长度都可以修改:
var
val
输出结果:
{
mapOf 返回一个Map对象,只读,如果要便捷构造一个可修改的Map,请使用HashMap:
val
输出结果:
{
Map的遍历:
map
还可以这样:
for
for
头尾都是闭区间。
没有三元操作符,但可以用if/else代替
val
when(强大的switch替代品)
when
不需要break,多个条件也可以合并到一起:
when
fun
fun
@Synchronized 为Kotlin的函数同步锁注解,并非关键字。 如果在代码中使用同步锁,那么需要使用 synchronized 函数:
fun
如何转到主线程执行代码:
runOnUiThread
协程 - 轻量级线程
虽然Kotlin中使用线程已经很方便了,但还是推荐使用协程代替线程。
协程主要是让原来要使用“异步+回调方式”写出来的复杂代码, 简化成可以用看似同步的方式写出来(对线程的操作进一步抽象)。 这样我们就可以按串行的思维模型去组织原本分散在不同上下文中的代码逻辑,而不需要去处理复杂的状态同步问题,基本上也不再需要接口处理代码了。
先来看看如下代码:
fun
输出结果:
###
GlobalScope.launch(Dispatchers.Default) 用于启动协程。 从输出结果可以看出,启动协程之前,是在主线程中,但是协程启动后,协程的代码Block是在子线程中执行的。这不是重点,重点在于delay过后,协程的代码一定是在子线程执行的,哪怕launch指定了Unconfined参数,协程一开始将在主线程中执行,但是delay依然不会阻塞主线程,但它的确可以在指定的时间过后返回代码块继续执行后面的代码。这就是delay的强大之处,这个delay是不可以在协程外部的代码中调用的。
| 协程调度器 | 功能描述 |
| ---------- | ---- | | Dispatchers.Default | 运行在 Dispatchers.Default 的线程池中 |
| Dispatchers.Main | 运行在主线程中 |
| http://Dispatchers.IO | 运行在 IO 线程中 |
| Dispatchers.Unconfined | 运行在当前线程中 |
PS:之前低版本的那套launch/await 全局函数已经废弃,新版本必须使用http://GlobalScope.xxx。
协程的作用,就是让开发者感觉是在多线程中工作一样,可以异步处理耗时操作,但实际上可能并没有真正使用线程,而就在同一线程中切换。协程的切换是由编译器来完成的,因而开销很小,并不依赖系统资源,你可以开100000个协程,而无法启动100000个线程。
delay跟线程的sleep很相似,都是延时一段时间,但是不同点在于,delay不会阻塞当前线程,而是挂起协程本身,从而将线程资源释放出来,供其它协程使用。
我们所必须要了解的是,在协程中,当你的耗时任务做完之后,你的代码很可能不在刚才的线程当中,此时必须要注意代码的线程安全问题,例如访问UI,你可以使用runOnUiThread { }。
在startCoroutine的结尾处,可以使用c1.join()来等待协程结束,一旦使用join,编译器便提醒必须添加suspend关键字,该函数也必须在协程中调用。
再来看看修改后的代码:
suspend
该方法因为添加了suspend关键字,因此只能在协程中调用:
GlobalScope
输出结果如下:
###
可以看到,代码中的日志顺序,是按1、2、3、4的顺序输出的了,join函数会等待协程结束。由于我指定了startCoroutine在Dispatchers.Main父协程中运行,因此当join等待子协程完成之后,又回到了主线程执行,这种方式来更新UI的话,都不再需要使用runOnUiThread了,很适合用于做动画。
我们通过一个网络URL加载Web数据的实例,来展示协程对于异步处理的强大之处。
首先,需要在build.gradle中添加:
implementation
新建一个UrlDownload类:
class
执行以上程序,在主线程调用startDownload()函数,可以看到控制台打印出了网页内容。请注意整个程序没有定义任何回调接口,但结果的确是在业务层打印出来的,阅读代码就好像是同步执行的一样,你也可以看的出,以上代码并不会阻塞主线程。 download(url: String)是一个同步方法,实现联网返回网页数据的功能,该方法会阻塞当前线程,不能在主线程调用。 asyncDownload方法添加了suspend关键字,说明该函数将被挂起并异步执行,等到异步执行完毕才会返回结果。 * suspend关键字声明的函数,只能在协程里面调用。
如果需要一层一层的往上传递,那么将startDownload做个简单改造即可:
suspend
GlobalScope.launch 启动一个协程,并返回这个协程对象,我们可以调用 join()来**等待**协程结束,join没有返回值。
而 **await()** 则有返回值,可以返回数据,要使用await(),必须使用GlobalScope.async来启动协程。再来看看上述启动代码的学习修改版本:
suspend
输出结果如下:
###
从日志可以看出,虽然日志顺序也是严格按照代码中1、2、3、4的顺序执行的,但是4号日志跟1号日志已经不在同一个线程,而是跟3号日志在同一个线程。这就是异步等待await的结果,所以该方法必须使用suspend关键字,告诉编译器这个是协程函数,必须在协程中调用。不然随意切换客户代码的线程,肯定要出乱子的。这就是协程的关键,也是协程的强大之处,但是越是强大的东西,使用时一定要知道它的特点,虽然使用起来很简单。
刚才的代码,有一个费解的地方:
suspend
那么细心的同学可能会问,如果我在这里写了两行代码呢?既然是实战学习,当然不能放过这个问题,继续编写学习测试代码:
suspend
测试发现,await会将最后一个表达式的值作为返回值,而前面的多个asyncDownload都会执行,而且是顺序执行,原因是asyncDownload内部本身也使用了await()来等待。至此,相信读者对于协程的概念、使用都能很好的理解了,测试代码就不再贴出来了,有兴趣的同学可以自行编写代码来验证,以加深理解。
相信很多同学都写过类似如下代码:
interface
Kotlin的解决方案:
interface
一个by关键字就将接口委托给另一个对象处理,而不必编写那些样板代码。