Kotlin 遇上 Android

Andrew Chen
靠der Cowder
Published in
10 min readSep 24, 2015

卡特琳

Jetbrain 發明的語言來開發 Intellij/Android-Studio 。

可轉譯 jvm 與 js 。類似 swift, scala 語言。

特點:

  • null-safety / Optional 最佳解決方案. computer?.getSoundcard()?.getUSB()?.getVersion() ?: “UNKNOWN”
  • 語法簡潔 for ((key, value) in map)
  • AutoValue
  • lambdas setOnClickListener { finish() }

Kotlin 開始知名的時候,大概可以追溯到 2014 年中旬登上 Android 開發週報的:http://blog.gouline.net/2014/08/31/kotlin-the-swift-of-android/ ,剛開始看到是覺得確實很簡潔,但是對於成熟度抱著遲疑得態度。

在這之後,筆者是在 2015 年一月份 Jake Wharton 在 G+ 發表了一篇貼文之後,確實很多人跟筆者一樣,較為積極的看待這個語言。

除了這些特性之外,對於 android 來說,滿大的優勢在於 symbol size 以及 code size 相較於其他語言,十分羽量。

  • kotlin: 6k~
  • scala: 50k~

導入 Android 考量點:

  • 編譯時間增幅不大 (1.0.2+)
  • 十分羽量

其他進階特點:

  • 高階函式 + 擴充函式 (function extensions)
  • inline
  • infix

宣告

變數宣告

Before (Java):

String name = "Andrew Chen";

After:

var name: String = "Andrew Chen"

可以簡化成

var name = "Andrew Chen"

因為這邊可以透過初始化推斷(infer) 型別,所以就不用再宣告型別了。

也不用分號 “;” 。

Immutable

final var = val

Before:

final List<String> names = Arrays.asList("Andrew Chen");names = Collections.emptyList(); // compile-error

After:

val names = listOf("Andrew Chen")
names = Collections.emptyList() // compile-error

Before:

final List<String> names = new ArrayList<>(Arrays.asList("Andrew"));

After:

val names = mutableListOf("Andrew")

Null Safety 安全檢查

Before:

@Nullable String name;
name = "Andrew Chen"; // 編譯成功
name = null; // 編譯成功
name.length(); // 編譯成功

After:

var name: String?
name = null // 編譯成功
name.length // 編譯時期出錯,要求應該要檢查 null
name?.length() // 編譯成功,因為有檢查 non-null,才執行 length()
name!!.length() // 編譯成功,如果發生 null ,拋出執行時期 NullPointerException

這樣我們可以在編譯時期就留意到 NullPointerException(簡稱 NPE) ,並當下要求碼者提供因應措施, 像是 “!!.” 維持傳統的執行時期通報,或者常用的 “?.” 檢查後才執行

Before:

String version = "UNKNOWN";
if(computer != null){
Soundcard soundcard = computer.getSoundcard();
if(soundcard != null){
USB usb = soundcard.getUSB();
if(usb != null){
version = usb.getVersion();
}
}
}

Before:

String version = 
Optional.of(computer)
.flatMap(Computer::getSoundcard)
.flatMap(Soundcard::getUSB)
.map(USB::getVersion)
.orElse("UNKNOWN");

Before:

String version =
Observable.just(computer)
.filter(it -> it != null)
.map(Computer::getSoundcard)
.filter(it -> it != null)
.map(Soundcard::getUSB)
.filter(it -> it != null)
.map(USB::getVersion)
.defaultIfEmpty("UNKNOWN").toBlocking().single();

Before:

String version = 
Maybe.just(computer)
.flatMap(Computer::getSoundcard)
.flatMap(Soundcard::getUSB)
.map(USB::getVersion)
.defaultIfEmpty("UNKNOWN").blockingGet();

After:

computer?.getSoundcard()?.getUSB()?.getVersion() ?: “UNKNOWN”

NonNull 就不用檢查 null:

var name: String
name = null // 編譯出錯
name.length() // 編譯成功

More, Nullable 區塊性:

name?.let {
// ..
}
.apply
.run
.use

函數宣告

Before:

int sum(int a, int b) {
return a + b
}

After:

fun sum(a: Int, b: Int): Int {
return a + b
}

我們可以發現型別宣告是後綴式

可以簡化成 inline :

fun sum(a: Int, b: Int) = a + b

這邊因為也是型別推斷,所以回傳型別可以省略

Elvis operator

Before:

