Kotlin coroutine +Retrofit the most elegant network request to use

1. Introduction

Retrofit’s support for coroutines is very rudimentary. To use it in Kotlin is not kotlin’s elegance

interface TestServer {
    @GET("banner/json")
    suspend fun banner(a): ApiResponse<List<Banner>>
}

// Implement parallel capture of abnormal network requests
 fun oldBanner(a){
        viewModelScope.launch {
            // Traditional mode uses Retrofit with a 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()
        }
    }
Copy the code

One layer nested another layer, true unbearable. Kotlin should solve the problem in one line of code, in keeping with Kotlin’s elegance

After using this framework

interface TestServer {
    @GET("banner/json")
    suspend fun awaitBanner(a): Await<List<Banner>>
}

   // Implement parallel capture of abnormal network requests
fun parallel(a){
     viewModelScope.launch {
     val awaitBanner1 = service.awaitBanner().tryAsync(this)
     val awaitBanner2 = service.awaitBanner().tryAsync(this)

      // Both interfaces are called together
      awaitBanner1.await()
      awaitBanner2.await()
   }
}
Copy the code

2. Source address

GitHub

3. Look at Retrofit source code

So let’s look at Retrofit Create

public <T> T create(final Class<T> service) {
    validateServiceInterface(service);
    return(T) Proxy.newProxyInstance( service.getClassLoader(), new Class<? >[] {service}, new InvocationHandler() {private final Platform platform = Platform.get(a);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);// Specific call}}); }Copy the code

LoadServiceMethod (method).invoke(args) Enter this method to see what is called

We look at ‘adapt’ in suspenForResponse

@Override
    protected Object adapt(Call<ResponseT> call, Object[] args) {
      call = callAdapter.adapt(call);/ / if the user does not set use DefaultCallAdapterFactory callAdapterFactory

      //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) {
        returnKotlinExtensions.suspendAndThrow(e, continuation); }}}Copy the code

The coroutine calls the call directly. Specific call in DefaultCallAdapterFactory okhttp. Or in a user-defined callAdapterFactory

Therefore, we can customize the CallAdapterFactory to not perform network request access after the call, and then perform network request access when the user calls a specific method.

4. Customize the CallAdapterFactory

Retrofit makes a network request directly after the call, which makes it difficult to operate. We put control of network requests in our hands and we can do whatever we want.

class ApiResultCallAdapterFactory : CallAdapter.Factory() {
    override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
        // Check if returnType is of Call
      
        type
      
        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)
        // If you are not Await, this calladapter.factory process is compatible with normal mode
        if(getRawType(apiResultType) ! = Await::class.java) return null
        check(apiResultType is ParameterizedType) { "$apiResultType must be parameterized. Raw types are not supported" }

        // retrieve the T in Await
      
        that corresponds to the data type returned by the API
      
// val dataType = getParameterUpperBound(0, apiResultType)

        return ApiResultCallAdapter<Any>(apiResultType)
    }

}

class ApiResultCallAdapter<T>(private val type: Type) : CallAdapter<T, Call<Await<T>>> {
    override fun responseType(a): Type = type

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

class ApiResultCall<T>(private val delegate: Call<T>) : Call<Await<T>> {
    /** * This method is called by Retrofit code that handles the suspend method and passes in a callback. If you call callback.onResponse, If you call callback.onFailure, the suspend method throws an exception. So our implementation here is callback callback.onResponse, which will be okHTTP's call delegate * /
    override fun enqueue(callback: Callback<Await<T> >) {
        // Put the okHTTP call into AwaitImpl and return it directly without making a network request. The network request actually begins when the await of AwaitImpl is called
        callback.onResponse(this@ApiResultCall, Response.success(delegate.toResponse()))
    }
}


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

    override suspend fun await(a): T {

        return try {
            call.await()
        } catch (t: Throwable) {
            throw t
        }
    }
}
Copy the code

By customizing the callAdapter above, we defer the network request and do not request the network after calling Retrofit, only put the call required by the network request into await.

   @GET("banner/json")
    suspend fun awaitBanner(a): Await<List<Banner>>
Copy the code

We got Await<List> and did not make a network request. The call for okHttp is contained in this entity class.

In this case, we can define the following methods to catch exceptions

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}}Copy the code

Similarly, requests that catch exceptions in parallel can be called in the following way, which is much more elegant and concise

   /** * parallel async */
    fun parallel(a){
        viewModelScope.launch {
            val awaitBanner1 = service.awaitBanner().tryAsync(this)
            val awaitBanner2 = service.awaitBanner().tryAsync(this)

            // Both interfaces are called together
            awaitBanner1.await()
            awaitBanner2.await()
        }
    }
Copy the code

At this point we find that the network request was successful and the data parsing failed. Because we have an await layer on the data. Certainly not parse success.

In the spirit of what is wrong and what is fixed, we customize Gson parsing

5. Customize Gson parsing

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
        // check whether it is Await
      
