用 Kotlin 協程把網絡請求玩出花來

前言

一般咱們作網絡請求的時候,幾乎都是 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()
}複製代碼

好了,此次的介紹到此爲止,若是看官以爲玩得還不夠花,那麼大家也能夠嘗試一下喲

相關文章
相關標籤/搜索