Kotlin Coroutine(协程)系列:
1. Kotlin Coroutine(协程) 简介
2. Kotlin Coroutine(协程) 基本知识
3. Android中用Kotlin Coroutine(协程)和Retrofit进行网络请求和取消请求java
前面两篇文章介绍了协程的一些基本概念和基本知识,这篇则介绍在Android
中如何使用协程配合Retrofit
发起网络请求,同时介绍在使用协程时如何优雅的取消已经发起的网络请求。android
此篇文章的Demo地址:https://github.com/huyongli/AndroidKotlinCoroutineios
在前面的文章中我写到CoroutineScope.launch
方法是一个很经常使用的协程构建器。所以使用协程必须先得建立一个CoroutineScope
对象,代码以下:git
CoroutineScope(Dispatchers.Main + Job())
复制代码
上面的代码建立了一个CoroutineScope
对象,为其协程指定了在主线程中执行,同时分配了一个Job
github
在demo中我使用的是MVP模式写的,因此我将CoroutineScope
的建立放到了BasePresenter
中,代码以下:api
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()
}
}
复制代码
你们应该能够看到上面BasePresenter.detachView
中调用了presenterScope.cancel()
,那这个方法有什么做用呢,做用就是取消掉presenterScope
建立的全部协程和其子协程。网络
前面的文章我也介绍过使用launch
建立协程时会返回一个Job
对象,经过Job
对象的cancel
方法也能够取消该任务对应的协程,那我这里为何不使用这种方式呢?app
很明显,若是使用Job.cancel()
方式取消协程,那我建立每一个协程的时候都必须保存返回的Job
对象,而后再去取消,显然要更复杂点,而使用CoroutineScope.cancel()
则能够一次性取消该协程上下文建立的全部协程和子协程,该代码也能够很方便的提取到基类中,这样后面在写业务代码时也就不用关心协程与View的生命周期的问题。异步
其实你们看源码的话也能够发现CoroutineScope.cancel()
最终使用的也是Job.cancel()
取消协程async
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
的异步请求同时在协程中挂起该函数,直到异步请求成功或者出错再从新恢复所在协程。
全局函数,此函数能够获取当前方法所在协程上下文,并将当前协程挂起,直到某个时机再从新恢复协程执行,可是这个时机实际上是由开发者本身控制的,就像上面代码中的it.resume
和it.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
构建器执行的时候不会挂起当前协程,因此两个请求是并行执行的,所以效率较上一个写法要高不少。
第三个写法就是在Retorfit
的CallAdapter
上作文章,经过自定义实现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的地址见文章开头,有兴趣的能够看看。