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

前言

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容