前言
通常我們做網路請求的時候,幾乎都是 callback 的形式:
request.execute(callback)複製程式碼
callback = {
onSuccess = { res ->
// TODO
}
onFail = { error ->
// TODO
}
}複製程式碼
長久以來,我都習慣了這樣子的寫法。即便遇到困難,有過質疑,但仍然不知道能有什麼樣的替代方式。也許有的小夥伴會說 RxJava,沒錯,RxJava 在一定程度上確實可以緩解一下 callback 方式帶來的一些麻煩,但本質上subscriber 真的脫離 callback 了嗎?
request.subscribe(subscriber)
...
subscriber = ...複製程式碼
request.subscribe({
// TODO Success
}, {
// TODO Error
})複製程式碼
相比之下,Kotlin 提供的非同步方式更為清爽。程式碼沒有被割裂成兩塊甚至 N 塊,邏輯還是順序的。
doAsync {
val response = request.execute()
uiThread {
// TODO
}
}複製程式碼
當然這不是我這次想要說的重點,這畢竟還只是前言
####初見
前些日子學習了一下 Kotlin 的協程,坦白的講,雖然我明白了協程的概念和一定程度的理論,但是一下子讓我看那麼多那麼複雜的 API,我感覺頭好暈(其實是懶)。
關於協程是什麼,建議小夥伴們自行 google。
偶然的一天,聽朋友說 anko 支援協程了,我一下子就興奮了起來,馬上前往 github 打算觀摩一番。至於我為什麼興奮,瞭解 anko 的人應該都懂。可當我真正開啟 anko-coroutines 的 wiki 之後,我震驚了,因為在我的觀念中這麼複雜的協程,wiki 居然只寫了兩個函式的介紹?
看到這裡估計很多小夥伴要不耐煩了,好吧,我們們進入 code 時間:
fun getData(): Data { ... }
fun showData(data: Data) { ... }
async(UI) {
val data: Deferred<Data> = bg {
// Runs in background
getData()
}
// This code is executed on the UI thread
showData(data.await())
}複製程式碼
讓我們暫且忽略掉最外層的 async(UI) :
val data: Deferred<Data> = bg {
// Runs in background
getData()
}
// This code is executed on the UI thread
showData(data.await())複製程式碼
註釋說的很清楚,bg {} 所包裹的 getData() 函式是跑在 background 的,可是接下來在 UI thread 上執行的程式碼居然直接引用了 getData 返回的物件??這於理不合吧??
聰明的小夥伴從程式碼上或許已經看出端倪了,那就是 bg {} 包裹的程式碼快最終返回的是一個 Deferred 物件,而這個 Deferred 物件的 await 函式在這裡起到了關鍵作用 —— 阻塞當前的協程,等待結果。
而至於被我們暫且忽略的 async(UI) {} ,則是指在 UI 執行緒上開闢一條非同步的協程任務。因為是非同步的,哪怕被阻塞了也不會導致整個 UI 執行緒阻塞;因為還是在 UI 執行緒上的,所以我們可以放心的做 UI 操作。相應的,bg {} 其實可以理解為 async(BACKGROUND) {},所以才可以在 Android 上做網路請求。
所以,上面的程式碼其實是 UI 執行緒上的 ui 協程,和 BG 執行緒上的 bg 協程之間的小故事。
對比
比起之前的 doAsync -- uiThread 程式碼,看著很像,但也僅僅是像而已。doAsync 是開闢一條新的執行緒,在這個執行緒中你寫的程式碼不可能再和 doAsync 外部的執行緒同步上,要想產生關聯,就得通過之前的 callback 方式。
而通過上面的程式碼我們已經看到,採用協程的方式,我們卻可以讓協程等待另一個協程,哪怕這另一個協程還是屬於另一個執行緒的。
能夠用寫同步程式碼的方式去寫非同步的任務,想必這是不少人喜歡協程的一大原因。在這裡我嘗試了一下,用協程配合 Retrofit 做網路請求:
asyncUI {
val deferred = bg {
// 在 BG 執行緒的 bg 協程中呼叫介面
Server.getApiStore().login("173176360", "123456").execute()
}
// 模擬彈出載入進度條之類的操作,反正是在 UI 執行緒上搞事
textView.text = "loading"
// 等待介面呼叫的結果
val response = deferred.await()
// 根據介面呼叫狀況做處理,反正是在 UI 執行緒,隨便玩
if (response.isSuccessful) {
textView.text = response.body().toString()
} else {
toast(response.errorBody().string())
}
}複製程式碼
怕你們沒耐心,我想說的話都在註釋裡了。
正文
吃瓜群眾:什麼?這才到正文嗎?
在下:當然,就上面那點內容,我好意思說玩出花?
好了,調侃歸調侃,我還是得說,如果就只是上面那一段程式碼,價值也是有的,但真不大。因為相對於傳統 callback 而言的優勢還沒能展現出來。那優勢怎麼展現呢?請看程式碼:
async(UI) {
// 假設這是兩個不同的 api 請求
val deferred1 = bg {
Server.getApiStore().login("173176360", "123456").execute()
}
val deferred2 = bg {
Server.getApiStore().login("173176360", "123456").execute()
}
val res1 = deferred1.await()
val res2 = deferred2.await()
// 此時兩個請求都完成了
textView.text = res1.body().toString() + res2.body().toString()
}複製程式碼
看見了嗎?要知道我這還沒做任何封裝,像這樣的邏輯,哪怕是 RxJava 也不能寫得如此簡單。這就是用同步的程式碼寫非同步任務的魅力。
想想我們以前是怎麼寫這樣的邏輯的?如果再多來幾個這樣的呢?callback hell 是不是就有了?
稍作封裝,我們能見到這樣的請求:
asyncUI {
val deferred = bg {
Server.getApiStore().login("173176360", "123456").execute()
}
textView.text = "loading"
// 接收 response.body 如有異常則 toast 出來
val info = deferred.wait(TOAST) // or Log
// 因為有, 能走到這裡一定是沒有異常
textView.text = info.toString()
}複製程式碼
等待的同時新增一種預設的處理異常的方式,不用每次都中斷流暢的邏輯,寫 if-else 程式碼。
有人說:除了 toast 和 log,異常的時候我還想做別的事咋辦?
asyncUI {
val deferred = bg {
Server.getApiStore().login("173176360", "123456").execute()
}
textView.text = "loading"
val info = deferred.handleException {
// 自定義異常處理,足夠靈活 (it == errorBody)
toast(it.string())
}
textView.text = info.toString()
}複製程式碼
又有人說,你這樣子讓我很難辦啊,如果我成功失敗時的做的事情都一樣,那不是同樣的程式碼要寫兩份?
asyncUI {
val deferred = bg {
Server.getApiStore().login("173176360", "123456").execute()
}
textView.text = "loading"
// 我不關心返回來的是成功還是失敗,也不關心返回的引數
// 我需要的是請求完成(包括成功、失敗)後執行後續任務
deferred.wait(THROUGH)
// type 為 through,即就算有異常發生也會走到這裡來
textView.text = "done"
}複製程式碼
如果我只是想複用部分程式碼,成功失敗還是有不同的呢?那您老還是用最原始的 await 函式吧。。當然,我這裡還是封裝了一下的,至少可以將 Response 轉化為 Data,多多少少省點心
asyncUI {
val deferred = bg {
Server.getApiStore().login("1731763609", "123456").execute()
}
textView.text = "loading"
// 我不關心返回來的是成功還是失敗,也不關心返回的引數
// 我需要的是請求完成(包括成功、失敗)後執行後續任務
val info = deferred.wait(THROUGH)
// type 為 through,即就算有異常發生也會走到這裡來
textView.text = "done"
if (info.isSuccess) {
// TODO 成功
} else {
// TODO 失敗
}
}複製程式碼
結合上面的多個 api 請求的狀況
asyncUI {
// 假設這是兩個不同的 api 請求
val deferred1 = bg {
Server.getApiStore().login("173176360", "123456").execute()
}
val deferred2 = bg {
Server.getApiStore().login("173176360", "123456").execute()
}
// 後臺請求著 api,此時我還可以在 UI 協程中做我想做的事情
textView.text = "loading"
delay(5, TimeUnit.SECONDS)
// 等 UI 協程中的事情做完了,專心等待 api 請求完成(其實 api 請求有可能已經完成了)
// 通過提供 ExceptionHandleType 進行異常的過濾
val response = deferred1.wait(TOAST)
deferred2.wait(THROUGH) // deferred2 的結果我不關心
// 此時兩個請求肯定都完成了,並且 deferred1 沒有異常發生
textView.text = response.toString()
}複製程式碼
好了,這次的介紹到此為止,如果看官覺得玩得還不夠花,那麼你們也可以嘗試一下喲