Retrofit Source Code Reading

2021, Dec 20    

Retrofit is a well known network util framework which allows the developer to declare the HTTP APIs via JAVA interfaces. This article takes a close look at Retrofit source code and explain how it works. Before we start the code study, let’s take a look at use cases provided by the library:

  • Declaration of HTTP api interface via Java interface class
    • support declaration of http request method
    • support declaration of url path and dynamic variables in path
    • support declaration of http bodies/query map/multi-form
    • support customisation of http headers
  • Support invocation of sync and async calls

1. Invocation Flow

To study the code design, let’s take a look at the following fundamental usecase of retrofit:

public final class SimpleService {
  public static final String API_URL = "https://api.github.com";

  public static class Contributor {
    public final String login;
    public final int contributions;

    public Contributor(String login, int contributions) {
      this.login = login;
      this.contributions = contributions;
    }
  }

  public interface GitHub {
    @GET("/repos/{owner}/{repo}/contributors")
    Call<List<Contributor>> contributors(@Path("owner") String owner, @Path("repo") String repo);
  }

  public static void main(String... args) throws IOException {

	// Step 1. 
	// Initialisation of Retrofit and register converter | adaptors  
	// - adaptors are used for function invocation call, which returns a object:
	// 	- it can be a plain okhttp call object by default.
	//  - it can also be a observable for the case of rxjava integration.
	// - converters transforms the model form/to http body/responses.
	// the retrofit instance can be further invoked after HTTP interfaces like `github.contributors` get invoked.
	// Create a very simple REST adapter which points the GitHub API.
    Retrofit retrofit =
        new Retrofit.Builder()
            .baseUrl(API_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build();

	// Step 2. 
    // Invoke from the reflection proxy and get a interface proxy instance.
    GitHub github = retrofit.create(GitHub.class);

	// Step 3. 
    // Call via the proxy instance and based on the interface, shall return the expected return type.
    Call<List<Contributor>> call = github.contributors("square", "retrofit");

    // Step 4.
	// Actually now it belongs to a standardized 
    List<Contributor> contributors = call.execute().body();
    for (Contributor contributor : contributors) {
      System.out.println(contributor.login + " (" + contributor.contributions + ")");
    }
  }
}

Based on the several steps above, here we consolidate the invocation diagram.

1.1 Initialisation

when a Retrofit instance is build.

1

1.2 Http API Proxy Creation

when a retrofit instance is creating HTTP API proxy, the code is straight forward, refers to the code block below.

public <T> T create(final Class<T> service) {
    validateServiceInterface(service);
    return (T)
        Proxy.newProxyInstance(
            service.getClassLoader(),
            new Class<?>[] {service},
            new InvocationHandler() {
              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;
                Platform platform = Platform.get();
                return platform.isDefaultMethod(method)
                    ? platform.invokeDefaultMethod(method, service, proxy, args)
                    : loadServiceMethod(method).invoke(args);
              }
            });
  }

1.3 Proxy Invocation

The proxy based on the interface return type, function annotation and param annotation choose the most suitable CallerAdapter factory (call adapter is responsible to return the instance declared in the method return type). In the meanwhile, the method annotations such as HTTP methods, HTTP headers are parsed and cached into the request factory property. annotated function parameters are checked and paramHandlers are produced via registering requestBody converter for each of the function input if necessary.

M

Once the proxy instance is invoked. The callAdapter (like a wrapper) is returned to the user for use. Hence, the developer can inject their own types of CallerAdapterFactory to product their own types of call adapters. Refer to the following block of code:

static final class ObserveOnMainCallAdapterFactory extends CallAdapter.Factory {
    final Scheduler scheduler;

    ObserveOnMainCallAdapterFactory(Scheduler scheduler) {
      this.scheduler = scheduler;
    }

    @Override
    public @Nullable CallAdapter<?, ?> get(
        Type returnType, Annotation[] annotations, Retrofit retrofit) {
      if (getRawType(returnType) != Observable.class) {
        return null; // Ignore non-Observable types.
      }

      // Look up the next call adapter which would otherwise be used if this one was not present.
      //noinspection unchecked returnType checked above to be Observable.
      final CallAdapter<Object, Observable<?>> delegate =
          (CallAdapter<Object, Observable<?>>)
              retrofit.nextCallAdapter(this, returnType, annotations);

      return new CallAdapter<Object, Object>() {
        @Override
        public Object adapt(Call<Object> call) {
          // Delegate to get the normal Observable...
          Observable<?> o = delegate.adapt(call);
          // ...and change it to send notifications to the observer on the specified scheduler.
          return o.observeOn(scheduler);
        }

        @Override
        public Type responseType() {
          return delegate.responseType();
        }
      };
    }
}

2. Core Techniques

3. Design Patterns Used

  • Builder (for build complicated objects like RequestFactory, Retrofit, CalladapterFactory, etc)
  • Factory (for build data models like call adapater, etc)
  • The default callAdapter’s adapt method, which wraps a call object into ExecuorCallbackCall, the executor callback call is a decorator of the passed in call object.

Refer to the references

TOC