Android中用Kotlin Coroutine(协程)和Retrofit进行网络请求和取消请求

栏目: IOS · Android · 发布时间: 5年前

内容简介:前面两篇文章介绍了协程的一些基本概念和基本知识,这篇则介绍在此篇文章的Demo地址在前面的文章中我写到

前面两篇文章介绍了协程的一些基本概念和基本知识,这篇则介绍在 Android 中如何使用协程配合 Retrofit 发起网络请求,同时介绍在使用协程时如何优雅的取消已经发起的网络请求。

此篇文章的Demo地址 :https://github.com/huyongli/AndroidKotlinCoroutine

创建CoroutineScope

在前面的文章中我写到 CoroutineScope.launch 方法是一个很常用的协程构建器。因此使用协程必须先得创建一个 CoroutineScope 对象,代码如下:

CoroutineScope(Dispatchers.Main + Job())
复制代码

上面的代码创建了一个 CoroutineScope 对象,为其协程指定了在主线程中执行,同时分配了一个 Job

在demo中我使用的是MVP模式写的,所以我将 CoroutineScope 的创建放到了 BasePresenter 中,代码如下:

interface MvpView

interface MvpPresenter<V: MvpView> {

    @UiThread
    fun attachView(view: V)

    @UiThread
    fun detachView()
}

open class BasePresenter<V: MvpView> : MvpPresenter<V> {
    lateinit var view: V
    val presenterScope: CoroutineScope by lazy {
        CoroutineScope(Dispatchers.Main + Job())
    }

    override fun attachView(view: V) {
        this.view = view
    }

    override fun detachView() {
        presenterScope.cancel()
    }
}
复制代码

使用CoroutineScope.cancel()取消协程

大家应该可以看到上面 BasePresenter.detachView 中调用了 presenterScope.cancel() ,那这个方法有什么作用呢,作用就是取消掉 presenterScope 创建的所有协程和其子协程。

前面的文章我也介绍过使用 launch 创建协程时会返回一个 Job 对象,通过 Job 对象的 cancel 方法也可以取消该任务对应的协程,那我这里为什么不使用这种方式呢?

很明显,如果使用 Job.cancel() 方式取消协程,那我创建每个协程的时候都必须保存返回的 Job 对象,然后再去取消,显然要更复杂点,而使用 CoroutineScope.cancel() 则可以一次性取消该协程上下文创建的所有协程和子协程,该代码也可以很方便的提取到基类中,这样后面在写业务代码时也就不用关心协程与View的生命周期的问题。

其实大家看源码的话也可以发现 CoroutineScope.cancel() 最终使用的也是 Job.cancel() 取消协程

扩展Retrofit.Call适配协程

interface ApiService {
    @GET("data/iOS/2/1")
    fun getIOSGank(): Call<GankResult>

    @GET("data/Android/2/1")
    fun getAndroidGank(): Call<GankResult>
}

class ApiSource {
    companion object {
        @JvmField
        val instance = Retrofit.Builder()
            .baseUrl("http://gank.io/api/")
            .addConverterFactory(GsonConverterFactory.create())
            .build().create(ApiService::class.java)
    }
}
复制代码

大家可以看到上面的api接口定义应该很熟悉,我们可以通过下面的代码发起异步网络请求

ApiSource.instance.getAndroidGank().enqueue(object : Callback<T> {
    override fun onFailure(call: Call<T>, t: Throwable) {
        
    }

    override fun onResponse(call: Call<T>, response: Response<T>) {
        
    }
})
复制代码

前面的文章介绍过协程可以让异步代码像写同步代码那样方便,那上面这段异步代码能不能使用协程改造成类似写同步代码块那样呢?很显然是可以的,具体改造代码如下:

//扩展Retrofit.Call类,为其扩展一个await方法,并标识为挂起函数
suspend fun <T> Call<T>.await(): T {
    return suspendCoroutine {
        enqueue(object : Callback<T> {
            override fun onFailure(call: Call<T>, t: Throwable) {
                //请求失败,抛出异常,手动结束当前协程
                it.resumeWithException(t)
            }

            override fun onResponse(call: Call<T>, response: Response<T>) {
                if(response.isSuccessful) {
                   //请求成功,将请求结果拿到并手动恢复所在协程
                   it.resume(response.body()!!)
                } else{
                   //请求状态异常,抛出异常,手动结束当前协程
                   it.resumeWithException(Throwable(response.toString()))
                }
            }
        })
    }
}
复制代码

上面的代码扩展了一个挂起函数 await ,执行该方法时,会执行 Retrofit.Call 的异步请求同时在协程中挂起该函数,直到异步请求成功或者出错再重新恢复所在协程。

suspendCoroutine

全局函数,此函数可以获取当前方法所在协程上下文,并将当前协程挂起,直到某个时机再重新恢复协程执行,但是这个时机其实是由开发者自己控制的,就像上面代码中的 it.resumeit.resumeWithException

发起请求,写法一

//使用CoroutineScope.launch创建一个协程,此协程在主线程中执行
presenterScope.launch {
    val time = System.currentTimeMillis()
    view.showLoadingView()
    try {
        val ganks = queryGanks()
        view.showLoadingSuccessView(ganks)
    } catch (e: Throwable) {
        view.showLoadingErrorView()
    } finally {
        Log.d(TAG, "耗时:${System.currentTimeMillis() - time}")
    }
}

