Click here for chapter one

Click here for chapter 2

In Chapter 2 Retrofit support for Kotlin coroutines, we saw how Retrofit supports Kotlin coroutines. Write the following

Interface WanAndroidService {@get ("banner/json") // suspend fun banner(); Banner} / / Retrofit initialize val Retrofit = Retrofit. The Builder (). The baseUrl (" https://www.wanandroid.com/ ") .addConverterFactory(GsonConverterFactory.create()) .build() val service: WanAndroidService = retrofit. The create (WanAndroidService: : class. Java) lifecycleScope. Launch {/ / direct call banner method here, No need to call execute() and run tvresult.settext (banner.toString())} on the child thread val banner = service.banner() // main threadCopy the code

The problem

Such code works under normal circumstances, but in business development, such code is problematic because we ignore exception handling.

As you can see from Retrofit source code, an exception is thrown when we define a Banner that is not empty but returns an empty result, or if the network request fails, For example, when I changed the Android baseUrl from wanAndroid.com to wanAndroid.com 1, THERE was a UnknownHostException.

So how do you handle this anomaly?

Normal processing

So if you throw an exception like this, it’s actually pretty easy to do, just try catch

lifecycleScope.launch{
	try {
        val banner = service.banner()
        tvResult.setText(result.body().toString())
    } catch (e: Exception) {
    	e.printStackTrace()
	}
}
Copy the code

This code, however, looks inelegant, and more importantly, what if I forget to write it? There are no errors and the compilation passes. This is an optimization point

Optimization scheme

Plan 1: Official plan

Launch {kotlin.runcatching {service.banner()}.onsuccess {// Execute log. I (TAG, "onCreate: "+ it)}.onFailure {// Exception message handling}.getorNull ()?.let {// get data not null}}Copy the code

Kotlin’s runCatching function makes it easy to handle exceptions by streaming them. Generally, we only need to focus on onSuccess, or we can call getOrNull() to determine whether the result is null.

This solution looks good, but it feels a bit wordy, there is no uniform exception handling, and if there is a business-level errorCode == 0 judgment, you still have to handle it yourself.

Scheme 2: Response can be null

Interface WanAndroidService {@get ("banner/json") // suspend fun banner(): banner? } lifecycleScope. Launch {val = service.banner() // void if(banner! = null) { tvResult.setText(banner.toString()) } }Copy the code

For the business layer, I only care about the result that the banner is not empty, but other network exceptions are not known. Not sound enough.

Solution 3: Retrofit Customizes callAdapters

Here I want to think, what kind of effect do I want to achieve?

Such as

LifecycleScope. Launch {val result = service.banner(); tvResult.setText(result.body().toString()) } }Copy the code

Service.banner () returns a result that includes three scenarios: network exception, service failure, and service success. It does not throw an exception when an exception occurs, and then determines whether the service is successful through code 1.

Determine the feasibility of the scheme

Consider, can Retrofit handle such a situation? Of course you can, changing the function that returns the result requires customizing the CallAdapter, which is Retrofit’s feature and advantage.

implementation

Want to customize CallAdapter, let’s look at the structure of CallAdapterFactory, want to understand CallAdapterFactory structure, then, from the perspective of the Retrofit own DefaultCallAdapterFactory is better.

The following is a partial DefaultCallAdapterFactory source code

final class DefaultCallAdapterFactory extends CallAdapter.Factory {
  private final @Nullable Executor callbackExecutor;

  DefaultCallAdapterFactory(@Nullable Executor callbackExecutor) {
    this.callbackExecutor = callbackExecutor;
  }

  @Override
  public @Nullable CallAdapter<?, ?> get(
      Type returnType, Annotation[] annotations, Retrofit retrofit) {
    //代码1
    if (getRawType(returnType) != Call.class) {
      return null;
    }
    //代码2
    if (!(returnType instanceof ParameterizedType)) {
      throw new IllegalArgumentException(
          "Call return type must be parameterized as Call<Foo> or Call<? extends Foo>");
    }
    //代码3
    final Type responseType = Utils.getParameterUpperBound(0, (ParameterizedType) returnType);

    final Executor executor =
        Utils.isAnnotationPresent(annotations, SkipCallbackExecutor.class)
            ? null
            : callbackExecutor;
    //代码4
    return new CallAdapter<Object, Call<?>>() {
      @Override
      public Type responseType() {
        return responseType;
      }

      @Override
      public Call<Object> adapt(Call<Object> call) {
        return executor == null ? call : new ExecutorCallbackCall<>(executor, call);
      }
    };
  }
}
Copy the code

DefaultCallAdapterFactory CallAdapter inheritance. The Factory, and rewrite the get () method, finally get a CallAdapter anonymous inner class.

Let’s do it step by step

Code 1, getRawType(returnType), this is the returnType that’s passed in, and as we saw earlier when we looked at the principle, this returnType is Call when you’re not suspending the method. GetRawType () gets the outermost Call, which is matched here.

In code 2, returnType instanceof ParameterizedType indicates that the Call is a ParameterizedType. That is, with generics? Yes, apparently.

In code 3, since it’s a parameterized type, let’s take the specific type, which is Banner.

Code 4, this Banner to the anonymous inner class responseType () method, and the get () method returns the CallAdapter, said, this type of Call, be DefaultCallAdapterFactory matching, and use it.

So let’s do that

Custom ApiResultCallAdapterFactory

class ApiResultCallAdapterFactory :
    CallAdapter.Factory() {
    
    override fun get(
        returnType: Type,
        annotations: Array<out Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
        return null
    }
}
Copy the code

As above, we define a ApiResultCallAdapterFactory, rewrite the get () method, this just to think of it, we need the get () method to judge the type of you need, but this type is not created yet, and the results of this class is a parcel network request, such as business success, Business failure or something. We can use one of Kotlin’s features, the sealed class, to understand that the subclasses of this class are finite and knowable.

To create an ApiResult

sealed class ApiResult<T> {
    data class BizSuccess<T>(val data: T?) : ApiResult<T>()
    data class BizError(val errorCode: Int, val errorMsg: String) : ApiResult<Nothing>()
    data class OtherError(val throwable: Throwable) : ApiResult<Nothing>()
}
Copy the code

We focus on BizSuccess, and the data in the package is the result of the request, so how is the interface defined? As shown below.

interface WanAndroidSer
    @GET("banner/json")
    suspend fun banner(): ApiResult<Banner>
}
Copy the code

Override the get() method

Now that we know what type we want, let’s decide

class ApiResultCallAdapterFactory : CallAdapter.Factory() { override fun get( returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit ): CallAdapter<*, *>? If (getRawType(returnType)! = ApiResult.class) { return null; }}}Copy the code

Write code 1, we copy DefaultCallAdapterFactory, the original Call. The class to ApiResult. Class went, is it?

That’s true in theory, but not in practice. Why is that? This can be seen from Retrofit’s previous support for Kotlin coroutines

HttpServiceMethod# parseAnnotations () the source code

static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations( Retrofit retrofit, Method method, RequestFactory requestFactory) { if (isKotlinSuspendFunction) { ... Judgment logic omitted / / code 1 adapterType = new Utils. ParameterizedTypeImpl (null, Call class, responseType); } // CallAdapter<ResponseT, ReturnT> CallAdapter = createCallAdapter(retrofit, method, adapterType, Annotations); Return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForBody<>(requestFactory, callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter, continuationBodyNullable); }Copy the code

Code 1, if the suspend method is used, encapsulates responseType and Call.class as ParameterizedTypeImpl, which acts as the new adapterType. Encapsulate the responseType as Call, encapsulate the ApiResult as Call<ApiResult>, and then execute code 2, createCallAdapter, to find the CallAdapter.

Now that is encapsulated into the Call, why don’t match to DefaultCallAdapterFactory?

Because our custom ApiResultCallAdapterFactory priority is higher, the row in front of the collection, traversed the natural match ApiResultCallAdapterFactory first, will return once the match is successful

So the get() method should look something like this

class ApiResultCallAdapterFactory : CallAdapter.Factory() { override fun get( returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit ): CallAdapter<*, *>? {// Call<ApiResult<Banner>> if (getRawType(returnType)! = Call::class.java) return null if (! (returnType is ParameterizedType)) { throw new IllegalArgumentException( "return type must be parameterized as Call<Foo>  or Call<? extends Foo>"); } / / get ApiResult < Banner > val apiResultType = Utils. GetParameterUpperBound (0, ApiResult if (getRawType(apiResultType)! = ApiResult::class.java) return null //ApiResult is parameterized if (! (apiResultType is ParameterizedType)) { throw new IllegalArgumentException( "return type must be parameterized"); } // get Banner val dataType = getParameterUpperBound(0, ApiResultType) //Banner to ApiResultCallAdapter return ApiResultCallAdapter(dataType)}}Copy the code

The ApiResultCallAdapter returned is our customized CallAdapter. Now look at the ApiResultCallAdapter

Custom ApiResultCallAdapter

Or to imitate CallAdapter DefaultCallAdapterFactory under way

final class DefaultCallAdapterFactory extends CallAdapter.Factory {
  @Override
  public @Nullable CallAdapter<?, ?> get(
      Type returnType, Annotation[] annotations, Retrofit retrofit) {
    return new CallAdapter<Object, Call<?>>() {
      @Override
      public Type responseType() {
        return responseType;
      }

      @Override
      public Call<Object> adapt(Call<Object> call) {
        return executor == null ? call : new ExecutorCallbackCall<>(executor, call);
      }
    };
  }
}
Copy the code

So we’re going to subclass CallAdapter, and then we’re going to override responseType and adapt, so let’s customize it

class ApiResultCallAdapter(
    private val responseType: Type
) : CallAdapter<Any, Call<Any>> {
    override fun responseType(): Type = responseType

    override fun adapt(call: Call<Any>): Call<Any> {
        return ApiResultCall(call)
    }
}
Copy the code

So this is the responseType() that specifies what we want to parse, so we’re just going to have a GsonConverterFactory, and that GsonConverterFactory resolves to a Banner based on the responseType that we passed in.

Adapt () has seen this before when analyzing its principles. The call object passed in is OkHttpCall

The call chain is HttpServiceMethod#invoke() -> SuspendForBody#adapt() -> ApiResultCallAdapter#adapt().

Finally, the Adapt () method returns ApiResultCall(), which we also customized.

Custom ApiResultCall

We still imitate DefaultCallAdapterFactory, to see the Call object, principle analysis is ExecutorCallbackCall before

static final class ExecutorCallbackCall<T> implements Call<T> { .... @Override public void enqueue(final Callback<T> callback) { Objects.requireNonNull(callback, "callback == null"); delegate.enqueue( new Callback<T>() { @Override public void onResponse(Call<T> call, final Response<T> response) { ... Override public void onFailure(Call<T> Call, final Throwable T) {... Switch to main thread callback}}); } @Override public boolean isExecuted() { return delegate.isExecuted(); } @Override public Response<T> execute() throws IOException { return delegate.execute(); } @Override public void cancel() { delegate.cancel(); } @Override public boolean isCanceled() { return delegate.isCanceled(); } @SuppressWarnings("CloneDoesntCallSuperClone") // Performing deep clone. @Override public Call<T> clone() { return new  ExecutorCallbackCall<>(callbackExecutor, delegate.clone()); } @Override public Request request() { return delegate.request(); } @Override public Timeout timeout() { return delegate.timeout(); }}Copy the code

ExecutorCallbackCall is not called in SuspendForBody, but it is instructive.

ExecutorCallbackCall inherits Call and overwrites many methods, but we’ll focus on the enqueue() method because our custom ApiResultCall#enqueue() method will eventually be called from SuspendForBody. And the way the callback is written in enqueue() is important because it affects whether the exception is thrown and the business succeeds.

Analyze KotlinExtensions#await()

KotlinExtensions#await()

suspend fun <T : Any> Call<T>.await(): T { return suspendCancellableCoroutine { continuation -> ... enqueue(object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { if (response.isSuccessful) { val body = response.body() if (body == null) { .... Abnormal information / / code 1 continuation. ResumeWithException (e)} else {/ / code 2 continuation. Resume (body)}} else {/ / code 3 continuation.resumeWithException(HttpException(response)) } } override fun onFailure(call: Call<T>, t: Throwable) {/ / code 4 continuation. ResumeWithException (t)}}}})Copy the code

The KotlinExtensions#await() method, described earlier, is the key step to finally start the coroutine and request the network.

Focusing on my markup in the code, the only way to not throw an exception is to go through code 2, otherwise code 1, code 3, and code 4 will throw an exception. Code 1 threw an exception because I wanted the return value not to be null, so Retrofit found that it didn’t and threw an exception, so let’s go with that, just get around code 1,3, and 4 anyway.

Now that the callback logic is clear, we can write our own.

ApiResultCall write

class ApiResultCall( private val delegate: Call<Any> ) : Call<Any> { override fun enqueue(callback: Callback<Any>) {//delegate as OkHttpCall delegate.enqueue(object: Callback<Any> {override fun onResponse(call: Call<Any>, response: Response<Any>) {if (response.issuccessful) {// After GsonConverterFactory conversion, Response.body () becomes Banner Val body = response.body() if (body == null) {// code 1 Callback. OnResponse (this @ ApiResultCall Response. Success (ApiResult. BizError (1, "error")))} else {/ / code 2 Callback. OnResponse (this @ ApiResultCall Response. Success (ApiResult. BizSuccess (body)))}} else {1} / / reference code} override fun  onFailure(call: Call<Any>, t: Callback. OnResponse (this@ApiResultCall, response.success (apiresult.otherError (t)))}})}}Copy the code

The callback in codes 1, 2, and 3 is the callback in KotlinExtensions#await() and will directly affect the result. This is important!

So you can see what we’re doing here is we’re calling onResponse() in code 1, 2, and 3, Response.success(body) or response.success (apiResult.bizError (-1,” error “)) The goal is to always use continuation. Resume (body) logic in KotlinExtensions#await(), that is, not throw an exception. Of course, the processing and judgment of body can be customized, and the meaning of errorCode can also be defined. Finally, we repackage the successfully resolved Banner into ApiResult, which conforms to the result we want. Other methods in the ApiResultCall can be rewritten as ExecutorCallbackCall

Just to emphasize the point again

  1. Call back the onResponse() method
  2. The incoming Response. Success

Both are indispensable.

There are a couple of questions here.

  1. What is response.success ()?

    This Response is retroFIT2’s Response, and success() is a way to encapsulate the Response. Take a look

    public static <T> Response<T> success(@Nullable T body) {
      return success(
          body,
          new okhttp3.Response.Builder() //
              .code(200)
              .message("OK")
              .protocol(Protocol.HTTP_1_1)
              .request(new Request.Builder().url("http://localhost/").build())
              .build());
    }
    Copy the code

    Response.error() cannot be used, otherwise it will also raise an exception, because in KotlinExtensions#await() it will go through code 3, Because response. IsSuccessful is false. If we don’t want to use response.success (), we can write it ourselves.

  2. Response.success(Any())?

    Although the example does not write response.success (Any()), in fact, if we use response.success (Any()), the crash, response.success () method is ok, the problem is Any(), we can’t pass Any(), If you fail to enforce body in continuation.resume(body), it tells you that Any() cannot enforce ApiResult.

conclusion

Finally, we have wrapped the Kotlin coroutine exception so that no exception is thrown whether it succeeds or fails, and all we have to do is judge the success and failure in the result.

New optimization point

Now let’s look at the actual business scenario. WanAndroid’s Banner interface returns data like this

{ "data": [...] , "errorCode": 0, "errorMsg": "" }Copy the code

Such a structure is very common in our actual development. It usually requires the judgment of errorCode == 0 before business processing can be carried out, so our code has become like this.

lifecycleScope.launch { val result = service.banner() if (result is ApiResult.BizSuccess) { if (result.data.errorCode ==  0) { tvResult.setText(result.data.data.toString()) } } }Copy the code

And all parsed beans need to contain duplicate ErrorCodes. How can we optimize for such business scenarios? Or what kind of code do we want? I think it should be something like this

lifecycleScope.launch { val result = service.banner() if (result is ApiResult.BizSuccess) { Tvresult.settext (result.data.toString())}} errorMsg data class Banner(val desc: Val id: Int = 0, // 10.....) Interface wanAndroidser@get ("banner/json") // ApiResult< banner >> suspend fun banner(): ApiResult<List<Banner>>} ApiResult below sealed class ApiResult<T> {//BizSuccess add errorCode, errorMsg, Data class BizSuccess<T>(val errorCode: Int, val errorMsg: String, val data: T?) : ApiResult<T>() data class BizError(val errorCode: Int, val errorMsg: String) : ApiResult<Nothing>() data class OtherError(val throwable: Throwable) : ApiResult<Nothing>() }Copy the code

Next, how to change the code.

First is ApiResultCallAdapterFactory the get () method, we can get the parameters of the need to type, ApiResult, the List can we get it, but the responseType ApiResultCallAdapter () pass?

Sealed class (int <List>, sealed class (int <List>, sealed class (int <List>, sealed class))

If you pass a List, the existing GsonConverterFactory won’t help us parse it to what we want.

How to do?

Two approaches come to mind:

  1. Wrap the List to BizSuccess , or create a new BaseResponse instead of BizSuccess, mainly for GsonConverterFactory to parse.
  2. Custom GsonConverterFactory parsing to parse the results according to our own parsing scheme.

For method 1, Retrofit uses this line to convert responseType to Call in the suspend method

adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
Copy the code

So, I wrote code like this

returnType = new Utils.ParameterizedTypeImpl(null, ApiResult.BizSuccess.class, responseType);
Copy the code

Utils ParameterizedTypeImpl is external access restrictions, I copied “will this series code. The returnType is then passed to the ApiResultCallAdapter. But after a lot of attempts, due to the reflection code is not thorough understanding, ultimately failed. Capable friends can try again

Here’s method 2, which is relatively simple.

Custom GsonConverterFactory

Let’s first determine what is passed in the responseType() of the ApiResultCallAdapter.

No doubt is a List, ApiResultCallAdapterFactory get remain unchanged () method

Let’s take a look at the original GsonConverterFactory, okay

Public final class GsonConverterFactory extends Converter.Factory {// GsonConverterFactory#create(Gson) initializes private final Gson gson; @Override public Converter<ResponseBody, ? > responseBodyConverter( Type type, Annotation[] annotations, List<Banner> adapter TypeAdapter<? > adapter = gson.getAdapter(TypeToken.get(type)); / / return code 2 new GsonResponseBodyConverter < > (gson, adapter); } @Override public Converter<? , RequestBody> requestBodyConverter() { ..... }}Copy the code

After get the List of adapter, return a GsonResponseBodyConverter, let’s take a look at this class

final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> { private final Gson gson; private final TypeAdapter<T> adapter; GsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) { this.gson = gson; this.adapter = adapter; } @override public T convert(ResponseBody value) throws IOException gson.newJsonReader(value.charStream()); T result = adapter.read(jsonReader); . return result; } finally { value.close(); }}}Copy the code

Main way is GsonResponseBodyConverter# convert () method, this will convert method in OkHttpCall# parseResponse () method calls, before analysis principle said that used to resolve the request results.

Code 1 puts the result directly into JsonReader as an IO stream

In code 2, use adapter for conversion. But the adapter here is the Adapter of the List, so when we customize it, we need to segment it, so let’s go to the code.

@Override
public T convert(ResponseBody value) throws IOException {
    JsonReader jsonReader = gson.newJsonReader(value.charStream());
    jsonReader.beginObject();
    int errorCode = -1;
    String errorMsg = "";
    T data = null;
    while (jsonReader.hasNext()) {
        String nextName = jsonReader.nextName();
        if (TextUtils.equals(nextName, "errorCode")) {
            errorCode = jsonReader.nextInt();
        } else if (TextUtils.equals(nextName, "errorMsg")) {
            errorMsg = jsonReader.nextString();
        } else if (TextUtils.equals(nextName, "data")) {
            data = adapter.read(jsonReader);
        }
    }
    jsonReader.endObject();
    return ((T) new ApiResult.BizSuccess(errorCode, errorMsg, data));
}
Copy the code

After parsing the errorCode and errorMsg separately, the data data is still handed over to adapter for parsing, which is ok. Finally, an APIresult. BizSuccess is returned, because the apiresult. BizSuccess structure is relatively complete. So use it for the final result, which is equivalent to what BaseResponse said earlier.

However, this is just sample code and the logic is not rigorous. For example, the original convert method ends up with value.close(); , including other parsing node judgments and so on.

Handling of callbacks

In the end, we return to ApiResultCallAdapterFactory, see ApiResultCall handling of callback.

override fun enqueue(callback: Callback<Any>) { delegate.enqueue(object : Callback<Any> { override fun onResponse(call: Call<Any>, response: Response<Any>) { if (response.isSuccessful) { val body = response.body() if (body == null) { Callback. OnResponse (this @ ApiResultCall Response. Success (ApiResult. BizError (1, "error")))} else {/ / code 1 if the body is ApiResult.BizSuccess<*> && body.errorCode == 0) { callback.onResponse(this@ApiResultCall, Response.success(body)) } } } else { .... }}... })}Copy the code

Looking at code 1, if body is apiresult.bizSuccess and body.errorCode == 0, then success is indicated.

So we can do this at the business level

LifecycleScope. Launch {val result = service.banner() // This BizSuccess contains body.errorCode == 0 if (result is ApiResult.BizSuccess) { tvResult.setText(result.data.toString()) } }Copy the code

The code has been uploaded to Github (including chapter 4, please read chapter 4) : github.com/lt19931203/…

The end.

Continue with chapter 4 – Custom Converter support for Kotlin Air Security

Reference links:

1. Blog. Yujinyan. Me/posts/kotli…

2. blog.csdn.net/taotao11012…