用 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()
}复制代码

好了,此次的介绍到此为止,若是看官以为玩得还不够花,那么大家也能够尝试一下哟

相关文章
相关标签/搜索