int l = a != null ? a.length() : -1;
// var l = if (a != null) a.length() else -1

After:

val l = a?.length() ?: -1

POJO

為了正規化,除了寫 getter 與 setter 還要實現 equals 與 hashCode 甚至 toString()

Before:

public class User {
private String name;
private int id;

public String getName() {
return name;
}

public int getId() {
return id;
}

public User(String name, int id) {
this.name = name;
this.id = id;
}

@Override
public String toString() {
return "User{"
+ "name=" + name
+ ", id=" + id
+ "}";
}

@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o instanceof User) {
User that = (User) o;
return (this.name.equals(that.getName()))
&& (this.id == that.getId());
}
return false;
}

@Override int hashCode() {
return Objects.hashCode(name, id);
}
}

Before:

@AutoValue
public abstract class User {
public abstract String name();
public abstract int id();
public static User of(String name, int id) {
return new AutoValue_User(name, id);
}
}

After:

data class User(val name: String, val id: Int)

Lambda

Before:

view.setOnClickListener(new OnClickListener() {
@Override public void onClick(View v) {
System.out.println("click!");
}
});

After:

view.setOnClickListener { v -> println("click!") }

Multi-assignment for loop

Before:

for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}

After:

for ((key, value) in map) {
println("$key: $value") // string interpolation
}

String interpolation

println("pi = $pi.format(2)")fun Double.format(digits: Int) = java.lang.String.format("%.${digits}f", this) // function extension

簡便 getter 與 setter

Before:

class User {
private String firstName;
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
}

After:

class User {
var firstName: String?
var lastName: String?
}

如果你需要改寫 setter/getter:

var json: String
var firstName?: String
get {
json.get
}

範圍

Before:

for (int i = 0; i < 10; i++) {  }

Before:

Observable.range(0, 9).forEach(i -> {});

After:

for (i in 0..9)

step, After:

for (i in 1..4 step 2) println(i) // "1" "3"

擴充 Cursor

Before:

String firstName = cursor.getString(cursor.getColumnIndexOrThrow("first_name"));

After:

val firstName = cursor.getString("first_name");fun Cursor.getString(columnName: String): String {
return getString(getColumnIndexOrThrow(columnName))
}

避免 NullPointerException:

Before:

String firstName = null;
int firstNameColumnIndex = cursor.getColumnIndexOrThrow("first_name");
if (!cursor.isNull(firstNameColumnIndex)) {
firstName = cursor.getString(firstNameColumnIndex);
}
firstNameView.setText(firstName != null ? firstName : "Andrew");

After:

val firstName = cursor.getStringOrNull("first_name")
firstNameView.setText(firstName ?: "Andrew")
fun Cursor.getStringOrNull(columnName: String): String? {
val index = getColumnIndexOrThrow(columnName)
return if (isNull(index)) null else getString(index)
}
fun Cursor.getString(columnName: String): String = getStringOrNull(columnName)!!

findViewById?

Before:

@InjectView(R.id.first_name)
TextView firstNameTextView;
@Override
public void onCreate(...) {
super.onCreate(...);
setContentView(R.layout.activity_main);
ButterKnife.inject(this);
firstNameTextView.setText("Andrew");
}

After:

import kotlinx.android.synthetic.activity_main.first_name as firstNameTextViewfirstNameTextView.setText("Andrew")

high-order function (高階函數)

函數化

Before:

obs.filter(it -> it != null)

After:

collection.filter { it != null }

透過 callback 影響回傳行為

Functional/Promise/Reactive/Curry

例如:filter

After:

val filteredNames = names.filter { "Andrew".equals(it) }fun <T> List<T>.filter(predicate: (T) -> Boolean): List<T> {
val newList = ArrayList<T>()
for (item in this) {
if (!predicate(item)) continue
newList.add(item);
}
return newList;
} // TODO: lazy instead of eager

Before:

List<String> filteredNames =
filter(names, it -> "Andrew".equals(it));
public static <T> List<T> filter(List<T> list, Predicate<T, Boolean> predicate) {
List<T> newList = new ArrayList<>();
for (T item : list) {
if (!predicate.test(item)) continue;
newList.add(item);
}
return newList;
}

擴充資料庫 Transaction (High-order fun)

Before:

db.beginTransaction();
try {
db.delete("users", "first_name = ?", new String[] { "Andrew" });
db.delete("users", "first_name = ?", new String[] { "yongjhih" });
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}

or

