Retrofit, a well-known web framework, is open sourced by Square. Square, it is our old acquaintance, many frameworks are his open source, such as OkHttp, Picasso was, leakcanary and so on. Many of their open source libraries are now standard for Android APP development.

In short,Retrofit actually uses OkHttp to make web requests at the bottom, but it’s wrapped up to make it easier and more efficient for developers to access the web.

To summarize :Retrofit dynamically generates implementation classes from an interface that defines annotations such as request mode + path + parameters, which Retrofit can easily retrieve from annotations and then concatenate to access the network over OkHttp.

1. Basic use

It’s very simple. I simply request a GET interface, take some JSON data, and parse it into an object instance.

First, we need to define an interface. This interface is the NETWORK request API interface, which defines the request method, input parameters, data, return value and other data.

I use the interface here is hong god wanandroid website open interface, address: www.wanandroid.com/blog/show/2. I use https://wanandroid.com/wxarticle/chapters/json.

interface TodoService {

     @GET("wxarticle/list/{id}/{page}/json")
    fun getAirticles(@Path("id") id: String, @Path("page") page: String): Call<BaseData>

}

Copy the code

With the API defined, we need to build an instance of Retrofit.

private val mRetrofit by lazy {
        Retrofit.Builder()
            .baseUrl("https://wanandroid.com/")
            .addConverterFactory(GsonConverterFactory.create())   // Data parser
            .build()
    }
Copy the code

Once we have a Retrofit instance, we need Retrofit to help us translate the API defined above into an instance, and then we can call it directly as an instance