        if (Utils.getRawType(type) == Await::class.java && type is ParameterizedType){
            // retrieve T in Await
      
            val awaitType =  Utils.getParameterUpperBound(0, type)
            if(awaitType ! =null){
                adapter = gson.getAdapter(TypeToken.get(ParameterizedTypeImpl[responseCz,awaitType]))
            }
        }
        // Not aWIAT normal resolution, compatible with normal mode
        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 the return value list encapsulates the class, and is the first page with empty data, then give the empty exception to make the interface display empty
            if (listData.isRefresh() && listData.isEmpty()) {
                throw ParseException(NetConstant.EMPTY_CODE, data.errorMsg)
            }
        }

        // errCode does not equal SUCCESS_CODE, an exception is thrown
        if (data.errorCode ! = NetConstant.SUCCESS_CODE) {throw ParseException(data.errorCode, data.errorMsg)
        }

        returnt!! }}Copy the code

6. Use of this framework

Add the dependent

implementation "io.github.cnoke.ktnet:api:?"
Copy the code

Write a network request base class

open class ApiResponse<T>(
    var data: T? = null.var errorCode: String = "".var errorMsg: String = ""
)
Copy the code

Implement com.cnoke.net.factory.GsonResponseBodyConverter

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 the return value list encapsulates the class, and is the first page with empty data, then give the empty exception to make the interface display empty
            if (listData.isRefresh() && listData.isEmpty()) {
                throw ParseException(NetConstant.EMPTY_CODE, data.errorMsg)
            }
        }

        // errCode does not equal SUCCESS_CODE, an exception is thrown
        if (data.errorCode ! = NetConstant.SUCCESS_CODE) {throw ParseException(data.errorCode, data.errorMsg)
        }

        returnt!! }}Copy the code

Making network requests

interface TestServer {
    @GET("banner/json")
    suspend fun awaitBanner(a): Await<List<Banner>>
}

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()
}
Copy the code

If the request starts with a try, exceptions will be caught. If the request starts with a try, exceptions will not be caught.

fun banner(a){
    lifecycleScope.launch {
        TryAwait handles exceptions and returns null if an exception is returned
        valawaitBanner = service.awaitBanner().tryAwait() awaitBanner? .let {for(banner in it){
                Log.e("awaitBanner",banner.title)
            }
        }

        /** * The exception will be thrown directly
        val awaitBannerError = service.awaitBanner().await()
    }
}

/** * serial await */
fun serial(a){
    lifecycleScope.launch {
        // Call the first interface await
        val awaitBanner1 = service.awaitBanner().await()
        // Call the second interface after the first interface completes
        val awaitBanner2 = service.awaitBanner().await()
    }
}

/** * parallel async */
fun parallel(a){
    lifecycleScope.launch {
        val awaitBanner1 = service.awaitBanner().async(this)
        val awaitBanner2 = service.awaitBanner().async(this)

        // Both interfaces are called together
        awaitBanner1.await()
        awaitBanner2.await()
    }
}
Copy the code