天天看點

【從零開始撸一個App】Kotlin

工欲善其事必先利其器。像我們從零開始撸一個App的話,選擇最合适的語言是首要任務。如果你跟我一樣對Java蹒跚的步态和僵硬的文法頗感無奈,那麼Kotlin在很大程度上不會令你失望。雖然為了符合JVM規範和相容Java,它引入了一些較為複雜的概念和文法,很多同學就是是以放棄入門。其實越深入進去,就會越欲罷不能。除了Android開發,部落客也常在後端使用Kotlin編碼,有時因為某些原因同時使用Java混編。總的來說,能減少代碼量,提高生産效率,似乎代碼結構也更清晰了。如果你沒有Kotlin的經驗,但是比較過Java和C#,你就明白我的意思了,甚至Kotlin有些地方比C#還友善。可以說Kotlin既有C#便捷的文法,亦背靠Java平台良好的生态,那麼你還在猶豫什麼?

<code>var</code>:可變,是一個可變變量。可知var類型屬性不能設定為延遲加載屬性,因為在<code>lazy</code>中并沒有setValue(…)方法。在DI場景下,常與<code>lateinit</code>搭配使用,可參看Kotlin中lateinit變量在位元組碼層面上的解釋

<code>val</code>:不可變,一個隻讀變量。另外還有<code>const val</code>,隻允許在top-level級别和object中使用。它們的差別如下:

const val 可見性為public final static,可以直接通路。

val 可見性為private final static,并且val 會生成方法<code>getNormalObject()</code>,通過方法調用通路。

<code>Unit</code>:當一個函數沒有傳回值的時候,我們用Unit來表示這個特征,同Java中的void。

<code>open</code>:在java中允許建立任意的子類并重寫方法任意的方法,除非顯示的使用了final關鍵字進行标注。而在Kotlin的世界裡面則不是這樣,在Kotlin中它所有的類預設都是final的,那麼就意味着不能被繼承,而且在類中所有的方法也是預設是final的,那麼就是Kotlin的方法預設也不能被重寫。為類增加open,class就可以被繼承了;為方法增加open,那麼方法就可以被重寫了。

<code>inline</code>:Kotlin 内聯函數 inline。它會将代碼塊拷貝到調用的地方,減少了調用層數和額外對象的産生。

<code>crossinline</code>:這是因inline的副作用而引入的關鍵字。由于inline會将代碼拷貝到調用的地方,如果代碼裡面有return,那麼目标代碼(調用者)的邏輯可能就被破壞了。用<code>crossinline</code>修飾相應的lambda,将return傳回到對應标簽[,而不是傳回到整個方法]。

<code>reified</code>:為了應對Java僞泛型導緻的代碼備援問題。可參看使用Kotlin Reified 讓泛型更簡單安全。這主要是應對Java中的<code>泛型擦除</code>。Java中的泛型是僞泛型,即它的泛型隻存在于編譯期,在生成的位元組碼檔案中是不包含任何泛型資訊的(不過至少在編譯期就能及早發現類型不比對的問題),在編譯後的位元組碼檔案中,就已經被替換為原來的原始類型(Raw Type/Object)了,并且在相應的地方插入了強制轉型代碼,是為類型擦除。是以對于運作期的Java語言來說,ArrayList&lt;int&gt;與ArrayList&lt;String&gt;就是同一個類型。相對的,C#中使用的泛型,就是真泛型,其泛型無論在程式源碼中、編譯後的IL中或是運作期的CLR中都是切實存在的,List&lt;int&gt;與List&lt;String&gt;就是兩個不同的類型,它們有自己的虛方法表和類型資料。

下面是我封裝RabbitMQ消費端監聽的代碼(感興趣的同學可以參看本人博文RabbitMQ入門指南擷取更多資訊):

注意Java編譯器不支援<code>inline</code>和<code>reified</code>等關鍵字,是以如果要使用Java調用,還需要另外寫for java的版本。

<code>field</code>:用于屬性取值/指派邏輯(如果顯式定義的話),類似于C#屬性中的value關鍵字,防止通路器的自遞歸而導緻程式崩潰的 StackOverflowError異常,參看kotlin學習—Field

<code>this@ClassName</code>:匿名内部類對象引用[包含它的]外部對象。

<code>by</code>:修飾屬性和字段,提供若幹效用,可參看Kotlin by。

還可以在類定義時使用,可以将某執行個體的所有的 public 方法委托該類[,似乎這些方法就是在這個類中定義的]。這應該是<code>組合</code>的形态,但我們也可用它實作某種文法程度的<code>“多繼承”</code>,以後面協程部分的代碼片段為例:

其中<code>CoroutineScope</code>是interface,<code>MainScope()</code>傳回的是CoroutineScope的實作類<code>ContextScope</code>的執行個體。也就是說,BasicCorotineActivity實作了接口CoroutineScope,但BasicCorotineActivity本身不實作其中的方法,而是委托給MainScope()傳回的對象幫它實作。這減少了代碼備援,從寫法上看,也似乎BasicCorotineActivity同時繼承了AppCompatActivity類和CoroutineScope執行個體:)

在kotlin中<code>interface</code>不僅可以聲明函數,還可以對函數進行實作。與類唯一不同的是它們是無狀态的,是以屬性需要子類去重寫。類需要去負責儲存接口屬性的狀态。

Elvis操作符:<code>?:</code> ,類似js中的 | ,若前者為null則取後者。

Kotlin并非一門純粹的語言,它在文法部分常考慮到Java的相容和可轉換性,為此增添了不少讓新手困惑的文法和關鍵字。如對一個屬性或一個主構造器的參數進行注解時,Kotlin元素将會生成對應的多個Java元素,是以在Java位元組碼中該注解有多個可能位置。如果要精确指定該如何生成該注解,可使用以下文法:

更多可參看Kotlin編碼竅門之注解(Annotations)

<code>companion object</code> 和 <code>object</code>:Kotlin 移除了 static 的概念,這兩者轉換成Java後都有靜态單例的模式,容易讓人困惑它們的差別。其實從使用場景分析就比較明了了,前者作為一個類的靜态内部單例類[對象]使用(companion就是伴侶的意思),後者就是一個靜态單例類[對象],不需要外圍類的存在(沒有companion嘛)。

在companion object場景下我們常使用<code>@JvmStatic</code>和<code>@JvmField</code>以便将它們修飾的方法和字段[在外部Java代碼看來]暴露為類的子級,可參看微知識#1 Kotlin 的 @JvmStatic 和 @JvmField 注解。

相關概念:@JvmOverloads

object關鍵字還可用于建立接口或者抽象類的匿名對象。

Kotlin允許你在檔案中定義頂級的函數和屬性。

Kotlin除了有<code>擴充方法</code>,還有<code>擴充屬性</code>,參看Kotlin的擴充屬性和擴充方法

Kotlin的函數參數是隻讀的。

重溫一下表達式與語句的差別。表達式有值,并能作為另一個表達式的一部分來使用;而語句沒有傳回值。Java 中的控制結構皆為語句。而在 Kotlin 中,除了循環體結構外,大多數控制結構都是表達式。

Kotlin中的文法糖特别的多,比如<code>lambda表達式</code>,作為參數傳遞就有幾種不同的寫法:

普通方式:<code>button.setOnClickListener({strInfo: String -&gt; Unit})</code>

如果最後一個參數是傳遞的lambda表達式,可以在圓括号之外指定:<code>button.setOnClickListener(){strInfo: String -&gt; Unit}</code>

如果函數的參數隻有一個[或者其它參數都有預設值],并且這個參數是lambda,就可以省略圓括号:<code>button.setOnClickListener{strInfo: String -&gt; Unit}</code>

甚至可以省略為:<code>button.setOnClickListener{strInfo}</code>

以上面例子為例,如果setOnClickListener接受的參數不是lambda類型而是一個interface,該interface下隻有一個方法,那麼同樣可以使用上述文法[,似乎setOnClickListener接受的參數就是lambda類型]。此類interface常用<code>@FunctionalInterface</code>修飾。(其實這應該就是java的特性,如<code>RxJava</code>中的<code>subscribe(Consumer&lt;? super T&gt; onNext)</code>,在别人調用它的時候就可以直接傳lambda表達式)。

注意,用kotlin自己寫的interface并不支援此特性

在調用時将lambda方法體移至括号外面應該是為了代碼的可讀性,使得更貼近代碼邏輯塊而非單個參數的感覺。可參看Kotlin系列之let、with、run、apply、also函數的使用中這些擴充函數的簽名定義,順便了解下這些函數的使用場景。

首先我們要知道一點,<code>協程</code>這個概念現在有點被濫用了,市面上流行的語言似乎都想把協程納入自己的特性裡。如果你對協程還不了解,請參看部落客寫的再談協程或其它資料。部落客認為真正的協程是如Go那樣的實作。Kotlin雖然也有協程,但更類似于C#裡的async/await,是在多線程層面的文法處理。更深入的分析可參看Kotlin 協程真的比 Java 線程更高效嗎?

<code>suspend</code>:關鍵字,它一般辨別在一個函數的開頭,用于表示該函數是個耗時操作。這個關鍵字主要作用就是為了作一個提醒,并不會因為添加了這個關鍵字就會該函數立即跑到一個子線程上。是否切換線程仍是由<code>launch</code> ,<code>withContext</code> ,<code>async</code>決定的。當然了,有時候我們必須在函數前面加上suspend,如果函數内部調用了其它suspend函數的話。

