“This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!”

Since Retofit 2.6.0, the default is kotlin coroutines only. It’s also easy to use. Just say suspend in front of the function.

interface ApiService {
  @GET("article/list/{page}/json")
  suspend fun getArticleList(@Path("page") page:Int): Result<PageEntity<Article>> 
}
Copy the code

A quick review of usage

val retrofit= Retrofit.Builder()
    .baseUrl("https://www.wanandroid.com")
    .addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
    .build()

val apiService = retrofit.create(ApiService::class.java)

lifecycleScope.launch { 
    val result = apiService.getArticleList(1)}Copy the code

Return the result directly as we want, without the need for a Call package. So how does Retofit do it?

Simple source code analysis

There are many source analysis sites for Retofit that I won’t go into here.

Method entry Retofit#create

A look at Retofit’s create method reveals that there is a dynamic proxy that fires the InvocationHandler’s Invoke method when the interface method is invoked.

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)//java1.8 interfaces can have default implementations? platform.invokeDefaultMethod(method, service, proxy, args) : loadServiceMethod(method).invoke(args); }}); }Copy the code

The loadServiceMethod(method) at the end of the method calls the parseAnnotations() method of ServiceMethod.

ServiceMethod. Java source code


abstract class ServiceMethod<T> {
  static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
  // 0️ build request factory
    RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);

    Type returnType = method.getGenericReturnType();
    if (Utils.hasUnresolvableType(returnType)) {
      throw methodError(
          method,
          "Method return type must not include a type variable or wildcard: %s",
          returnType);
    }
    if (returnType == void.class) {
      throw methodError(method, "Service methods cannot return void.");
    }
  / / 1 ️ ⃣
    return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory);
  }

  abstract @Nullable T invoke(Object[] args);
}
Copy the code

When the RequestFactory object is built at 0️, the build() method of the RequestFactory is called, in which there is a piece of code to traverse the parsing method parameters

int parameterCount = parameterAnnotationsArray.length;
parameterHandlers = newParameterHandler<? >[parameterCount];for (int p = 0, lastParameter = parameterCount - 1; p < parameterCount; p++) {
  parameterHandlers[p] =
      / / 👇
      parseParameter(p, parameterTypes[p], parameterAnnotationsArray[p], p == lastParameter);
}
Copy the code

Found isKotlinSuspendFunction

ParseParameter method part of the code

private @NullableParameterHandler<? > parseParameter(int p, Type parameterType, @Nullable Annotation[] annotations, boolean allowContinuation) {
	// Omit part of the code...
  if (result == null) {
    //0️ whether to be the last parameter
    if (allowContinuation) {
      try {
        / / 1 ️ ⃣
        if (Utils.getRawType(parameterType) == Continuation.class) {
          isKotlinSuspendFunction = true;
          return null; }}catch (NoClassDefFoundError ignored) {
      }
    }
    throw parameterError(method, p, "No Retrofit annotation found.");
  }

  return result;
}
Copy the code

AllowContinuation in comment 0️ is the value of p == lastParameter passed on the parsing call, which means to determine whether it is the lastParameter. If it is, If so, mark isKotlinSuspendFunction to true. It looks like we found something about kotlin coroutines. This turns a traversal whether or not Kotlin Suspend is supported to true.

Kotlin’s dark magic

Why mark isKotlinSuspendFunction true if the last argument is continuation. class? Did we write the method without a Continuation type argument? So that brings us to the dark magic of Kotlin, and the reason Kotlin allows me to write very clean code is because the Kotlin compiler does a lot of code for us. We decompiled the results using kotlin’s bytecode tool provided by AS

//kotlin 	
@GET("article/list/{page}/json")
 suspend fun getArticleList(@Path("page") page:Int): Result<PageEntity<Article>>

-------------------------------------------------------------------------

// Kotlin bytecode decompiled Java code
@GET("article/list/{page}/json")
@Nullable
Object getArticleList(@Path("page") int var1, @NotNull Continuation<Result<PageEntity<Article>>> var2);
Copy the code

It is found that if the function is compiled using Suspend, a Continuation type parameter is added at the end of the parameter class table. The generic type of the Continuation is the return value type of the original function, which is invisible to the decomcompiled Java code. If you look directly at the Kotlin bytecode, you can see that the method signature looks like this. A Continuation of a Kotlin coroutine is the equivalent of a callback, and the value returned by the return function is what is returned at the callback.

Where is isKotlinSuspendFunction used

IsKotlinSuspendFunction isKotlinSuspendFunction Returning to the parseAnnotations method of ServiceMethod, we call HttpServiceMethod parseAnnotations last and return the value.

Let’s take a look at what we’ve done with parseAnnotations for HttpServiceMethod.

