一般咱們作網絡請求的時候,幾乎都是 callback 的形式:git
request.execute(callback)複製代碼
callback = {
onSuccess = { res ->
// TODO
}
onFail = { error ->
// TODO
}
}複製代碼
長久以來,我都習慣了這樣子的寫法。即使遇到困難,有過質疑,但仍然不知道能有什麼樣的替代方式。也許有的小夥伴會說 RxJava,沒錯,RxJava 在必定程度上確實能夠緩解一下 callback 方式帶來的一些麻煩,但本質上subscriber 真的脫離 callback 了嗎?github
request.subscribe(subscriber)
...
subscriber = ...複製代碼
request.subscribe({
// TODO Success
}, {
// TODO Error
})複製代碼
相比之下,Kotlin 提供的異步方式更爲清爽。代碼沒有被割裂成兩塊甚至 N 塊,邏輯仍是順序的。api
doAsync {
val response = request.execute()
uiThread {
// TODO
}
}複製代碼
固然這不是我此次想要說的重點,這畢竟還只是前言bash
####初見
前些日子學習了一下 Kotlin 的協程,坦白的講,雖然我明白了協程的概念和必定程度的理論,可是一會兒讓我看那麼多那麼複雜的 API,我感受頭好暈(實際上是懶)。網絡
關於協程是什麼,建議小夥伴們自行 google。異步
偶然的一天,聽朋友說 anko 支持協程了,我一會兒就興奮了起來,立刻前往 github 打算觀摩一番。至於我爲何興奮,瞭解 anko 的人應該都懂。可當我真正打開 anko-coroutines 的 wiki 以後,我震驚了,由於在個人觀念中這麼複雜的協程,wiki 竟然只寫了兩個函數的介紹?async
看到這裏估計不少小夥伴要不耐煩了,好吧,我們進入 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 返回的對象??這於理不合吧??ui
聰明的小夥伴從代碼上或許已經看出端倪了,那就是 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()
}複製代碼
好了,此次的介紹到此爲止,若是看官以爲玩得還不夠花,那麼大家也能夠嘗試一下喲