Using Kotlin for Android Development

Preface

本文主要记录了我在学习并使用 Kotlin 参与开发我司应用花瓣的过程中总结出来的心得体会,顺带也想安利一波 Kotlin


Kotlin

Statically typed programming language for the JVM, Android and the browser. (100% interoperable with Java™)

Kotlin 是由 JetBrains 在 2010 年推出的基于 JVM 的新编程语言,作为一个跨平台的语言,Kotlin 可以应用于任何 Java 的工作环境:服务器端的应用,移动应用(Android 版),桌面应用程序。

Advantage

  1. Concise to reduce the amount of boilerplate code you need to write.
  2. Expressive to make your code more readable and understandable.
  3. Safe to avoid entire classes of errors such as null pointer exceptions.
  4. Versatile for building server-side applications, Android apps or frontend code running in the browser.
  5. Interoperable to leverage existing frameworks and libraries of the JVM with 100 percent Java interoperability.

如果你之前对 Kotlin 并不了解,那么当你看到 Kotlin 的这些特性之后,现在是不是已经抑制不住内心的小激动了呢?

Talk is cheap. Show me the code. 下面将通过代码来直观的感受下 Kotlin 是如何提升开发者的幸福感的。

Concise

举个例子,在使用 Java 开发时,我们定义一个 Artist 数据类,我们需要去编写(至少生成)这些代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class Artist {
private long id;
private String name;
private String url;
private String mbid;

public long getId() {
return id;
}

public void setId(long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getUrl() {
return url;
}

public void setUrl(String url) {
this.url = url;
}

public String getMbid() {
return mbid;
}

public void setMbid(String mbid) {
this.mbid = mbid;
}

@Override public String toString() {
return "Artist{" +
"id=" + id +
", name='" + name + '\'' +
", url='" + url + '\'' +
", mbid='" + mbid + '\'' +
'}';
}
}

而使用 Kotlin ,我们只需编写如下代码:

1
2
3
4
5
data class Artist(
var id: Long,
var name: String,
var url: String,
var mbid: String)

Kotlin 将自动帮我们生成 gettersettertoStringequalshashCode 和其他方法,如 copy()

Null Safety

在使用 Java 开发的时候,我们的代码大多是防御性的。如果我们不想遇到 NullPointerException ,我们就需要在使用它之前不停地去判断它是否为 null 。如很多现代的语言,Kotlin空安全的,因为我们需要通过一个安全调用操作符(?)来明确地指定一个对象是否能为空。

Kotlin 中,所有变量在默认情况下都被视为不可为空,因此尝试为变量分配空值将导致编译错误。如果你想明确声明一个变量可以接受一个空值,那么你需要附加一个 操作符到变量类型:

1
2
3
4
5
// 此时并不能通过编译,Artist 不可以为 null
var notNullArtist: Artist = null

// 现在,Artist 可以为 null
var artist: Artist? = null

The Safe Call Operator \ The Elvis Operator \ The !! Operator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 无法编译,artist 可能为 null,我们需要进行处理
artist.print()

// 只要在 artist != null 时才会打印
artist?.print()

// 智能转换,如果我们在之前进行了空检查,则不需要使用安全调用操作符调用
if (artist != null) {
artist.print()
}

// 只有在确保 artist 不为 null 的情况下才能这么调用,否则它会抛出异常
artist!!.print()

// 使用 Elvis 操作符来给定一个在是 null 的情况下的替代值
val name = artist?.name ?: "empty"

Higher-Order functions

很多语言已经支持了高阶函数,比如 Java 8 ,但是你并不能用上 Java 8 。如果你在用 Java 6 或者 Java 7 ,下面的例子实现了一个具有过滤功能的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface Function<T, R> {
R call(T t);
}

public static <T> List<T> filter(Collection<T> items, Function<T, Boolean> f) {
final List<T> filtered = new ArrayList<T>();
for (T item : items) if (f.call(item)) filtered.add(item);
return filtered;
}

filter(numbers, new Function<Integer, Boolean>() {
@Override
public Boolean call(Integer value) {
return value % 2 == 0;
}
});

我们首先要声明一个函数接口,接受参数类型为 T ,返回类型为 R 。我们用接口中的方法遍历操作了目标集合,创建了一个新的列表,把符合条件的过滤了出来。

1
2
3
4
5
fun <T> filter(items: Collection<T>, f: (T) -> Boolean): List<T> {
val filtered = arrayListOf<T>()
for (item in items) if (f(item)) filtered.add(item)
return filtered
}

上面的代码是在 Kotlin 下的实现,是不是简单很多?我们调用的时候如下: kotlin filter(numbers, { value -> value % 2 == 0 })

你可能也发现了,我们没有定义任何的函数接口,这是因为在 Kotlin 中,函数也是一种数据类型。看到 f:(T) -> Boolean 这个语句了吗?这就是函数类型作为参数的写法,f 是函数别名,T 是函数接受参数,Boolean 是这个函数的返回值。定义完成后,我们随后就能跟调用其他函数一样调用 f 。调用 filter 的时候,我们是用 lambda 表达式来传入过滤函数的,即:{value -> value % 2 = 0}

Lambdas

同样,在 Java 8 前,由于缺少了对 lambdas 的支持,所以编写监听和回调事件是件非常繁琐的事情。而 Kotlin 有着非常好的 lambdas 支持。

我们来看一个常见的例子:将一个点击监听器添加到一个按钮。在 Java 7 及更早版本中,通常需要以下代码:

1
2
3
4
5
6
7
button.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
Toast.makeText(this, "Button clicked", Toast.LENGTH_LONG).show();
}
});