Databases.from(db).inTransaction(() -> {
db.delete("users", "first_name = ?", new String[] { "Andrew" })
db.delete("users", "first_name = ?", new String[] { "yongjhih" })
});

or

Databases.from(db).inTransaction(it -> {
it.delete("users", "first_name = ?", new String[] { "Andrew" })
it.delete("users", "first_name = ?", new String[] { "yongjhih" })
});

After:

這邊還利用了 T.() function extension 來簡化 lambda 內的呼叫方法,省了 it :

db.inTransation {
delete("users", "first_name = ?", arrayOf("Andrew"))
}
inline fun SQLiteDatabase.inTransaction(func: SQLiteDatabase.() -> Unit) {
beginTransaction()
try {
func()
setTransactionSuccessful()
} finally {
endTransaction()
}
}

擴充 SharedPreferences (High-order fun)

Before:

SharedPreferences.Editor editor = preferences.edit();editor.putString("first_name", "Andrew");
editor.putString("last_name", "Chen");
editor.remove("age");
editor.apply();

or

SharedPreferencesUtils.from(preferences).apply(editor -> {
editor.putString("first_name", "Andrew");
editor.putString("last_name", "Chen");
editor.remove("age");
});

After:

preferences.edit {
putString("first_name", "Andrew")
putString("last_name", "Chen")
remove("age")
}
inline fun SharedPreferences.edit(func: SharedPreferences.Editor.() -> Unit) {
val editor = edit()
editor.func()
editor.apply()
}

擴充 Notification Builder (High-order fun)

Before:

Notification notification = new NotificationCompat.Builder(context)
.setContentTitle("Hello")
.setSubText("World")
.build();

After:

val notification = Notification.build(context) {
setContentTitle("Hello")
setSubText("World")
}
inline fun Notification.build(context: Context, func: NotificationCompat.Builder.() -> Unit): Notification {
val builder = NotificationCompat.Builder(context)
builder.func()
return builder.build()
}

infix

Before:

Pair<String, String> pair = Pair.create("key", "value");

After:

var pair = "key" to "value"

Source:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

Before:

"Andrew".equals("Andrew");

After

"Andrew" eq "Andrew"public infix fun <A, B> A.eq(that: B): Boolean = this.equals(that)

Before:

assertThat("Andrwe").isEqualTo("Andrew");

After:

"Andrew" eq "Andrew"public infix fun <A, B> A.eq(that: B): Unit = assertThat(this).isEqualTo(that)

其他 infix

* and
* downTo
* intersect
* or
* shl
* shr
* step
* subtract
* then
* thenDescending
* to
* union
* until
* ushr
* xor
* zip

use: try-with-resource

Before:

String text = toString(stream);public static String toString(InputStream stream) {
final StringBuilder sb = new StringBuilder();
String strLine;
BufferedReader buffer = null;
InputStreamReader reader = null
try {
reader = new InputStreamReader(stream);
buffer = new BufferedReader());
while ((strLine = buffer.readLine()) != null) {
sb.append(strLine);
}
} catch (final IOException ignore) {
// ignore
} finally {
if (buffer != null) buffer.close();
if (reader != null) reader.close();
}
return sb.toString();
}

Before:

String text = toString(stream);public static String toString(InputStream stream) {
final StringBuilder sb = new StringBuilder();
String strLine;
try (final BufferedReader reader = new
BufferedReader(new InputStreamReader(stream))) {
while ((strLine = reader.readLine()) != null) {
sb.append(strLine);
}
} catch (final IOException ignore) {
// ignore
}
return sb.toString();
}

After:

var text = stream.readText()// default value for arguments
inline fun InputStream.readText(charset: Charset = Charsets.UTF_8): String? {
var text: String? = null
val sb = StringBuilder()
var reader: InputStreamReader? = null
var buffer: BufferedReader? = null

try {
reader = InputStreamReader(this, charset)
buffer = BufferedReader(reader)
buffer.readLine()

text = buffer.readLine()
while (text != null) {
sb.append(text);
text = buffer.readLine()
}
} catch (ignore: IOException) {
// ignore
} finally {
if (buffer != null) buffer.close()
if (reader != null) reader.close()
}
return sb.toString()
}

After:

var text = stream.readText()// p.s. return is not necessary
inline fun InputStream.readText(charset: Charset = Charsets.UTF_8) {
return InputStreamReader(this, charset).use {
return
BufferedReader(it).use {
var text: String? = null
val sb = StringBuilder()
text = readLine()
while (text != null) {
sb.append(text);
text = readLine()
}
return sb.toString()
}
}
}

