【從零開始擼一個(gè)App】Kotlin
工欲善其事必先利其器。像我們從零開始擼一個(gè)App的話,選擇最合適的語言是首要任務(wù)。如果你跟我一樣對(duì)Java蹣跚的步態(tài)和僵硬的語法頗感無奈,那么Kotlin在很大程度上不會(huì)令你失望。雖然為了符合JVM規(guī)范和兼容Java,它引入了一些較為復(fù)雜的概念和語法,很多同學(xué)就是因此放棄入門。其實(shí)越深入進(jìn)去,就會(huì)越欲罷不能。除了Android開發(fā),博主也常在后端使用Kotlin編碼,有時(shí)因?yàn)槟承┰蛲瑫r(shí)使用Java混編。總的來說,能減少代碼量,提高生產(chǎn)效率,似乎代碼結(jié)構(gòu)也更清晰了。如果你沒有Kotlin的經(jīng)驗(yàn),但是比較過Java和C#,你就明白我的意思了,甚至Kotlin有些地方比C#還方便。可以說Kotlin既有C#便捷的語法,亦背靠Java平臺(tái)良好的生態(tài),那么你還在猶豫什么?
基礎(chǔ)
var:可變,是一個(gè)可變變量。可知var類型屬性不能設(shè)置為延遲加載屬性,因?yàn)樵?code>lazy中并沒有setValue(…)方法。在DI場(chǎng)景下,常與lateinit搭配使用,可參看Kotlin中l(wèi)ateinit變量在字節(jié)碼層面上的解釋
val:不可變,一個(gè)只讀變量。另外還有const val,只允許在top-level級(jí)別和object中使用。它們的區(qū)別如下:
- const val 可見性為public final static,可以直接訪問。
- val 可見性為private final static,并且val 會(huì)生成方法
getNormalObject(),通過方法調(diào)用訪問。
Unit:當(dāng)一個(gè)函數(shù)沒有返回值的時(shí)候,我們用Unit來表示這個(gè)特征,同Java中的void。
open:在java中允許創(chuàng)建任意的子類并重寫方法任意的方法,除非顯示的使用了final關(guān)鍵字進(jìn)行標(biāo)注。而在Kotlin的世界里面則不是這樣,在Kotlin中它所有的類默認(rèn)都是final的,那么就意味著不能被繼承,而且在類中所有的方法也是默認(rèn)是final的,那么就是Kotlin的方法默認(rèn)也不能被重寫。為類增加open,class就可以被繼承了;為方法增加open,那么方法就可以被重寫了。
inline:Kotlin 內(nèi)聯(lián)函數(shù) inline。它會(huì)將代碼塊拷貝到調(diào)用的地方,減少了調(diào)用層數(shù)和額外對(duì)象的產(chǎn)生。
crossinline:這是因inline的副作用而引入的關(guān)鍵字。由于inline會(huì)將代碼拷貝到調(diào)用的地方,如果代碼里面有return,那么目標(biāo)代碼(調(diào)用者)的邏輯可能就被破壞了。用crossinline修飾相應(yīng)的lambda,將return返回到對(duì)應(yīng)標(biāo)簽[,而不是返回到整個(gè)方法]。
reified:為了應(yīng)對(duì)Java偽泛型導(dǎo)致的代碼冗余問題。可參看使用Kotlin Reified 讓泛型更簡(jiǎn)單安全。這主要是應(yīng)對(duì)Java中的泛型擦除。Java中的泛型是偽泛型,即它的泛型只存在于編譯期,在生成的字節(jié)碼文件中是不包含任何泛型信息的(不過至少在編譯期就能及早發(fā)現(xiàn)類型不匹配的問題),在編譯后的字節(jié)碼文件中,就已經(jīng)被替換為原來的原始類型(Raw Type/Object)了,并且在相應(yīng)的地方插入了強(qiáng)制轉(zhuǎn)型代碼,是為類型擦除。因此對(duì)于運(yùn)行期的Java語言來說,ArrayList<int>與ArrayList<String>就是同一個(gè)類型。相對(duì)的,C#中使用的泛型,就是真泛型,其泛型無論在程序源碼中、編譯后的IL中或是運(yùn)行期的CLR中都是切實(shí)存在的,List<int>與List<String>就是兩個(gè)不同的類型,它們有自己的虛方法表和類型數(shù)據(jù)。
下面是我封裝RabbitMQ消費(fèi)端監(jiān)聽的代碼(感興趣的同學(xué)可以參看本人博文RabbitMQ入門指南獲取更多信息):
/**
* 從指定隊(duì)列獲取消息,并定義回調(diào)(for kotlin)
*
* @param queue the name of the queue from where receive messages
* @param block callback when a message arrived
*/
inline fun <reified T> receive(queue: String, crossinline block: (T) -> Boolean) {
factory.newConnection().use {
val conn = it.get()
val channel = conn.createChannel()
channel.basicConsume(queue, false, object : DefaultConsumer(channel) {
override fun handleDelivery(consumerTag: String?, envelope: Envelope, properties: AMQP.BasicProperties?, body: ByteArray) {
try {
val message = JSON.parseObject(String(body), object : TypeReference<T>() {})
val done = block(message)
if (done) {
channel.basicAck(envelope.deliveryTag, false)
} else {
//若失敗則重新投遞一次,否則丟棄或投遞到死信隊(duì)列(若配置了的話)
channel.basicNack(envelope.deliveryTag, false, !envelope.isRedeliver)
}
} catch (e: Exception) {
_logger.error("處理消息-${String(body)}時(shí)發(fā)生錯(cuò)誤-${e.message}")
throw e
}
}
})
}
}
注意Java編譯器不支持inline和reified等關(guān)鍵字,所以如果要使用Java調(diào)用,還需要另外寫for java的版本。
field:用于屬性取值/賦值邏輯(如果顯式定義的話),類似于C#屬性中的value關(guān)鍵字,防止訪問器的自遞歸而導(dǎo)致程序崩潰的 StackOverflowError異常,參看kotlin學(xué)習(xí)—Field
this@ClassName:匿名內(nèi)部類對(duì)象引用[包含它的]外部對(duì)象。
by:修飾屬性和字段,提供若干效用,可參看Kotlin by。
在監(jiān)聽值更改的場(chǎng)景中,by 的作用類似于 C# 屬性的 set 方法。當(dāng)然, kotlin 自己也有 get/set 語法。
更進(jìn)一步,如果一個(gè)對(duì)象有 getValue() [和 setValue()] 方法,則這個(gè)對(duì)象就可以作為 by 后面的表達(dá)式。
還可以在類定義時(shí)使用,可以將某實(shí)例的所有的 public 方法委托該類[,似乎這些方法就是在這個(gè)類中定義的]。這應(yīng)該是組合的形態(tài),但我們也可用它實(shí)現(xiàn)某種語法程度的“多繼承”,以后面協(xié)程部分的代碼片段為例:
class BasicCorotineActivity : AppCompatActivity(), CoroutineScope by MainScope() {}
其中CoroutineScope是interface,MainScope()返回的是CoroutineScope的實(shí)現(xiàn)類ContextScope的實(shí)例。也就是說,BasicCorotineActivity實(shí)現(xiàn)了接口CoroutineScope,但BasicCorotineActivity本身不實(shí)現(xiàn)其中的方法,而是委托給MainScope()返回的對(duì)象幫它實(shí)現(xiàn)。這減少了代碼冗余,從寫法上看,也似乎BasicCorotineActivity同時(shí)繼承了AppCompatActivity類和CoroutineScope實(shí)例:)
在kotlin中interface不僅可以聲明函數(shù),還可以對(duì)函數(shù)進(jìn)行實(shí)現(xiàn)。與類唯一不同的是它們是無狀態(tài)的,所以屬性需要子類去重寫。類需要去負(fù)責(zé)保存接口屬性的狀態(tài)。
Elvis操作符:?: ,類似js中的 | ,若前者為null則取后者。
Kotlin并非一門純粹的語言,它在語法部分常考慮到Java的兼容和可轉(zhuǎn)換性,為此增添了不少讓新手困惑的語法和關(guān)鍵字。如對(duì)一個(gè)屬性或一個(gè)主構(gòu)造器的參數(shù)進(jìn)行注解時(shí),Kotlin元素將會(huì)生成對(duì)應(yīng)的多個(gè)Java元素,因此在Java字節(jié)碼中該注解有多個(gè)可能位置。如果要精確指定該如何生成該注解,可使用以下語法:
class Example(@field:Ann val foo, // annotate Java field
@get:Ann val bar, // annotate Java getter
@param:Ann val quux) // annotate Java constructor parameter
更多可參看Kotlin編碼竅門之注解(Annotations)
companion object 和 object:Kotlin 移除了 static 的概念,這兩者轉(zhuǎn)換成Java后都有靜態(tài)單例的模式,容易讓人困惑它們的區(qū)別。其實(shí)從使用場(chǎng)景分析就比較明了了,前者作為一個(gè)類的靜態(tài)內(nèi)部單例類[對(duì)象]使用(companion就是伴侶的意思),后者就是一個(gè)靜態(tài)單例類[對(duì)象],不需要外圍類的存在(沒有companion嘛)。
在companion object場(chǎng)景下我們常使用@JvmStatic和@JvmField以便將它們修飾的方法和字段[在外部Java代碼看來]暴露為類的子級(jí),可參看微知識(shí)#1 Kotlin 的 @JvmStatic 和 @JvmField 注解。
相關(guān)概念:@JvmOverloads
object關(guān)鍵字還可用于創(chuàng)建接口或者抽象類的匿名對(duì)象。
Kotlin允許你在文件中定義頂級(jí)的函數(shù)和屬性。
Kotlin除了有擴(kuò)展方法,還有擴(kuò)展屬性,參看Kotlin的擴(kuò)展屬性和擴(kuò)展方法
Kotlin的函數(shù)參數(shù)是只讀的。
重溫一下表達(dá)式與語句的區(qū)別。表達(dá)式有值,并能作為另一個(gè)表達(dá)式的一部分來使用;而語句沒有返回值。Java 中的控制結(jié)構(gòu)皆為語句。而在 Kotlin 中,除了循環(huán)體結(jié)構(gòu)外,大多數(shù)控制結(jié)構(gòu)都是表達(dá)式。
lambda
Kotlin中的語法糖特別的多,比如lambda表達(dá)式,作為參數(shù)傳遞就有幾種不同的寫法:
- 普通方式:
button.setOnClickListener({strInfo: String -> Unit}) - 如果最后一個(gè)參數(shù)是傳遞的lambda表達(dá)式,可以在圓括號(hào)之外指定:
button.setOnClickListener(){strInfo: String -> Unit} - 如果函數(shù)的參數(shù)只有一個(gè)[或者其它參數(shù)都有默認(rèn)值],并且這個(gè)參數(shù)是lambda,就可以省略圓括號(hào):
button.setOnClickListener{strInfo: String -> Unit} - 甚至可以省略為:
button.setOnClickListener{strInfo}
以上面例子為例,如果setOnClickListener接受的參數(shù)不是lambda類型而是一個(gè)interface,該interface下只有一個(gè)方法,那么同樣可以使用上述語法[,似乎setOnClickListener接受的參數(shù)就是lambda類型]。此類interface常用@FunctionalInterface修飾。(其實(shí)這應(yīng)該就是java的特性,如RxJava中的subscribe(Consumer<? super T> onNext),在別人調(diào)用它的時(shí)候就可以直接傳lambda表達(dá)式)。
注意,用kotlin自己寫的interface并不支持此特性
在調(diào)用時(shí)將lambda方法體移至括號(hào)外面應(yīng)該是為了代碼的可讀性,使得更貼近代碼邏輯塊而非單個(gè)參數(shù)的感覺。
協(xié)程Coroutine
首先我們要知道一點(diǎn),協(xié)程這個(gè)概念現(xiàn)在有點(diǎn)被濫用了,市面上流行的語言似乎都想把協(xié)程納入自己的特性里。如果你對(duì)協(xié)程還不了解,請(qǐng)參看博主寫的再談協(xié)程或其它資料。博主認(rèn)為真正的協(xié)程是如Go那樣的實(shí)現(xiàn)。Kotlin雖然也有協(xié)程,但更類似于C#里的async/await,是在多線程層面的語法處理。更深入的分析可參看Kotlin 協(xié)程真的比 Java 線程更高效嗎?
suspend:關(guān)鍵字,它一般標(biāo)識(shí)在一個(gè)函數(shù)的開頭,用于表示該函數(shù)是個(gè)耗時(shí)操作。這個(gè)關(guān)鍵字主要作用就是為了作一個(gè)提醒,并不會(huì)因?yàn)樘砑恿诉@個(gè)關(guān)鍵字就會(huì)該函數(shù)立即跑到一個(gè)子線程上。是否切換線程仍是由launch ,withContext ,async決定的。當(dāng)然了,有時(shí)候我們必須在函數(shù)前面加上suspend,如果函數(shù)內(nèi)部調(diào)用了其它suspend函數(shù)的話。
如果使用retrofit2封裝網(wǎng)絡(luò)請(qǐng)求的話,接口定義,原本每個(gè)函數(shù)應(yīng)該返回的是Call<>(若有返回的話)類型。或者可以使用Jake Wharton寫的CoroutineCallAdapterFactory組件,它使得函數(shù)支持Deferred<>返回值,簡(jiǎn)化協(xié)程+retrofit2的開發(fā)。不過從Retrofit 2.6.0起,Retrofit內(nèi)置了對(duì)suspend關(guān)鍵字的支持,可以以更純粹的方式定義函數(shù),如:
@GET("users/{id}")
suspend fun user(@Path("id") id: Long): User
若要將傳統(tǒng)的回調(diào)封裝成協(xié)程模式,可使用suspendCoroutine或suspendCancellableCoroutine,如下所示:
suspend fun MqttAsyncClient.aPublish(payload: MqttMessage, topic: String): IMqttToken =
suspendCoroutine { cont ->
publish(topic, payload, null, object: MqttActionListener {
override fun onSuccess(asyncActionToken: IMqttToken) {
cont.resume(asyncActionToken)
}
override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable) {
cont.resumeWithException(exception)
}
})
}
盡可能使用suspendCancellableCoroutine而不是suspendCoroutine,因?yàn)槭褂们罢邉t協(xié)程的取消是可控的。Kotlin沒有檢查異常,但是我們?nèi)匀恍枰趖ry-catch中處理所有可能的異常。否則,該應(yīng)用程序?qū)⒈罎ⅰ5莝uspendCancellableCoroutine取消拋出的異常CancellationException是個(gè)意外,它并不會(huì)導(dǎo)致程序崩潰。
慣常用CoroutineScope.launch創(chuàng)建協(xié)程(當(dāng)然還有runBlocking, withContext,async等),它會(huì)返回一個(gè)Job對(duì)象,便于在外部對(duì)協(xié)程進(jìn)行控制。
job.join():阻塞當(dāng)前線程,直到j(luò)ob執(zhí)行完畢。這是一個(gè) suspend 函數(shù),所以一般在 Coroutine 內(nèi)調(diào)用,阻塞當(dāng)前所在Coroutine。job.cancel():取消job,執(zhí)行后該job就進(jìn)入cancelling狀態(tài),但是否真的取消了需要看job自身實(shí)現(xiàn)。Coroutine標(biāo)準(zhǔn)庫中定義的 suspend function 都是支持取消操作的(比如 delay)。自定義job的時(shí)候可以通過 isActive 屬性來判斷當(dāng)前任務(wù)是否被取消了,如果發(fā)現(xiàn)被取消了則停止繼續(xù)執(zhí)行。如果自定義job沒有相應(yīng)的處理邏輯,那么就算調(diào)用job.cancel(),也并不能取消它的執(zhí)行。SupervisorJob(parent: Job? = null):返回一個(gè)job實(shí)例,里面的子Job不相互影響,一個(gè)子Job失敗了,不影響其他子Job的執(zhí)行。parent參數(shù)用于關(guān)聯(lián)自己本身的父job。如果研究協(xié)程源碼的話,會(huì)常看到ContextScope(SupervisorJob() + Dispatchers.Main)的寫法(如ViewModel.viewModelScope的實(shí)現(xiàn)),這里的 + 號(hào)是CoroutineContext對(duì)操作符plus的重載,前后兩者都是CoroutineContext的子類。
Dispatchers:
- Dispatchers.Main 調(diào)用程序在Android的主線程中
- Dispatchers.IO 適合主線程之外的執(zhí)行磁盤或者網(wǎng)絡(luò)io操作,例如文件的讀取與寫入,任何的網(wǎng)絡(luò)請(qǐng)求
- Dispatcher.Default 適合主線程之外的,cpu的操作,例如json數(shù)據(jù)的解析,以及列表的排序
注意,By default, the maximum number of threads used by this dispatcher is equal to the number of CPU cores, but is at least two. Dispatchers.IO的調(diào)度/執(zhí)行線程同Dispatcher.Default一樣,它們使用同一個(gè)線程池,但是遇到IO操作,Dispatchers.IO會(huì)另外創(chuàng)建線程用于處理IO過程(而Dispatcher.Default不會(huì),也就是同一個(gè)線程即干計(jì)算的活,也干搬運(yùn)的活)。
Dispatchers.IO能創(chuàng)建的線程數(shù):The number of threads used by tasks in this dispatcher is limited by the value of “kotlinx.coroutines.io.parallelism” (IO_PARALLELISM_PROPERTY_NAME) system property. It defaults to the limit of 64 threads or the number of cores (whichever is larger).
后記拾遺
當(dāng)使用公有屬性時(shí),有時(shí)會(huì)拋出“Smartcast is impossible because propery has open or custom getter”的編譯時(shí)錯(cuò)誤,究其原因是編譯器分析代碼發(fā)現(xiàn)每次get屬性時(shí)返回的對(duì)象可能不是同一個(gè)。解決方法很簡(jiǎn)單,只要定義一個(gè)臨時(shí)變量指向某次get獲得的值即可。可參看Smartcast is impossible because propery has open or custom getter
Java泛型擦除導(dǎo)致的問題。如下代碼可正常運(yùn)行:
private fun getToken(): Token? {
val preference = TaoismApplication.appContext.sharedPreferences(SharedPreference.SESSION)
val json = preference.getString(SharedPreference.TOKEN, "")
if (!json.isNullOrBlank()) {
return Gson().fromJson<Token>(json, object : TypeToken<Token>() {}.type)
} else {
return null
}
}
由于代碼中有較多getXXX(),抽取模板代碼:
private fun <T : Any?> get(key: String): T? {
val preference = TaoismApplication.appContext.sharedPreferences(SharedPreference.SESSION)
val json = preference.getString(key, "")
if (!json.isNullOrBlank()) {
return Gson().fromJson<T>(json, object : TypeToken<T>() {}.type)
} else {
return null
}
}
調(diào)用get<Token>(SharedPreference.TOKEN)報(bào)錯(cuò):java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to com.xxx.xxx.Token。
so,只能將類型信息顯式傳入,改造方法簽名為get(key: String, typeToken: Type)。
kotlin異常:Kotlin 的異常都是 Unchecked exception。若在函數(shù)上注解了@Throws,則編譯成Java代碼會(huì)變成符合Java模式的checked exception,即在方法定義上會(huì)顯式聲明可能拋出的異常類型,需要在調(diào)用鏈路上處理。對(duì)于Kotlin自身來說@Throws并沒有太多意義,it is only for Java developer to know that they need to handle that exception.參看[Kotlin] Try catch & Throw
使用intellij idea進(jìn)行kotlin和java混合開發(fā),最好將kotlin文件和java文件分各自文件夾存放,否則運(yùn)行時(shí)可能會(huì)報(bào)找不到類的錯(cuò)誤(因?yàn)榫幾g時(shí)會(huì)將不是屬于該文件夾的且沒有被其它文件引用的代碼文件忽略)。如下:

參考資料
Kotlin中標(biāo)準(zhǔn)函數(shù)run、with、let、also與apply的使用和區(qū)別詳解
Kotlin協(xié)程 —— 今天說說 launch 與 async
Kotlin之美——DSL篇

浙公網(wǎng)安備 33010602011771號(hào)