// Create an instance with the interface
val iArticleApi = mRetrofit.create(IArticleApi::class.java// Call method returnsCallobjectval airticlesCall = iArticleApi.getAirticles("408"."1")
Copy the code

The Call returns a Call object, which we can use to access the network

// Asynchronous request mode
airticlesCall.enqueue(object : Callback<BaseData> {
    override fun onFailure(call: Call<BaseData>, t: Throwable) {
        // The request failed
        t.printStackTrace()
        Log.e("xfhy"."Request failed")}override fun onResponse(call: Call<BaseData>, response: Response<BaseData>) {
        // The request succeeded
        val baseData = response.body()
        Log.e("xfhy".${baseData? .toString()}")}})Copy the code

Using this Call object, you can Call the enQueue method to access the network asynchronously and get the result. It’s very simple.

2. Build a Retrofit

Ps: Here’s a quick tip: We use Builder mode when we need to pass in many, many necessary parameters to build an object. For those of you who don’t know, look here

Retrofit source code uses a number of Builder patterns, such as Retrofit builds, which we’ll look at next

Retrofit.Builder()
        .baseUrl("https://wanandroid.com/")
        .addConverterFactory(GsonConverterFactory.create())   // Data parser
        .build()
Copy the code

The Builder() method does not look at the inside of the method, which is to get the current platform (Android,Java). Let’s look at the baseUrl method.

Retrofit#Builder#baseUrl
public Builder baseUrl(String baseUrl) {
  return baseUrl(HttpUrl.get(baseUrl));
}
Copy the code

HttpUrl static get: HttpUrl static get: HttpUrl static GET: HttpUrl static GET: HttpUrl static GET: HttpUrl static GET: HttpUrl static GET: HttpUrl static GET: HttpUrl static GET: HttpUrl static GET: HttpUrl static GET: HttpUrl static GET: HttpUrl static GET

Then there’s the addConverterFactory method, which adds the parser

private final List<Converter.Factory> converterFactories = new ArrayList<>();
public Builder addConverterFactory(Converter.Factory factory) {
  converterFactories.add(checkNotNull(factory, "factory == null"));
  return this;
}
Copy the code

You can have multiple parsers.

Finally, there is the build method of Retrofit.Builder

public Retrofit build(a) {
  // Just a bunch of empty logic
  if (baseUrl == null) {
    throw new IllegalStateException("Base URL required.");
  }

  okhttp3.Call.Factory callFactory = this.callFactory;
  if (callFactory == null) {
    callFactory = new OkHttpClient();
  }

  Executor callbackExecutor = this.callbackExecutor;
  if (callbackExecutor == null) {
    callbackExecutor = platform.defaultCallbackExecutor();
  }

  // Make a defensive copy of the adapters and add the default Call adapter.
  // Add an adapter to support return types other than Call objects
  List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories);
  callAdapterFactories.addAll(platform.defaultCallAdapterFactories(callbackExecutor));

  // Make a defensive copy of the converters.
  // Converters for serialization and deserialization
  List<Converter.Factory> converterFactories = new ArrayList<>(
      1 + this.converterFactories.size() + platform.defaultConverterFactoriesSize());

  // Add the built-in converter factory first. This prevents overriding its behavior but also
  // ensures correct behavior when using converters that consume all types.
  converterFactories.add(new BuiltInConverters());
  converterFactories.addAll(this.converterFactories);
  converterFactories.addAll(platform.defaultConverterFactories());

  return new Retrofit(callFactory, baseUrl, unmodifiableList(converterFactories),
      unmodifiableList(callAdapterFactories), callbackExecutor, validateEagerly);
}
Copy the code

As you can see, some of the previous parameters (baseUrl, converter, adapter, etc.) are configured into Retrofit objects.

3. Obtain network request parameters

The next interesting thing is that Retrofit actually gets the input to a web request through an API interface that we defined. Why does Retrofit translate an interface into an implementation class that we can call? So let’s look at the source code

3.1 Building an Interface Instance

In the example above, mretrofit.create (IArticleApi::class.java) converts the interface into an implementation class. Let’s take a look

public <T> T create(final Class<T> service) {
    // It must be an interface
    Utils.validateServiceInterface(service);
    
    return (T) Proxy.newProxyInstance(service.getClassLoader(), newClass<? >[] { service },new InvocationHandler() {
          private final Platform platform = Platform.get();

          @Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
              throws Throwable {...// Reading all data in method will be all input parameters to the network request
            ServiceMethod<Object, Object> serviceMethod =
                (ServiceMethod<Object, Object>) loadServiceMethod(method);
            Args is the parameter data of the method
            OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
            returnserviceMethod.adapt(okHttpCall); }}); }Copy the code

Using a dynamic proxy, you can retrieve data such as annotations and parameters from the methods that execute them and store them in a serviceMethod object. The serviceMethod and args(parameter values) are stored in the OkHttpCall, first here and later. Now let’s see, how do WE get the data in method

3.2 ServiceMethod Obtaining input parameters

We enter from the loadServiceMethod method above

// Cache Method and ServiceMethod, each time according to the Method to read the data is very difficult, cache, next time directly return, very efficient
private finalMap<Method, ServiceMethod<? ,? >> serviceMethodCache =newConcurrentHashMap<>(); ServiceMethod<? ,? > loadServiceMethod(Method method) {// There are caches for cachesServiceMethod<? ,? > result = serviceMethodCache.get(method);if(result ! =null) return result;
    
    synchronized (serviceMethodCache) {
      result = serviceMethodCache.get(method);
      if (result == null) {
        // Pass in method and read its data
        result = new ServiceMethod.Builder<>(this, method).build();
        // Cache the serviceMethodserviceMethodCache.put(method, result); }}return result;
}
Copy the code

LoadServiceMethod cache Method and ServiceMethod cache Method and ServiceMethod cache Method and ServiceMethod cache Method and ServiceMethod cache Let’s go to the part inside that reads the data

Builder(Retrofit Retrofit, Method Method) {this.retrofit = Retrofit; this.method = method; // Annotations to interface methods such as GET,PUT,POST, etc. This. methodAnnotations = method.getannotations (); / / this parameter types. The parameterTypes = method. GetGenericParameterTypes (); / / an array parameter annotation Such as Query this. ParameterAnnotationsArray = method. GetParameterAnnotations (); }Copy the code

ServiceMethod#Builder() contains Retrofit instance +method+ interface method annotation + parameter type + parameter annotation into ServiceMethod, which will be used later. Next comes the build method of ServiceMethod#Builder

public ServiceMethod build(a) {
      / / 1. Get the incoming adapter If not to use the default ExecutorCallAdapterFactory
      callAdapter = createCallAdapter();
      //2. Get the return value type of the interface
      responseType = callAdapter.responseType();
      / / 3. For converter I am the incoming GsonResponseBodyConverter
      responseConverter = createResponseConverter();
    
      // Comments on the methods of the loop interface such as the one I used in the example above is GET
      for (Annotation annotation : methodAnnotations) {
        // Parse what the annotations on this method are
        parseMethodAnnotation(annotation);
      }

      // Comments on parameters
      int parameterCount = parameterAnnotationsArray.length;
      parameterHandlers = newParameterHandler<? >[parameterCount];for (int p = 0; p < parameterCount; p++) {
        // Parameter type
        Type parameterType = parameterTypes[p];
        
        // Parameter annotations
        Annotation[] parameterAnnotations = parameterAnnotationsArray[p];

        parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
      }

      return new ServiceMethod<>(this);
    }



Copy the code

This method involves a lot of operations. Points 1,2 and 3 are all related to the result, so we won’t look at them for the moment. The next step is to parse the annotations above the interface method, using the parseMethodAnnotation method.

// Resolve which HTTP request the annotation on this method represents
private void parseMethodAnnotation(Annotation annotation) {
  if (annotation instanceof DELETE) {
    parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false);
  } else if (annotation instanceof GET) {
    parseHttpMethodAndPath("GET", ((GET) annotation).value(), false);
  } else if (annotation instanceof HEAD) {
    parseHttpMethodAndPath("HEAD", ((HEAD) annotation).value(), false);
    if(! Void.class.equals(responseType)) {throw methodError("HEAD method must use Void as response type."); }}else if (annotation instanceof PATCH) {
    parseHttpMethodAndPath("PATCH", ((PATCH) annotation).value(), true);
  } else if (annotation instanceof POST) {
    parseHttpMethodAndPath("POST", ((POST) annotation).value(), true);
  }
  // Use the same logic. }Copy the code

The parseMethodAnnotation method first determines what kind of HTTP request is the notation, and then analyzes it through the parseHttpMethodAndPath method

// Get the value above the annotation
private void parseHttpMethodAndPath(String httpMethod, String value, boolean hasBody) {
  
  this.httpMethod = httpMethod;
  this.hasBody = hasBody;

  Wxarticle /list/{id}/{page}/json
  this.relativeUrl = value;
  // Find the place where the replacement value is needed. The above example results in id and page
  this.relativeUrlParamNames = parsePathParameters(value);
}
Copy the code

The parseHttpMethodAndPath method analysis gets the HTTP request method + the URL that omits the domain name + where the value in the path needs to be replaced.

Let’s continue looking at the build method of ServiceMethod’s Builder. Once you’ve parsed the annotations for the method, start parsing the annotations for the parameters. The annotation on the parameter may be an array, because there may be more than one annotation.

It then iterates over the type of the argument

Type parameterType = parameterTypes[p];
Copy the code

Get the parameter’s annotation + parameter type, passed to the parseParameter method for parsing

// Parameter annotations
Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
Copy the code
privateParameterHandler<? > parseParameter(intp, Type parameterType, Annotation[] annotations) { ParameterHandler<? > result =null;
  for(Annotation annotation : annotations) { ParameterHandler<? > annotationAction = parseParameterAnnotation( p, parameterType, annotations, annotation); result = annotationAction; }return result;
}
Copy the code

ParseParameter basically calls parseParameterAnnotation to generate ParameterHandler

privateParameterHandler<? > parseParameterAnnotation(int p, Type type, Annotation[] annotations, Annotation annotation) {
      if (annotation instanceof Url) {

        gotUrl = true;

        if (type == HttpUrl.class
            || type == String.class
            || type == URI.class
            || (type instanceof Class && "android.net.Uri".equals(((Class<? >) type).getName()))) {return new ParameterHandler.RelativeUrl();
        } else {
          throw parameterError(p,
              "@Url must be okhttp3.HttpUrl, String, java.net.URI, or android.net.Uri type."); }}else if (annotation instanceof Path) {
        gotPath = true; Path path = (Path) annotation; String name = path.value(); validatePathName(p, name); Converter<? , String> converter = retrofit.stringConverter(type, annotations);return new ParameterHandler.Path<>(name, converter, path.encoded());

      } else if (annotation instanceof Query) {
        Query query = (Query) annotation;
        String name = query.value();
        booleanencoded = query.encoded(); Class<? > rawParameterType = Utils.getRawType(type); gotQuery =true;
        if (Iterable.class.isAssignableFrom(rawParameterType)) {
          if(! (typeinstanceof ParameterizedType)) {
            throw parameterError(p, rawParameterType.getSimpleName()
                + " must include generic type (e.g., "
                + rawParameterType.getSimpleName()
                + "<String>)");
          }
          ParameterizedType parameterizedType = (ParameterizedType) type;
          Type iterableType = Utils.getParameterUpperBound(0, parameterizedType); Converter<? , String> converter = retrofit.stringConverter(iterableType, annotations);return newParameterHandler.Query<>(name, converter, encoded).iterable(); }...// The logic behind is similar, interested can read the source code to check

      return null; // Not a Retrofit annotation.
    }

Copy the code

Parses annotations on parameters that can be of many types, such as Path or Query. So parseParameterAnnotation has a lot of if.. else.. , I only listed several of them, the other logic is similar, interested in reading the source code to view.

For example, let’s just look at Path

gotPath = true;
Path path = (Path) annotation;
// Get the value of the annotationString name = path.value(); Converter<? , String> converter = retrofit.stringConverter(type, annotations);return new ParameterHandler.Path<>(name, converter, path.encoded());
Copy the code

ParameterHandler.Path or Query. ParameterHandler.Query. Each annotation has its own type.

The remaining build method for ServiceMethod is new ServiceMethod<>(this), which stores all of the above data.

And then we go back to the dynamic code method, which I’ve put down here.

ServiceMethod<Object, Object> serviceMethod =
                (ServiceMethod<Object, Object>) loadServiceMethod(method);
OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.adapt(okHttpCall);
Copy the code

OkHttpCall is a class from Retrofit, and finally we put both ServiceMethod and ARgs into the OkHttpCall object.

What serviceMethod. Adapt ultimately returns is that serviceMethod and okHttpCall are tied together,

T adapt(Call<R> call) {
    return callAdapter.adapt(call);
}
Copy the code

I initialize the Retrofit without addCallAdapterFactory (CallAdapterFactory), so this is the default ExecutorCallAdapterFactory, then ExecutorCallAdapterFactor The Y adapt method returns an ExecutorCallbackCall object

@Override public Call<Object> adapt(Call<Object> call) {
    return new ExecutorCallbackCall<>(callbackExecutor, call);
  }
Copy the code

At this point, the input parameters for the network request are almost resolved, but a little less, as we’ll see below. Encapsulate all the retrieved input parameters

4. Request the network

Our example makes a network request from the following code

airticlesCall.enqueue(object : Callback<BaseData> {
    override fun onFailure(call: Call<BaseData>, t: Throwable) {
        t.printStackTrace()
        Log.e("xfhy"."Request failed")}override fun onResponse(call: Call<BaseData>, response: Response<BaseData>) {
        val body = response.body()
        Log.e("xfhy"."Request successful ${body? .toString()}")}})Copy the code

The enqueue method of the Call, which is the ExecutorCallbackCall object because an instance of this object is returned in the dynamic proxy, is the enqueue method of the Call

ExecutorCallbackCall# enqueue@override public void enqueue(final Callback<T> Callback) {// delegate is the OkHttpCall object passed in earlier  delegate.enqueue(new Callback<T>() { @Override public void onResponse(Call<T> call, final Response<T> response) { callbackExecutor.execute(new Runnable() { @Override public void run() { if (delegate.isCanceled()) { // Emulate OkHttp's behavior of throwing/delivering an IOException on cancellation. callback.onFailure(ExecutorCallbackCall.this, new IOException("Canceled")); } else { callback.onResponse(ExecutorCallbackCall.this, response); }}}); } @Override public void onFailure(Call<T> call, final Throwable t) { callbackExecutor.execute(new Runnable() { @Override public void run() { callback.onFailure(ExecutorCallbackCall.this, t); }}); }}); }Copy the code

The enqueue method of ExecutorCallbackCall calls the enqueue method of the OkHttpCall passed in earlier, the proxy

@Override public void enqueue(final Callback<T> callback) {
    okhttp3.Call call;
    Throwable failure;

    synchronized (this) {
      executed = true;

      call = rawCall;
      failure = creationFailure;
      if (call == null && failure == null) {
        try {
          // Before we can start, we need to build the okHttp3.Call object
          call = rawCall = createRawCall();
        } catch(Throwable t) { throwIfFatal(t); failure = creationFailure = t; }}}if(failure ! =null) {
      callback.onFailure(this, failure);
      return;
    }

    if (canceled) {
      call.cancel();
    }

    call.enqueue(new okhttp3.Callback() {
      @Override public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) {
        Response<T> response;
        try {
          response = parseResponse(rawResponse);
        } catch (Throwable e) {
          callFailure(e);
          return;
        }

        try {
          callback.onResponse(OkHttpCall.this, response);
        } catch(Throwable t) { t.printStackTrace(); }}@Override public void onFailure(okhttp3.Call call, IOException e) {
        callFailure(e);
      }

      private void callFailure(Throwable e) {
        try {
          callback.onFailure(OkHttpCall.this, e);
        } catch(Throwable t) { t.printStackTrace(); }}}); }Copy the code

The Call object for OKHttp3 needs to be built from the start, because ultimately OkHttp is still needed to access the network

private okhttp3.Call createRawCall(a) throws IOException {
    okhttp3.Call call = serviceMethod.toCall(args);
    return call;
}
okhttp3.Call toCall(@Nullable Object... args) throws IOException {
    RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl, headers,
        contentType, hasBody, isFormEncoded, isMultipart);
    
    @SuppressWarnings("unchecked") // It is an error to invoke a method with the wrong arg types.
    // This is the ParameterHandler array we created earlier that contains the annotation value of the method parameter
    ParameterHandler<Object>[] handlers = (ParameterHandler<Object>[]) parameterHandlers;
    
    // Because the method arguments in the above example are annotated as Path, the apply method replaces the ID and page in the URL with real data
    for (int p = 0; p < argumentCount; p++) {
      handlers[p].apply(requestBuilder, args[p]);
    }
    
    return callFactory.newCall(requestBuilder.build());
}
Copy the code

Before building the OkHttp Call, you need to get the URL all right. For example, if the parameter annotation is Path, you need to replace the ID and page in the URL with real data. then

//RequestBuilder#build
Request build(a) {
    HttpUrl url;
    HttpUrl.Builder urlBuilder = this.urlBuilder;
    if(urlBuilder ! =null) {
      url = urlBuilder.build();
    } else {
      // No query parameters triggered builder creation, just combine the relative URL and base URL.
      //noinspection ConstantConditions Non-null if urlBuilder is null.
      url = baseUrl.resolve(relativeUrl);
    }

    RequestBody body = this.body;
    if (body == null) {
      // Try to pull from one of the builders.
      if(formBuilder ! =null) {
        body = formBuilder.build();
      } else if(multipartBuilder ! =null) {
        body = multipartBuilder.build();
      } else if (hasBody) {
        // Body is absent, make an empty body.
        body = RequestBody.create(null.new byte[0]);
      }
    }

    MediaType contentType = this.contentType;
    if(contentType ! =null) {
      if(body ! =null) {
        body = new ContentTypeOverridingRequestBody(body, contentType);
      } else {
        requestBuilder.addHeader("Content-Type", contentType.toString()); }}//requestBuilder is the request. Builder object initialized in the constructor
    // This is what a normal OkHttp web Request would do: encapsulate the URL,method, and Request
    return requestBuilder
        .url(url)
        .method(method, body)
        .build();
  }

Copy the code

At this point, you pass in the previously obtained data to the Request object, make a normal OkHttp network Request, and build a Request object.

The Call object is then created from this Request object, returning to the method in OkHttpCall above, showing only the rest of the logic

@Override public void enqueue(final Callback<T> callback) {
    call.enqueue(new okhttp3.Callback() {
      @Override public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) {
        Response<T> response;
        try {
          response = parseResponse(rawResponse);
        } catch (Throwable e) {
          callFailure(e);
          return;
        }

        try {
          // Here is the CallBack object passed in our example, the network request succeeded
          callback.onResponse(OkHttpCall.this, response);
        } catch(Throwable t) { t.printStackTrace(); }}@Override public void onFailure(okhttp3.Call call, IOException e) {
        callFailure(e);
      }

      private void callFailure(Throwable e) {
        try {
          // The network request failed
          callback.onFailure(OkHttpCall.this, e);
        } catch(Throwable t) { t.printStackTrace(); }}}); }Copy the code

Well, at this point, the network request is complete. See this article if you are interested in OkHttp access to the web

5. To summarize

Retrofit mainly implemented the interface method using the dynamic proxy pattern, which took all the input parameters of the network access request and then assembled the OkHttp request method to implement the network request using OkHttp. Easy for developers to use. The code encapsulates extremely well, impressive.