After:

var text = stream.bufferedReader(charset).use { readText() }
//
stream.reader().readText() leaks cloasable

let, apply

let:

fun <T, R> T.let(f: (T) -> R): R = f(this)

let 用法:

var prefs: Preferenens? = getSharedPreferences();
prefs?.let { p ->
p.putString("name", "Andrew")
p.commit();
}

apply:

fun <T> T.apply(f: T.() -> Unit): T { f(); return this }

apply 用法:

var prefs: Preferenens? = getSharedPreferences();
prefs?.apply {
putString("name", "Andrew")
commit();
}

回傳自己,所以類似鏈式 builder 。

run 基本上就是不傳參數的 let:

fun <T, R> T.run(f: T.() -> R): R = f()

而 with(x) 基本上就是 x.apply 差別在於回傳型別可以是別的型別 ,可以鏈式。

Delegated Property

Before:

Resources resources;Resources getResources() {
if (resources == null) resources = context.getResources();
return resources;
}

After:

val resources by lazy { context.getResources() }

Smart cast

Before:

void setText(View view) {
if (view instanceof TextView) {
((TextView) view).setText("hello");
}
}

After:

fun setText(view: View) {
if (view is TextView) {
view.setText("hello");
}
}

switch case: when with smart cast

After:

fun display(view: View, url: String) {
when (view) {
is DraweeView -> view.setImageURI(url)
is TextView -> view.setText(url)
else -> println("No thing")
}
}

集合運算子

filterNotNull()

Before:

List<String> repos = Arrays.asList("RetroFacebook", "NotRetrofit", null, "RxParse");
List<String> reposNotNull = new ArrayList<>();
for (String repo : repos) {
if (repo != null) reposNotNull.add(repo);
}

After:

val repos = listOf("RetroFacebook", "NotRetrofit", null, "RxParse");
repos.filterNotNull()

sort()

Before:

Collections.sort(repoNotNull, new Comparator<String>() {
@Override public int compare(String l, String r) {
return r.length() - l.length();
}
});

After:

repos.sortedBy { it.length() }

toUpperCase()

repos.map { it.toUpperCase() }

any(其中)

其中一個項目成立,就回傳真。

語法:

list.any(() -> Boolean)val list = listOf(1, 2, 3, 4, 5, 6)
// 任何一個數字能整除 2
assertTrue(list.any { it % 2 == 0 })
// 任何一個數字大於 10
assertFalse(list.any { it > 10 })

all(都)

其中一個項目不成立,就回傳假。(所有的項目都成立,就回傳真。)

val list = listOf(1, 2, 3, 4, 5, 6)
// 都小於 10
assertTrue(list.all { it < 10 })
// 都整除 2
assertFalse(list.all { it % 2 == 0 })

count(共有幾個)

val list = listOf(1, 2, 3, 4, 5, 6)
// 整除 2 的項目共有 3 個
assertEquals(3, list.count { it % 2 == 0 })

reduce

不解釋

val list = listOf(1, 2, 3, 4, 5, 6)
assertEquals(21, list.reduce { total, next -> total + next })

reduceRight (倒著 reduce)

不解釋

val list = listOf(1, 2, 3, 4, 5, 6)
assertEquals(21, list.reduceRight { total, next -> total + next })

fold (類似有初始值的 reduce)

val list = listOf(1, 2, 3, 4, 5, 6)
// 4 + 1+2+3+4+5+6 = 25
assertEquals(25, list.fold(4) { total, next -> total + next })

foldRight (同 fold ,只是倒著)

val list = listOf(1, 2, 3, 4, 5, 6)
// 4 + 6+5+4+3+2+1 = 25
assertEquals(25, list.foldRight(4) { total, next -> total + next })

forEach

不解釋

val list = listOf(1, 2, 3, 4, 5, 6)
list.forEach { println(it) }
for (item in list) print(item)

forEachIndexed

不解釋

val list = listOf(1, 2, 3, 4, 5, 6)
list forEachIndexed { index, value
-> println("$index : $value") }

max

不解釋

val list = listOf(1, 2, 3, 4, 5, 6)
assertEquals(6, list.max())

maxBy

在算最大之前,先運算一次。

val list = listOf(1, 2, 3, 4, 5, 6)
// 所有數值負數後,最大的那個是 1
assertEquals(1, list.maxBy { -it })

min

不解釋

val list = listOf(1, 2, 3, 4, 5, 6)
assertEquals(1, list.min())