如果使用<code>retrofit2</code>封裝網絡請求的話,接口定義,原本每個函數應該傳回的是<code>Call&lt;&gt;</code>(若有傳回的話)類型。或者可以使用Jake Wharton寫的<code>CoroutineCallAdapterFactory</code>元件,它使得函數支援<code>Deferred&lt;&gt;</code>傳回值,簡化協程+retrofit2的開發。不過從Retrofit 2.6.0起,Retrofit内置了對suspend關鍵字的支援,可以以更純粹的方式定義函數,如:

若要将傳統的回調封裝成協程模式,可使用<code>suspendCoroutine</code>或<code>suspendCancellableCoroutine</code>,如下所示:

盡可能使用suspendCancellableCoroutine而不是suspendCoroutine,因為使用前者則協程的取消是可控的。Kotlin沒有檢查異常,但是我們仍然需要在try-catch中處理所有可能的異常。否則,該應用程式将崩潰。但是suspendCancellableCoroutine取消抛出的異常CancellationException是個意外,它并不會導緻程式崩潰。

慣常用<code>CoroutineScope.launch</code>建立協程(當然還有<code>runBlocking</code>, <code>withContext</code>,<code>async</code>等),它會傳回一個<code>Job</code>對象,便于在外部對協程進行控制。

<code>job.join()</code>:阻塞目前線程,直到job執行完畢。這是一個 suspend 函數,是以一般在 Coroutine 内調用,阻塞目前所在Coroutine。

<code>job.cancel()</code>:取消job,執行後該job就進入<code>cancelling</code>狀态,但是否真的取消了需要看job自身實作。Coroutine标準庫中定義的 suspend function 都是支援取消操作的(比如 delay)。自定義job的時候可以通過 isActive 屬性來判斷目前任務是否被取消了,如果發現被取消了則停止繼續執行。如果自定義job沒有相應的處理邏輯,那麼就算調用job.cancel(),也并不能取消它的執行。

<code>SupervisorJob(parent: Job? = null)</code>:傳回一個job執行個體,裡面的子Job不互相影響,一個子Job失敗了,不影響其他子Job的執行。parent參數用于關聯自己本身的父job。如果研究協程源碼的話,會常看到<code>ContextScope(SupervisorJob() + Dispatchers.Main)</code>的寫法(如<code>ViewModel.viewModelScope</code>的實作),這裡的 + 号是<code>CoroutineContext</code>對操作符<code>plus</code>的重載,前後兩者都是CoroutineContext的子類。

<code>Dispatchers</code>:

Dispatchers.Main 調用程式在Android的主線程中

Dispatchers.IO 适合主線程之外的執行磁盤或者網絡io操作,例如檔案的讀取與寫入,任何的網絡請求

Dispatcher.Default 适合主線程之外的,cpu的操作,例如json資料的解析,以及清單的排序

注意,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的排程/執行線程同Dispatcher.Default一樣,它們使用同一個線程池,但是遇到IO操作,Dispatchers.IO會另外建立線程用于處理IO過程(而Dispatcher.Default不會,也就是同一個線程即幹計算的活,也幹搬運的活)。

Dispatchers.IO能建立的線程數: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).

當使用公有屬性時,有時會抛出“Smartcast is impossible because propery has open or custom getter”的編譯時錯誤,究其原因是編譯器分析代碼發現每次get屬性時傳回的對象可能不是同一個。解決方法很簡單,隻要定義一個臨時變量指向某次get獲得的值即可。可參看Smartcast is impossible because propery has open or custom getter

Java泛型擦除導緻的問題。如下代碼可正常運作:

由于代碼中有較多getXXX(),抽取模闆代碼:

調用<code>get&lt;Token&gt;(SharedPreference.TOKEN)</code>報錯:<code>java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to com.xxx.xxx.Token</code>。

so,隻能将類型資訊顯式傳入,改造方法簽名為<code>get(key: String, typeToken: Type)</code>。

kotlin異常:Kotlin 的異常都是 Unchecked exception。若在函數上注解了<code>@Throws</code>,則編譯成Java代碼會變成符合Java模式的checked exception,即在方法定義上會顯式聲明可能抛出的異常類型,需要在調用鍊路上處理。對于Kotlin自身來說@Throws并沒有太多意義,it is only for Java developer to know that they need to handle that exception.參看[Kotlin] Try catch &amp; Throw

使用intellij idea進行kotlin和java混合開發,最好将kotlin檔案和java檔案分各自檔案夾存放,否則運作時可能會報找不到類的錯誤(因為編譯時會将不是屬于該檔案夾的且沒有被其它檔案引用的代碼檔案忽略)。如下:

【從零開始撸一個App】Kotlin

Kotlin協程 —— 今天說說 launch 與 async

Kotlin之美——DSL篇