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 wait < list > 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
Add dependency
implementation "io.github.cnoke.ktnet:api:?"
Write a network request data base class
open class ApiResponse<T>( var data: T? = null, var errorCode: String = "", var errorMsg: String = "" )
Realize 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<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() }
For asynchronous requests and synchronous requests, the exception capture reference is as follows. Exceptions beginning with try will be captured, and exceptions not beginning with try will not be 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() //After the first interface is completed, the 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() } }
Related tutorials
Android Foundation Series tutorials:
Android foundation course U-summary_ Beep beep beep_ bilibili
Android foundation course UI layout_ Beep beep beep_ bilibili
Android basic course UI control_ Beep beep beep_ bilibili
Android foundation course UI animation_ Beep beep beep_ bilibili
Android basic course - use of activity_ Beep beep beep_ bilibili
Android basic course - Fragment usage_ Beep beep beep_ bilibili
Android basic course - Principles of hot repair / hot update technology_ Beep beep beep_ bilibili
This article is transferred from https://juejin.cn/post/7051437444062773285 , in case of infringement, please contact to delete.