Android开发:用Kotlin Coroutine(协程)和Retrofit进行网络请求和取消请求
介绍在Android
中如何使用协程配合Retrofit
发起网络请求,同时介绍在使用协程时如何优雅的取消已经发起的网络请求。
此篇文章的Demo地址:
创建CoroutineScope
在前面的文章中我写到方法是一个很常用的协程构建器。因此使用协程必须先得创建一个
CoroutineScope
对象,代码如下:
CoroutineScope( + Job())
复制代码
上面的代码创建了一个CoroutineScope
对象,为其协程指定了在主线程中执行,同时分配了一个Job
在demo中我使用的是MVP模式写的,所以我将CoroutineScope
的创建放到了BasePresenter
中,代码如下:
interfaceMvpViewinterfaceMvpPresenter<V: MvpView> {
@UiThreadfunattachView(view: V)@UiThreadfundetachView()
}
openclassBasePresenter<V: MvpView> : MvpPresenter<V> {
lateinitvar view: V
val presenterScope: CoroutineScope by lazy {
CoroutineScope( + Job())
}
overridefunattachView(view: V) {
this.view = view
}
overridefundetachView() {
()
}
}
复制代码
使用()取消协程
大家应该可以看到上面中调用了
()
,那这个方法有什么作用呢,作用就是取消掉presenterScope
创建的所有协程和其子协程。
前面的文章我也介绍过使用launch
创建协程时会返回一个Job
对象,通过Job
对象的cancel
方法也可以取消该任务对应的协程,那我这里为什么不使用这种方式呢?
很明显,如果使用()
方式取消协程,那我创建每个协程的时候都必须保存返回的Job
对象,然后再去取消,显然要更复杂点,而使用()
则可以一次性取消该协程上下文创建的所有协程和子协程,该代码也可以很方便的提取到基类中,这样后面在写业务代码时也就不用关心协程与View的生命周期的问题。
其实大家看源码的话也可以发现()
最终使用的也是()
取消协程
扩展适配协程
interfaceApiService{
@GET("data/iOS/2/1")fungetIOSGank(): Call<GankResult>
@GET("data/Android/2/1")fungetAndroidGank(): Call<GankResult>
}
classApiSource{
companionobject {
@JvmFieldval instance = ()
.baseUrl("")
.addConverterFactory(())
.build().create(ApiService::class.java)
}
}
复制代码
大家可以看到上面的api接口定义应该很熟悉,我们可以通过下面的代码发起异步网络请求
().enqueue(object : Callback<T> {
overridefunonFailure(call: Call<T>, t: Throwable) {
}
overridefunonResponse(call: Call<T>, response: Response<T>) {
}
})
复制代码
前面的文章介绍过协程可以让异步代码像写同步代码那样方便,那上面这段异步代码能不能使用协程改造成类似写同步代码块那样呢?很显然是可以的,具体改造代码如下:
suspend fun<T> Call<T>.await(): T {
return suspendCoroutine {
enqueue(object : Callback<T> {
overridefunonFailure(call: Call<T>, t: Throwable) {
(t)
}
overridefunonResponse(call: Call<T>, response: Response<T>) {
if() {
(()!!)
} else{
(Throwable(()))
}
}
})
}
}
复制代码
上面的代码扩展了一个挂起函数await
,执行该方法时,会执行的异步请求同时在协程中挂起该函数,直到异步请求成功或者出错再重新恢复所在协程。
suspendCoroutine
全局函数,此函数可以获取当前方法所在协程上下文,并将当前协程挂起,直到某个时机再重新恢复协程执行,但是这个时机其实是由开发者自己控制的,就像上面代码中的和
。
发起请求,写法一
{
val time = ()
()
try {
val ganks = queryGanks()
(ganks)
} catch (e: Throwable) {
()
} finally {
(TAG, "耗时:${() - time}")
}
}
suspend funqueryGanks(): List<Gank> {
returntry {
val androidResult = ().await()
val iosResult = ().await()
val result = mutableListOf<Gank>().apply {
addAll()
addAll()
}
result
} catch (e: Throwable) {
()
throw e
}
}
复制代码
从上面的代码大家可以发现,协程中对异常的处理使用的是try-catch
的方式,初学,我也暂时只想到了这种方式。所以在使用协程时,最好在业务的适当地方使用try-catch
捕获异常,否则一旦协程执行出现异常,程序就崩掉了。
另外上面的代码的写法还有一个问题,因为挂起函数执行时会挂起当前协程,所以上述两个请求是依次顺序执行,因此上面的queryGanks()
方法其实是耗费了两次网络请求的时间,因为请求Android列表和请求ios列表两个请求不是并行的,所以这种写法肯定不是最优解。
发起请求,写法二
下面我们再换另外一种写法。
suspend funqueryGanks(): List<Gank> {
return withContext() {
try {
val androidDeferred = async {
val androidResult = ().await()
androidResult
}
val iosDeferred = async {
val iosResult = ().await()
iosResult
}
val androidResult = ().results
val iosResult = ().results
val result = mutableListOf<Gank>().apply {
addAll(iosResult)
addAll(androidResult)
}
result
} catch (e: Throwable) {
()
throw e
}
}
}
复制代码
这种写法与前一种写法的区别是采用async
构建器创建了两个子协程分别去请求Android列表和IOS列表,同时因为async
构建器执行的时候不会挂起当前协程,所以两个请求是并行执行的,因此效率较上一个写法要高很多。
发起请求,写法三
第三个写法就是在Retorfit
的CallAdapter
上做文章,通过自定义实现CallAdapterFactory
,将api定义时的结果Call
直接转换成Deferred
,这样就可以同时发起Android列表请求和IOS列表请求,然后通过获取请求结果,这种写法是写法一写法二的结合。
这种写法JakeWharton
大神早已为我们实现了,地址在这…
这里我就不说这种方案的具体实现了,感兴趣的同学可以去看其源码。
写法三的具体代码如下:
val instance = ()
.baseUrl("")
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.addConverterFactory(())
.build().create(CallAdapterApiService::class.java)
suspend funqueryGanks(): List<Gank> {
return withContext() {
try {
val androidDeferred = ()
val iosDeferred = ()
val androidResult = ().results
val iosResult = ().results
val result = mutableListOf<Gank>().apply {
addAll(iosResult)
addAll(androidResult)
}
result
} catch (e: Throwable) {
()
throw e
}
}
}
复制代码
上面的第三种写法看起来更简洁,也是并行请求,耗时为请求时间最长的那个请求的时间,和第二种差不多。