suspend fun queryGanks(): List<Gank> {
    //此方法执行线程和调用者保持一致,因此也是在主线程中执行
    return try {
        //先查询Android列表,同时当前协程执行流程挂起在此处
        val androidResult = ApiSource.instance.getAndroidGank().await()
        
        //Android列表查询完成之后恢复当前协程,接着查询IOS列表,同时将当前协程执行流程挂起在此处
        val iosResult = ApiSource.instance.getIOSGank().await()

        //Android列表和IOS列表都查询结束后,恢复协程,将两者结果合并,查询结束
        val result = mutableListOf<Gank>().apply {
            addAll(iosResult.results)
            addAll(androidResult.results)
        }
        result
    } catch (e: Throwable) {
        //处理协程中的异常,否则程序会崩掉
        e.printStackTrace()
        throw e
    }
}
复制代码

从上面的代码大家可以发现,协程中对异常的处理使用的是 try-catch 的方式,初学,我也暂时只想到了这种方式。所以在使用协程时,最好在业务的适当地方使用 try-catch 捕获异常,否则一旦协程执行出现异常,程序就崩掉了。

另外上面的代码的写法还有一个问题,因为挂起函数执行时会挂起当前协程,所以上述两个请求是依次顺序执行,因此上面的 queryGanks() 方法其实是耗费了两次网络请求的时间,因为请求Android列表和请求ios列表两个请求不是并行的,所以这种写法肯定不是最优解。

发起请求,写法二

下面我们再换另外一种写法。

suspend fun queryGanks(): List<Gank> {
    /**
     * 此方法执行线程和调用者保持一致,因此也在主线程中执行
     * 因为网络请求本身是异步请求,同时async必须在协程上下文中执行,所以此方法实现中采用withContext切换执行线程到主线程,获取协程上下文对象
     */
    return withContext(Dispatchers.Main) {
        try {
            //在当前协程中创建一个新的协程发起Android列表请求,但是不会挂起当前协程
            val androidDeferred = async {
                val androidResult = ApiSource.instance.getAndroidGank().await()
                androidResult
            }

            //发起Android列表请求后,立刻又在当前协程中创建了另外一个子协程发起ios列表请求,也不会挂起当前协程
            val iosDeferred = async {
                val iosResult = ApiSource.instance.getIOSGank().await()
                iosResult
            }

            val androidResult = androidDeferred.await().results
            val iosResult = iosDeferred.await().results

            //两个列表请求并行执行,等待两个请求结束之后,将请求结果进行合并
            //此时当前方法的执行时间实际上两个请求中耗时时间最长的那个,而不是两个请求所耗时间的总和,因此此写法优于上面一种写法
            val result = mutableListOf<Gank>().apply {
                addAll(iosResult)
                addAll(androidResult)
            }
            result
        } catch (e: Throwable) {
            e.printStackTrace()
            throw e
        }
    }
}
复制代码

这种写法与前一种写法的区别是采用 async 构建器创建了两个子协程分别去请求Android列表和IOS列表,同时因为 async 构建器执行的时候不会挂起当前协程,所以两个请求是并行执行的,因此效率较上一个写法要高很多。

发起请求,写法三

第三个写法就是在 RetorfitCallAdapter 上做文章,通过自定义实现 CallAdapterFactory ,将api定义时的结果 Call 直接转换成 Deferred ,这样就可以同时发起Android列表请求和IOS列表请求,然后通过 Deferred.await 获取请求结果,这种写法是写法一写法二的结合。

这种写法 JakeWharton 大神早已为我们实现了,地址在这 github.com/JakeWharton…

这里我就不说这种方案的具体实现了,感兴趣的同学可以去看其源码。

写法三的具体代码如下:

val instance = Retrofit.Builder()
        .baseUrl("http://gank.io/api/")
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .addConverterFactory(GsonConverterFactory.create())
        .build().create(CallAdapterApiService::class.java)
        
suspend fun queryGanks(): List<Gank> {
    return withContext(Dispatchers.Main) {
        try {
            val androidDeferred = ApiSource.callAdapterInstance.getAndroidGank()

            val iosDeferred = ApiSource.callAdapterInstance.getIOSGank()

            val androidResult = androidDeferred.await().results

            val iosResult = iosDeferred.await().results

            val result = mutableListOf<Gank>().apply {
                addAll(iosResult)
                addAll(androidResult)
            }
            result
        } catch (e: Throwable) {
            e.printStackTrace()
            throw e
        }
    }
}
复制代码

上面的第三种写法看起来更简洁,也是并行请求,耗时为请求时间最长的那个请求的时间,和第二种差不多。

具体实现demo的地址见文章开头,有兴趣的可以看看。


以上所述就是小编给大家介绍的《Android中用Kotlin Coroutine(协程)和Retrofit进行网络请求和取消请求》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Java Servlet&JSP经典实例

Java Servlet&JSP经典实例

(美)佩里 / 朱涛江、邹红霞、林琪 / 中国电力出版社 / 2005-7 / 86.00元

本书将用于帮助指导Java web开发人员的日常任务,提供典型的web相关问题的快速解决方案。本书集中介绍了如何用Java初始化某些与web相关的任务,而不是教会读者如何使用Java语言,或者事无巨细地解释servlet和JSP API。书中包含了大量关于复杂的日常开发任务的技巧,这些技巧涵盖了许多与Servlet 2.4和JSP 2.0规范相关联的新特性,包括ServletRequestList......一起来看看 《Java Servlet&JSP经典实例》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

SHA 加密
SHA 加密

SHA 加密工具

html转js在线工具
html转js在线工具

html转js在线工具