Kotlin collaboration + Retrofit is the most elegant network request

Posted by mightymax on Sat, 15 Jan 2022 12:56:02 +0100

1. Introduction

Retrofit's support for collaborative processes is very rudimentary. The use of kotlin does not conform to kotlin's elegance

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

//Network request for parallel exception capture
 fun oldBanner(){
        viewModelScope.launch {
            //try catch is required to use retrofit in traditional mode

            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()
        }
    }

It's unbearable to nest one layer at a time. Kotlin should solve the problem in one line of code to meet kotlin's elegance

After using this framework

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

   //Network request for parallel exception capture
fun parallel(){
     viewModelScope.launch {
     val awaitBanner1 = service.awaitBanner().tryAsync(this)
     val awaitBanner2 = service.awaitBanner().tryAsync(this)

      //The two interfaces are called together
      awaitBanner1.await()
      awaitBanner2.await()
   }
}

3. Check the Retrofit source code

First look at the Retrofit create method

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();
              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
              }
            });
  }

loadServiceMethod(method).invoke(args) enter this method to see the specific call

Let's look at adapt in suspendforresponse

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

      //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);
      }
    }
  }

Then it is directly handed over to the coroutine to call. The specific okhttp call is in the DefaultCallAdapterFactory. Or in the user-defined callAdapterFactory

Therefore, we can customize CallAdapterFactory not to access the network request after calling, but to access the network request when the user calls a specific method.

4. Customize CallAdapterFactory

Retrofit makes a network request directly after the call, so it is difficult to operate. We put the control of network requests in our hands and can operate at will.

class ApiResultCallAdapterFactory : CallAdapter.Factory() {
    override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
        //Check whether the returnType is of type call < T >
        if (getRawType(returnType) != Call::class.java) return null
        check(returnType is ParameterizedType) { "$returnType must be parameterized. Raw types are not supported" }
        //Take out the T in call < T > and check whether it is wait < T >
        val apiResultType = getParameterUpperBound(0, returnType)
        // If it is not wait, it will not be used by this calladapter Factory processing 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" }

        //Take out t in wait < T >, that is, the data type corresponding to the data 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(): 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 will be called by the code that Retrofit handles the suspend method and pass in a callback. If you call back the callback Onresponse, the suspend method will return successfully
     * If you call back Onfailure, the suspend method throws an exception
     *
     * So our implementation here is callback Onresponse, call delegate of okhttp
     */
    override fun enqueue(callback: Callback<Await<T>>) {
        //Put okhttp call into AwaitImpl and return directly without making network request. The network request is really started when 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(): T {

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

After customizing the callAdapter above, we delay the network request. After calling Retrofit, we will not request the network, but only put the calls required by the network request into await.

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

The Await we got did not make network requests. This entity class contains the call of okHttp.

At this time, 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
    }
}

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

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

            //The two interfaces are called together
            awaitBanner1.await()
            awaitBanner2.await()
        }
    }

At this time, we found that the network request succeeded and the data parsing failed. Because we put a layer of await outside the data. It must not be resolved successfully.

Based on the idea of solving where the error is, we customize Gson parsing

5. Custom 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 wait < T >
        if (Utils.getRawType(type) == Await::class.java && type is ParameterizedType){
            //Take out t in wait < T >
            val awaitType =  Utils.getParameterUpperBound(0, type)
            if(awaitType != null){
                adapter = gson.getAdapter(TypeToken.get(ParameterizedTypeImpl[responseCz,awaitType]))
            }
        }
        //Not awiat normal parsing, 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 returned value list encapsulates the class and is the first page with empty data, give an empty exception and let the interface display empty
            if (listData.isRefresh() && listData.isEmpty()) {
                throw ParseException(NetConstant.EMPTY_CODE, data.errorMsg)
            }
        }

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

        return t!!
    }

}

6. Use of this framework

implementation "io.github.cnoke.ktnet:api:?"

Write a network request data base class

open class ApiResponse(
var data: T? = null,
var errorCode: String = "",
var errorMsg: String = ""
)

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

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

    return t!!
}

}

Make a network request

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

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()
}

Asynchronous request, synchronous request, exception capture reference is as follows try Exceptions will be caught at the beginning, not at the beginning try The beginning is not captured.

fun banner(){
lifecycleScope.launch {
//Handle the exception separately. tryAwait will handle the exception. If the exception returns null
val awaitBanner = service.awaitBanner().tryAwait()
awaitBanner?.let {
for(banner in it){
Log.e("awaitBanner",banner.title)
}
}

    /**
     * Do not handle exceptions. Exceptions will be thrown directly and handled uniformly
     */
    val awaitBannerError = service.awaitBanner().await()
}

}

/**

  • Serial await
    */
    fun serial(){
    lifecycleScope.launch {
    //Call the first interface await first
    val awaitBanner1 = service.awaitBanner().await()
    / / when the first interface is completed, second interfaces are called.
    val awaitBanner2 = service.awaitBanner().await()
    }
    }

/**

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

     //The two interfaces are called together
     awaitBanner1.await()
     awaitBanner2.await()
    

    }
    }


This article is transferred from [https://juejin. cn/post/7051437444062773285]( https://juejin.cn/post/7051437444062773285 ), in case of infringement, please contact to delete.

Topics: Android network Programmer kotlin