minBy

不解釋

val list = listOf(1, 2, 3, 4, 5, 6)
assertEquals(6, list.minBy { -it })

none (沒有一個)

val list = listOf(1, 2, 3, 4, 5, 6)
// 沒有一個整除 7
assertTrue(list.none { it % 7 == 0 })

sumBy

val list = listOf(1, 2, 3, 4, 5, 6)
// 2 餘數的總和
assertEquals(3, list.sumBy { it % 2 })

withIndices

Before:

int index = 0;
for (String item : list) {
System.out.println(index + " : " + item);
index++;
}

After:

for ((index, item) in list.withIndices()) {
println("$index : $item")
}

Null 陣列處理

fun max(numbers: Array<Int>?): Int {
var max: Int? = null
return numbers?.forEach { max = Math.max(it, max) } ?: 0
}

另一種方式:

fun max(numbers: Array<Int>?): Int = numbers?.run {
var max: Int? = null
forEach { max = Math.max(it, max) }
return max ?: 0
} ?: 0

第三種:

fun max(numbers: Array<Int>?): Int = numbers.orEmpty().run {
var max: Int? = null
forEach { max = Math.max(it, max) }
return max ?: 0
}

Dagger

MVP

RecyclerView

ViewPager

導入方法

  • 可利用 Android Studio kotlin plugin 轉換程式碼 (轉完不一定可動,大多稍微改一下就好了)
  • buildscript.dependencies: classpath “org.jetbrains.kotlin:kotlin-gradle-plugin
  • apply plugin: ‘kotlin-android’
  • dependencies: compile ‘org.jetbrains.kotlin:kotlin-stdlib:0.12.200’

RxKotlin

observable<String> { subscriber ->
subscriber.onNext("H")
subscriber.onNext("e")
subscriber.onNext("l")
subscriber.onNext("")
subscriber.onNext("l")
subscriber.onNext("o")
subscriber.onCompleted()
}.filter { it.isNotEmpty() }.
fold (StringBuilder()) { sb, e -> sb.append(e) }.
map { it.toString() }.
subscribe { result ->
a.received(result)
}
verify(a, times(1)).received("Hello")

Anko

捨棄 xml 直接用 kotlin 語言來配置 UI。

verticalLayout {
val name = editText()
button("Say Hello") {
onClick { toast("Hello, ${name.text}!") }
}
}
  • Android Studio 預覽插件:Anko Preview Plugin for idea

kovenant — Promise

async {
//some (long running) operation, or just:
1 + 1
} then {
i -> "result: $i"
} success {
msg -> println(msg)
}
async { "world" } and async { "Hello" } success {
println("${it.second} ${it.first}!")
}

injekt — DI

funKtionale — functional (Deprecated)

val sum = { x: Int, y: Int -> x + y }
val curriedSum: (Int) -> (Int) -> Int = sum.curried()
assertEquals(curriedSum(2)(4), 6)val add3 = curriedSum(3)
assertEquals(add3(5), 8)
  • ref: 2015/3 mid 1

Closure

Before:

add(1) { 2 }fun add(x: Int, func: () -> Int): Int {
return x + func()
}

After:

add(1)(2)fun add(x: Int): (Int) -> Int {
return { y -> x + y }
}

Fuel — networking

"http://github.com/yongjhih/".httpGet().responseString { request, response, either ->
//do something with response
when (either) {
is Left -> // left means failure
is Right -> // right means success
}
}

其他議題

編譯緩慢問題

在 M12 之前 clean build 十分的緩慢,但是之後的版本就快多了,雖然還是會慢一點點。如果是二次編譯就幾乎跟一般編譯差不多速度,啟動遞增編譯(kotlin.incremental=true) 後甚至比原生 java 快,這裡有篇文章比較詳細。

我們也可以重新思考什麼叫做緩慢,應該從 SPEC 需求發包到施工到釋出的時間觀念,透過導入新的語言減少除錯以及避免釋出潛藏災難機率、加快施工開發、加快程式碼審核流程,然後扣掉機器編譯較慢的時間,是否真的緩慢了呢?

lateinit 與 lazy 應用場合

大概的場合會是在:

  • lazy 主要使用在 外部存取 singleton member 隨著自己類別的生命週期結束
  • lateinit 引進內部存取,這個物件由外部建構,與自己的類別生命週期無關

See Also

Origin from https://yongjhih.gitbooks.io/feed/content/kotlin.html

--

--