而然,使用 Kotlin 只需一行代码:

1
button.setOnClickListener({ view -> toast("Button clicked") })

真是简单且易读。

Extension Functions

在使用 Java 开发的时候,我们经常会写一大堆的 Utils 类,以满足不同使用场景的需求,而 Kotlin 提供了 Extension Functions 这一特性允许我们可以给任何已存在的类添加方法,包括在 Java 中的一些基本类型。毫不夸张,Extension Functions 绝对算得上 Kotlin 最强大的特性之一。

下面是一个工具函数,判断传入的字符串是否为电话号码格式,它接受一个字符串参数:

1
2
3
fun isPhoneNumber(phoneNumber: String): Boolean {
return phoneNumber.isNotBlank() && Pattern.matches("^1\\d{10}$", phoneNumber)
}

使用 Extension Functions 特性,就能给已存在的类(String)添加方法,写法如下:

1
2
3
fun String.isPhoneNumber() : Boolean {
return this.isNotBlank() && Pattern.matches("^1\\d{10}$", this)
}

下面就是我们的调用方法,我们可以直接在字符串类型上调用这个方法:

1
"18612345678".isPhoneNumber()

或许有人会问了,那既然可以给已存在的类添加方法,那是否可以添加属性(成员)呢?答案是肯定的!

关于上述问题的阐述,请参见:Kotlin的黑魔法

Anko

Anko is a library which makes Android application development faster and easier. It makes your code clean and easy to read, and lets you forget about rough edges of Android SDK for Java.

Anko 大大简化了工作视图、线程和 Android 生命周期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
val act = this
val layout = LinearLayout(act)
layout.orientation = LinearLayout.VERTICAL
val name = EditText(act)
val button = Button(act)
button.text = "Say Hello"
button.setOnClickListener {
Toast.makeText(act, "Hello, ${name.text}!",Toast.LENGTH_SHORT).show()
}
layout.addView(name)
layout.addView(button)


verticalLayout{
val name = editText()
button("Say Hello") {
onClick { toast("Hello, ${name.text}!") }
}
}

Tips

Kotlin Android Extensions

Kotlin Android Extensions 本质上是一个视图绑定,使得开发者在代码中通过 id 就可以使用 XML 文件中定义的 View 。它将自动为 View 创建属性值,而不用使用第三方注解框架或者 findViewById 函数。

例如,我们有 activity_main.xml 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="me.itangqi.practicekotlin.MainActivity">


<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"/>


</android.support.constraint.ConstraintLayout>

如果我们想在 MainActivity 中使用 activity_main.xml 里面定义的 TextView ,你只需要导入:

1
import kotlinx.android.synthetic.main.activity_main.*

之后便可以直接引用了:

1
2
3
4
5
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView.text = "I´m a welcome text!!"
}

但是,Kotlin Android Extensions 在给我们带来开发上的快感的同时,也带来了隐患(我是真的被坑过…),那就是:只要你 import kotlinx.android.synthetic.main.xxx ,你便可以在当前 Activity \ Fragment,使用任何 xml 中声明的 View,而这也许并不是你想要的 ,例如,我们有一个 activity_second.xml ,其中定义了一个 Button

1
2
3
4
<Button
android:id="@+id/actionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

只要我们导入了:

1
import kotlinx.android.synthetic.main.activity_second.*

我们便可在 MainActivity 中使用不属于 activity_main.xml 中的 Button

1
2
3
4
5
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
actionButton.text = "Click Me"
}

此时可以正常通过编译,并不会报错,直到运行到该页面时 (>_<。)

因为 Android Studio 的智能提示,你需要在开发时格外注意,不要引入不属于本页面的 View

此时,多个 xml 中可能都声明了 idtextViewTextView,一定要注意了。

minSdkVersion API

这又是一个实实在在踩过的坑,由于 AS 之前的版本(<2.3),Lint 并没有提示,我直接调用了高于 minSdkVersion 的 API ,结果当然惨不忍睹…

不过,当我现在试图复现这个问题时,发现最新版 2.3 已经有提示了:

上述两个问题,其实严格上说,并不是 Kotlin 或者 Android Studio 的锅,因为通过问题反应出的,更多的是,要求我们在平日的开发工作中,提高编程时的规范,并时刻注重细节。


Reference

  1. Kotlin Reference
  2. Coding Functional Android Apps in Kotlin: Lambdas, Null Safety & More
  3. Kotlin: A New Hope in a Java 6 Wasteland
  4. Learn Kotlin with Keddit
  5. Kotlin的黑魔法
我知道是不会有人点的,但万一有人想不开呢?