前言
通常我们做网络请求的时候,几乎都是 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> 转化为 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()
}
好了,这次的介绍到此为止,如果看官觉得玩得还不够花,那么你们也可以尝试一下哟