static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations( Retrofit retrofit, Method method, RequestFactory requestFactory) {
  boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;
  boolean continuationWantsResponse = false;
  boolean continuationBodyNullable = false;
//0️ processing according to whether kotlin suspend or not
  if (isKotlinSuspendFunction) {
    Type[] parameterTypes = method.getGenericParameterTypes();
    Type responseType = Utils.getParameterLowerBound(0,
        (ParameterizedType) parameterTypes[parameterTypes.length - 1]);
    / / 1 ️ ⃣ ⬇ ️
    if (getRawType(responseType) == Response.class && responseType instanceof ParameterizedType) {/ / 2
      // Unwrap the actual body type from Response<T>.
      responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType);
      continuationWantsResponse = true;
    } else {
      // TODO figure out if type is nullable or not
      // Metadata metadata = method.getDeclaringClass().getAnnotation(Metadata.class)
      // Find the entry for method
      // Determine if return type is nullable or not
    }

    adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
    annotations = SkipCallbackExecutorImpl.ensurePresent(annotations);
  } else {
    adapterType = method.getGenericReturnType();
  }
 // Omit part of the code...
  if(! isKotlinSuspendFunction) {return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
  } else if (continuationWantsResponse) {
    //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
    / / 2 ️ ⃣ ⬇ ️
    return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForResponse<>(requestFactory,
        callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
  } else {
    //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
    / / 3 ️ ⃣ ⬇ ️
    return (HttpServiceMethod<ResponseT, ReturnT>) newSuspendForBody<>(requestFactory, callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter, continuationBodyNullable); }}Copy the code

On note 0️, judge whether kotlin suspend function is processed according to isKotlinSuspendFunction. Above, we know that the return value type of suspend function will eventually become the generic type of the last parameter Continuation. In note 1 ️ ⃣ place to figure out whether the last parameter of the generic class, if is the continuationWantsResponse tag to true.

In 2 ️ ⃣ and comments 3 ️ ⃣ place according to the different objects of continuationWantsResponse create. As you can see from this, there should be another way to write the suspend function, where the return value is wrapped in Response.

@GET("article/list/{page}/json")
suspend fun getArticleList1(@Path("page") page:Int): Response<Result<PageEntity<Article>>>
Copy the code

The above is another way of writing. Let’s continue to analyze the return value without Response, so let’s analyze the situation at 3️, namely SuspendForBody.

The mystery of supporting coroutines

SuspendForBody

static final class SuspendForBody<ResponseT> extends HttpServiceMethod<ResponseT.Object> {
  private final CallAdapter<ResponseT, Call<ResponseT>> callAdapter;
  private final boolean isNullable;

  SuspendForBody(RequestFactory requestFactory, okhttp3.Call.Factory callFactory,
      Converter<ResponseBody, ResponseT> responseConverter,
      CallAdapter<ResponseT, Call<ResponseT>> callAdapter, boolean isNullable) {
    super(requestFactory, callFactory, responseConverter);
    this.callAdapter = callAdapter;
    this.isNullable = isNullable;
  }
  @Override protected Object adapt(Call<ResponseT> call, Object[] args) {
    call = callAdapter.adapt(call);
    Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];
   
    try {
      return isNullable
          ? KotlinExtensions.awaitNullable(call, continuation)
          : KotlinExtensions.await(call, continuation);
    } catch (Exception e) {
      returnKotlinExtensions.yieldAndThrow(e, continuation); }}}Copy the code

Use a different await function from KotlinExtensions depending on whether the return value is air conditioner.

suspend fun <T : Any> Call<T>.await(a): T {
  return suspendCancellableCoroutine { continuation ->
    continuation.invokeOnCancellation {
      cancel()
    }
    enqueue(object : Callback<T> {
      override fun onResponse(call: Call<T>, response: Response<T>) {
        if (response.isSuccessful) {
          val body = response.body()
          if (body == null) {
    				// Omit part of the code
            continuation.resumeWithException(e)
          } else {
            continuation.resume(body)
          }
        } else {
          continuation.resumeWithException(HttpException(response))
        }
      }
      override fun onFailure(call: Call<T>, t: Throwable) {
        continuation.resumeWithException(t)
      }
    })
  }
}
Copy the code

It is a Call of extension function suspendCancellableCoroutine help me Call the enqueue function in the function, Call continuation.resume(body) in its onResponse callback to return the requested data to us.

How do you call adapt for SuspendForBody? Let’s go back to the beginning and see which method is triggered when an interface method call is invoked

returnloadServiceMethod(method).invoke(args ! =null ? args : emptyArgs);
Copy the code

So we’re just analyzing the loadServiceMethod(method) process, and the return value of loadServiceMethod(method) is the SuspendForBody object, The SuspendForBody object calls the Invoke method and ends up calling the adapt method.

@Override final @Nullable ReturnT invoke(Object[] args) {
  Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
  return adapt(call, args);
}
Copy the code

That’s the rough version of the Retofit coroutine request pattern.

Extension — callback variant coroutines

We see above Retofit kotlin provide suspendCancellableCoroutine function is used to kotlin coroutines support. A good use of kotlin coroutines is to solve callback hell and make asynchronous code “synchronous code.” If Retofit supports kotlin coroutines, what about those that don’t. We can write the callback as a coroutine, just like Retofit. For example, let’s make OKHTTP support coroutines. We can write a Call extension to Okhttp.

suspend fun Call.awaitResponse(a): Response {
    return suspendCancellableCoroutine { continuation ->
        continuation.invokeOnCancellation { cancel() }
        enqueue(object : Callback {
            override fun onResponse(call: Call, response: Response) {
                continuation.resume(response)
            }

            override fun onFailure(call: Call, e: IOException) {
                continuation.resumeWithException(e)
            }
        })
    }
}
Copy the code

use

lifecycleScope.launch {
    val request = Request.Builder().url("https://www.wanandroid.com").get().build()
    val response = okHttpClient.newCall(request).awaitResponse()
   if (response.isSuccessful){
       //TODO}}Copy the code

Any callback can be made to support coroutines in this way and can be optimized in this way if you encounter nested callbacks.