Kotlin 協程+Retrofit 最優雅的網路請求使用

語言: CN / TW / HK

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.原始碼地址

GitHub

3.檢視Retrofit原始碼

先看Retrofit create方法

```kotlin public T create(final Class service) { validateServiceInterface(service); return (T) Proxy.newProxyInstance( service.getClassLoader(), new Class<?>[] {service}, new InvocationHandler() { private final Platform platform = Platform.get(); private final Object[] emptyArgs = new Object[0];

          @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)進入這個方法看具體呼叫

20220110110008.png

20220110110243.png

我們檢視suspenForResponse中的adapt

```kotlin @Override protected Object adapt(Call call, Object[] args) { call = callAdapter.adapt(call);//如果使用者不設定callAdapterFactory就使用DefaultCallAdapterFactory

  //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, retrofit: Retrofit): CallAdapter<, >? { //檢查returnType是否是Call型別的 if (getRawType(returnType) != Call::class.java) return null check(returnType is ParameterizedType) { "$returnType must be parameterized. Raw types are not supported" } //取出Call 裡的T,檢查是否是Await val apiResultType = getParameterUpperBound(0, returnType) // 如果不是 Await 則不由本 CallAdapter.Factory 處理 相容正常模式 if (getRawType(apiResultType) != Await::class.java) return null check(apiResultType is ParameterizedType) { "$apiResultType must be parameterized. Raw types are not supported" }

    //取出Await<T>中的T 也就是API返回資料對應的資料型別

// val dataType = getParameterUpperBound(0, apiResultType)

    return ApiResultCallAdapter<Any>(apiResultType)
}

}

class ApiResultCallAdapter(private val type: Type) : CallAdapter>> { override fun responseType(): Type = type

override fun adapt(call: Call<T>): Call<Await<T>> {
    return ApiResultCall(call)
}

}

class ApiResultCall(private val delegate: Call) : Call> { /* * 該方法會被Retrofit處理suspend方法的程式碼呼叫,並傳進來一個callback,如果你回調了callback.onResponse,那麼suspend方法就會成功返回 * 如果你回調了callback.onFailure那麼suspend方法就會拋異常 * * 所以我們這裡的實現是回撥callback.onResponse,將okhttp的call delegate / override fun enqueue(callback: Callback>) { //將okhttp call放入AwaitImpl直接返回,不做網路請求。在呼叫AwaitImpl的await時才真正開始網路請求 callback.onResponse([email protected], Response.success(delegate.toResponse())) } }

internal class AwaitImpl( private val call : Call, ) : Await {

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.本框架使用

新增依賴

Download

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("https://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()
}

} ```