美文网首页KotlinKotlin
用 Kotlin 协程把网络请求玩出花来

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

作者: neverwoods | 来源:发表于2017-09-28 16:02 被阅读401次

    前言

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

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

    相关文章

      网友评论

      • 78f78f232537:很胖很胖
      • cheetah747:请问你写的那个async(UI){}是什么????怎么我写了通不过编译?UI是常量吗?有点迷糊。
        neverwoods:如果还不行,还是得去 github 上看 anko 的 wiki
        neverwoods:import kotlinx.coroutines.experimental.android.UI
        val UI = HandlerContext(Handler(Looper.getMainLooper()), "UI")
        这两行你应该就能看出 UI 是什么了。

        gradle 记得加上:
        compile "org.jetbrains.anko:anko-coroutines:$anko_version"

        kotlin {
        experimental {
        coroutines "enable"
        }
        }
      • xiasuhuei321:先赞再看好习惯

      本文标题:用 Kotlin 协程把网络请求玩出花来

      本文链接:https://www.haomeiwen.com/subject/gpedextx.html