376

用 Kotlin 协程把网络请求玩出花来

 6 years ago
source link: https://juejin.im/post/59cf35206fb9a00a5143b83f
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

用 Kotlin 协程把网络请求玩出花来

2017年09月30日 06:10 ·  阅读 6697

通常我们做网络请求的时候,几乎都是 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()
}复制代码

好了,这次的介绍到此为止,如果看官觉得玩得还不够花,那么你们也可以尝试一下哟

安装掘金浏览器插件
多内容聚合浏览、多引擎快捷搜索、多工具便捷提效、多模式随心畅享,你想要的,这里都有!
前往安装

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK