Kotlin 遇上 Android
卡特琳
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
- https://github.com/ReactiveX/RxKotlin
- Using Project Kotlin for Android @JackWharton
- https://github.com/JetBrains/anko
- http://kotlinlang.org/docs/reference/
Origin from https://yongjhih.gitbooks.io/feed/content/kotlin.html