Kotlin 協程+Retrofit 最優雅的網路請求使用
Kotlin 協程+Retrofit 最優雅的網路請求使用
1.簡介
Retrofit對協程的支援非常的簡陋。在kotlin中使用不符合kotlin的優雅
```kotlin
interface TestServer {
@GET("banner/json")
suspend fun banner(): ApiResponse>
}
//實現並行捕獲異常的網路請求 fun oldBanner(){ viewModelScope.launch { //傳統模式使用retrofit需要try catch
val bannerAsync1 = async {
var result : ApiResponse<List<Banner>>? = null
kotlin.runCatching {
service.banner()
}.onFailure {
Log.e("banner",it.toString())
}.onSuccess {
result = it
}
result
}
val bannerAsync2 = async {
var result : ApiResponse<List<Banner>>? = null
kotlin.runCatching {
service.banner()
}.onFailure {
Log.e("banner",it.toString())
}.onSuccess {
result = it
}
result
}
bannerAsync1.await()
bannerAsync2.await()
}
}
```
一層巢狀一層,屬實無法忍受。kotlin應該一行程式碼解決問題,才符合kotlin的優雅
使用本框架後
```kotlin
interface TestServer {
@GET("banner/json")
suspend fun awaitBanner(): Await>
}
//實現並行捕獲異常的網路請求 fun parallel(){ viewModelScope.launch { val awaitBanner1 = service.awaitBanner().tryAsync(this) val awaitBanner2 = service.awaitBanner().tryAsync(this)
//兩個介面一起呼叫
awaitBanner1.await()
awaitBanner2.await()
} } ```
2.原始碼地址
3.檢視Retrofit原始碼
先看Retrofit create方法
```kotlin
public
@Override
public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
args = args != null ? args : emptyArgs;
return platform.isDefaultMethod(method)
? platform.invokeDefaultMethod(method, service, proxy, args)
: loadServiceMethod(method).invoke(args);//具體呼叫
}
});
} ```
loadServiceMethod(method).invoke(args)進入這個方法看具體呼叫
我們檢視suspenForResponse中的adapt
```kotlin
@Override
protected Object adapt(Call
//noinspection unchecked Checked by reflection inside RequestFactory.
Continuation<Response<ResponseT>> continuation =
(Continuation<Response<ResponseT>>) args[args.length - 1];
// See SuspendForBody for explanation about this try/catch.
try {
return KotlinExtensions.awaitResponse(call, continuation);
} catch (Exception e) {
return KotlinExtensions.suspendAndThrow(e, continuation);
}
}
} ```
後面直接交給協程去呼叫call。具體的okhttp呼叫在DefaultCallAdapterFactory。或者使用者自定義的callAdapterFactory中
因此我們這邊可以自定義CallAdapterFactory在呼叫後不進行網路請求的訪問,在使用者呼叫具體方法時候再進行網路請求訪問。
4.自定義CallAdapterFactory
Retrofit在呼叫後直接進行了網路請求,因此很不好操作。我們把網路請求的控制權放在我們手裡,就能隨意操作。
```kotlin
class ApiResultCallAdapterFactory : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array
//取出Await<T>中的T 也就是API返回資料對應的資料型別
// val dataType = getParameterUpperBound(0, apiResultType)
return ApiResultCallAdapter<Any>(apiResultType)
}
}
class ApiResultCallAdapter
override fun adapt(call: Call<T>): Call<Await<T>> {
return ApiResultCall(call)
}
}
class ApiResultCall
internal class AwaitImpl
override suspend fun await(): T {
return try {
call.await()
} catch (t: Throwable) {
throw t
}
}
} ```
通過上面自定義callAdapter後,我們延遲了網路請求,在呼叫Retrofit後並不會請求網路,只會將網路請求所需要的call的放入await中。
kotlin
@GET("banner/json")
suspend fun awaitBanner(): Await<List<Banner>>
我們拿到的Await>並沒有做網路請求。在這個實體類中包含了okHttp的call。
這時候我們可以定義如下方法就能捕獲異常
kotlin
suspend fun <T> Await<T>.tryAsync(
scope: CoroutineScope,
onCatch: ((Throwable) -> Unit)? = null,
context: CoroutineContext = SupervisorJob(scope.coroutineContext[Job]),
start: CoroutineStart = CoroutineStart.DEFAULT
): Deferred<T?> = scope.async(context, start) {
try {
await()
} catch (e: Throwable) {
onCatch?.invoke(e)
null
}
}
同樣並行捕獲異常的請求,就可以通過如下方式呼叫,優雅簡潔了很多 ```kotlin /* * 並行 async / fun parallel(){ viewModelScope.launch { val awaitBanner1 = service.awaitBanner().tryAsync(this) val awaitBanner2 = service.awaitBanner().tryAsync(this)
//兩個介面一起呼叫
awaitBanner1.await()
awaitBanner2.await()
}
}
```
這時候我們發現網路請求成功了,解析資料失敗。因為我們在資料外面套了一層await。肯定無法解析成功。
本著哪裡錯誤解決哪裡的思路,我們自定義Gson解析
5.自定義Gson解析
```kotlin class GsonConverterFactory private constructor(private var responseCz : Class<*>,var responseConverter : GsonResponseBodyConverter, private val gson: Gson) : Converter.Factory() {
override fun responseBodyConverter(
type: Type, annotations: Array<Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *> {
var adapter : TypeAdapter<*>? = null
//檢查是否是Await<T>
if (Utils.getRawType(type) == Await::class.java && type is ParameterizedType){
//取出Await<T>中的T
val awaitType = Utils.getParameterUpperBound(0, type)
if(awaitType != null){
adapter = gson.getAdapter(TypeToken.get(ParameterizedTypeImpl[responseCz,awaitType]))
}
}
//不是awiat正常解析,相容正常模式
if(adapter == null){
adapter= gson.getAdapter(TypeToken.get(ParameterizedTypeImpl[responseCz,type]))
}
return responseConverter.init(gson, adapter!!)
}
}
class MyGsonResponseBodyConverter : GsonResponseBodyConverter() {
override fun convert(value: ResponseBody): Any {
val jsonReader = gson.newJsonReader(value.charStream())
val data = adapter.read(jsonReader) as ApiResponse<*>
val t = data.data
val listData = t as? ApiPagerResponse<*>
if (listData != null) {
//如果返回值值列表封裝類,且是第一頁並且空資料 那麼給空異常 讓介面顯示空
if (listData.isRefresh() && listData.isEmpty()) {
throw ParseException(NetConstant.EMPTY_CODE, data.errorMsg)
}
}
// errCode 不等於 SUCCESS_CODE,丟擲異常
if (data.errorCode != NetConstant.SUCCESS_CODE) {
throw ParseException(data.errorCode, data.errorMsg)
}
return t!!
}
} ```
6.本框架使用
新增依賴
groovy
implementation "io.github.cnoke.ktnet:api:?"
寫一個網路請求資料基類
kotlin
open class ApiResponse<T>(
var data: T? = null,
var errorCode: String = "",
var errorMsg: String = ""
)
實現com.cnoke.net.factory.GsonResponseBodyConverter
```kotlin class MyGsonResponseBodyConverter : GsonResponseBodyConverter() {
override fun convert(value: ResponseBody): Any {
val jsonReader = gson.newJsonReader(value.charStream())
val data = adapter.read(jsonReader) as ApiResponse<*>
val t = data.data
val listData = t as? ApiPagerResponse<*>
if (listData != null) {
//如果返回值值列表封裝類,且是第一頁並且空資料 那麼給空異常 讓介面顯示空
if (listData.isRefresh() && listData.isEmpty()) {
throw ParseException(NetConstant.EMPTY_CODE, data.errorMsg)
}
}
// errCode 不等於 SUCCESS_CODE,丟擲異常
if (data.errorCode != NetConstant.SUCCESS_CODE) {
throw ParseException(data.errorCode, data.errorMsg)
}
return t!!
}
} ```
進行網路請求
```kotlin
interface TestServer {
@GET("banner/json")
suspend fun awaitBanner(): Await>
}
val okHttpClient = OkHttpClient.Builder() .addInterceptor(HeadInterceptor()) .addInterceptor(LogInterceptor()) .build()
val retrofit = Retrofit.Builder() .client(okHttpClient) .baseUrl("http://www.wanandroid.com/") .addCallAdapterFactory(ApiResultCallAdapterFactory()) .addConverterFactory(GsonConverterFactory.create(ApiResponse::class.java,MyGsonResponseBodyConverter())) .build() val service: TestServer = retrofit.create(TestServer::class.java) lifecycleScope.launch { val banner = service.awaitBanner().await() } ```
非同步請求同步請求,異常捕獲參考如下try開頭的會捕獲異常,非try開頭不會捕獲。
```kotlin fun banner(){ lifecycleScope.launch { //單獨處理異常 tryAwait會處理異常,如果異常返回空 val awaitBanner = service.awaitBanner().tryAwait() awaitBanner?.let { for(banner in it){ Log.e("awaitBanner",banner.title) } }
/**
* 不處理異常 異常會直接丟擲,統一處理
*/
val awaitBannerError = service.awaitBanner().await()
}
}
/* * 序列 await / fun serial(){ lifecycleScope.launch { //先呼叫第一個介面await val awaitBanner1 = service.awaitBanner().await() //第一個介面完成後呼叫第二個介面 val awaitBanner2 = service.awaitBanner().await() } }
/* * 並行 async / fun parallel(){ lifecycleScope.launch { val awaitBanner1 = service.awaitBanner().async(this) val awaitBanner2 = service.awaitBanner().async(this)
//兩個介面一起呼叫
awaitBanner1.await()
awaitBanner2.await()
}
} ```