SoFunction
Updated on 2025-04-09

A brief discussion on the Android development series of network chapters: Retrofit

Retrofit is a good network request library. The official introduction is:

A type-safe REST client for Android and Java

It is very easy to use when reading the introduction on the official website, but if you don’t understand how it is implemented, you won’t dare to use it, otherwise you won’t know what to do if something goes wrong. I've been idle these days, so I've gone to take a look and learn about the general implementation method, and I won't investigate the details. Let's take a look at an example from the official website and explain in detailWebsite Officerlook

Simple example

First, define the request interface, that is, what request operations are needed in the program

public interface GitHubService {
 @GET("/users/{user}/repos")
 List<Repo> listRepos(@Path("user") String user);
}

Then, through RestAdapter, a implementation class for the interface just defined is generated, using a dynamic proxy.

RestAdapter restAdapter = new ()
  .setEndpoint("")
  .build();

GitHubService service = ();

You can call the interface for request now

List<Repo> repos = ("octocat");

It is that simple to use. Just call the interface directly when requesting, and there is no need to encapsulate parameters, because the parameter information has been defined through Annotation when defining the interface.

From the above example, we can see that the interface directly returns the required Java type, instead of byte[] or String. The place where the data is parsed is Converter. This can be customized. By default, it is parsed with Gson. That is to say, the server returns Json data by default. Different parsing methods can be used by specifying different Converts, such as parsing Json with Jackson, or custom XmlConvert to parse xml data.

The use of Retrofit is as follows:

1. Define interfaces, parameter declarations, and Url are all specified through Annotation

2. Generate an interface implementation class (dynamic proxy) through RestAdapter

3. Call the interface to request data

The definition of the interface requires some Annotation defined using Rtrofit, so let’s take a look at the Annotation first.

Annotation

See the interface in the above example

@GET("/group/{id}/users")
List<User> groupList(@Path("id") int groupId);

See @GET

/** Make a GET request to a REST path relative to base URL. */
@Documented
@Target(METHOD)
@Retention(RUNTIME)
@RestMethod("GET")
public @interface GET {
 String value();
}

@GET itself is also annotated by several Anotation. @Target means that @GET annotation is used for the method. The value method returns the value value of this annotation. In the above example, it is /group/{id}/users, and then @RestMethod

@Documented
@Target(ANNOTATION_TYPE)
@Retention(RUNTIME)
public @interface RestMethod {
 String value();
 boolean hasBody() default false;
}

RestMethod is an Annotation used for Annotation. For example, the @GET used for annotation in the above example, the value method returns GET, hasBody indicates whether there is a Body, and for the POST method, it returns true.

@Documented
@Target(METHOD)
@Retention(RUNTIME)
@RestMethod(value = "POST", hasBody = true)
public @interface POST {
 String value();
}

Retrofit's Annotation includes @GET, @POST, @HEAD, @PUT, @DELETA, @PATCH related to the request method, and @Path, @Field, @Multipart, etc. related to the parameters.

If Annotation is defined, there is a method to parse it. The location of parsing in Retrofit is RestMethodInfo, but before this, you need to see where RestMethodInfo is used. As mentioned earlier, Retrofit uses dynamic proxy to generate the implementation class of the interface we defined, and this implementation class is returned, so the location of using dynamic proxy is RestAdapter. Next, let’s take a look at RestAdapter.

RestAdapter

RestAdapter restAdapter = new ()
  .setEndpoint("")
  .build();

GitHubService service = ();

public RestAdapter build() {
 if (endpoint == null) {
  throw new IllegalArgumentException("Endpoint may not be null.");
 }
 
 ensureSaneDefaults();
 
 return new RestAdapter(endpoint, clientProvider, httpExecutor, callbackExecutor,
   requestInterceptor, converter, profiler, errorHandler, log, logLevel);
}

I won't talk about setEndPoint. The interface defines relative Url. EndPoint is the domain name. The build method calls the ensureSaneDefaults() method, and then a RestAdapter object is constructed. Several objects outside EndPoint are passed into the parameters of the constructor. These objects are initialized in ensureSaneDefaults().

private void ensureSaneDefaults() {
 if (converter == null) { converter = ().defaultConverter(); }
 if (clientProvider == null) { clientProvider = ().defaultClient(); }
 if (httpExecutor == null) { httpExecutor = ().defaultHttpExecutor(); }
 if (callbackExecutor == null) { callbackExecutor = ().defaultCallbackExecutor(); }
 if (errorHandler == null) { errorHandler = ; }
 if (log == null) { log = ().defaultLog(); }
 if (requestInterceptor == null) { requestInterceptor = ; }
}

EnsureSaneDefaults() has initialized many members, so I won’t read the errorHandler and log. Except for the requestInterceptor, the others are all obtained through the Platform object, so I need to look at the Platform first.

Platform

private static final Platform PLATFORM = findPlatform();
 static final boolean HAS_RX_JAVA = hasRxJavaOnClasspath();

 static Platform get() {
  return PLATFORM;
 }

 private static Platform findPlatform() {
  try {
   ("");
   if (.SDK_INT != 0) {
    return new Android();
   }
  } catch (ClassNotFoundException ignored) {
  }

  if (("") != null) {
   return new AppEngine();
  }

  return new Base();
 }

PLATFORM, which uses singleton, initializes the instance through findPlatform(), if it is an Android platform, if it is Google AppEngine, otherwise, these are all subclasses of Platform, among which AppEngine is a subclass of Base.

Platform is an abstract class that defines the following abstract methods. The function of these methods is to return some default implementations that need to be used in RestAdapter.

abstract Converter defaultConverter(); // The default Converter is used to convert the request result into the required data. For example, GsonConverter parses the JSON request result into a Java object using Gson. abstract  defaultClient(); // Http request class, if it is AppEngine, use `UrlFetchClient`, otherwise if there is OKHttp, use OKHttp. If it is Android, use HttpURLConnection after 2.3, use HttpClient before 2.3. abstract Executor defaultHttpExecutor(); // Executor for executing Http requests abstract Executor defaultCallbackExecutor(); // Executor used in Callback call to execute Callback (probably synchronous) abstract  defaultLog(); // Log interface, used to output Log

After reading the Platform interface, you will know that it is clear that the Converter that initializes the conversion data, the Client that executes the request, the Executor that executes the request, the Executor that executes the Callback, the Log output class, the error handling class, and the Interceptor that adds additional processing before the request.

Converter uses GsonConverter by default, so I won't read it. The defaultClient returns the client that executes the network request.

@Override  defaultClient() {
 final Client client;
 if (hasOkHttpOnClasspath()) {
  client = ();
 } else if (.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
  client = new AndroidApacheClient();
 } else {
  client = new UrlConnectionClient();
 }
 return new () {
  @Override public Client get() {
   return client;
  }
 };
}

@Override  defaultClient() {
 final Client client;
 if (hasOkHttpOnClasspath()) {
  client = ();
 } else {
  client = new UrlConnectionClient();
 }
 return new () {
  @Override public Client get() {
   return client;
  }
 };
}

@Override  defaultClient() {
 final UrlFetchClient client = new UrlFetchClient();
 return new () {
  @Override public Client get() {
   return client;
  }
 };
}

For Android, use OKHttp first, otherwise use HttpUrlConnection after 2.3, and use HttpClient before 2.3.

defaultHttpExecutor returns an Executor. The thread executing the request is executed in this Executor. It does one thing, set the thread as a background thread

When defaultCallbackExecutor is used to execute Callback type request, it provides an Executor to execute Callback Runnable

@Override Executor defaultCallbackExecutor() {
  return new ();
}

@Override Executor defaultCallbackExecutor() {
  return new MainThreadExecutor();
}

SynchronousExecutor

static class SynchronousExecutor implements Executor {
  @Override public void execute(Runnable runnable) {
   ();
  }
}

MainThreadExecutor

public final class MainThreadExecutor implements Executor {
 private final Handler handler = new Handler(());

 @Override public void execute(Runnable r) {
  (r);
 }
}

If it is Android, send the callback to the main thread for execution through the Handler, and if it is not Android, execute it synchronously.

After reading Platform, the initialization of RestAdapter members is completed. It depends on how to generate the implementation class of the interface we defined.

 public <T> T create(Class<T> service) {
  (service);
  return (T) ((), new Class<?>[] { service },
    new RestHandler(getMethodInfoCache(service)));
 }
 
 Map<Method, RestMethodInfo> getMethodInfoCache(Class<?> service) {
  synchronized (serviceMethodInfoCache) {
   Map<Method, RestMethodInfo> methodInfoCache = (service);
   if (methodInfoCache == null) {
    methodInfoCache = new LinkedHashMap<Method, RestMethodInfo>();
    (service, methodInfoCache);
   }
   return methodInfoCache;
  }
 }

A dynamic proxy is used. InvocationHandler is a RestHandler. RestHandler has a parameter, which is a mapping of Method->RestMethodInfo. This mapping is empty when initialized. The key points are these two: RestHandler, RestMethodInfo,

@Override public Object invoke(Object proxy, Method method, final Object[] args)
  throws Throwable {
 // If the method is a method from Object then defer to normal invocation.
 if (() == ) { // 1
  return (this, args);
 }
 
 // Load or create the details cache for the current method.
 final RestMethodInfo methodInfo = getMethodInfo(methodDetailsCache, method); // 2
 
 if () { // 3
  try {
   return invokeRequest(requestInterceptor, methodInfo, args);
  } catch (RetrofitError error) {
   Throwable newError = (error);
   if (newError == null) {
    throw new IllegalStateException("Error handler returned null for wrapped exception.",
      error);
   }
   throw newError;
  }
 }
 
 if (httpExecutor == null || callbackExecutor == null) {
  throw new IllegalStateException("Asynchronous invocation requires calling setExecutors.");
 }
 
 // Apply the interceptor synchronously, recording the interception so we can replay it later.
 // This way we still defer argument serialization to the background thread.
 final RequestInterceptorTape interceptorTape = new RequestInterceptorTape();
 (interceptorTape); // 4
 
 if () { // 5
  if (rxSupport == null) {
   if (Platform.HAS_RX_JAVA) {
    rxSupport = new RxSupport(httpExecutor, errorHandler);
   } else {
    throw new IllegalStateException("Observable method found but no RxJava on classpath");
   }
  }
  
  return (new Callable<ResponseWrapper>() {
   @Override public ResponseWrapper call() throws Exception {
    return (ResponseWrapper) invokeRequest(interceptorTape, methodInfo, args);
   }
  });
 }
 
 Callback<?> callback = (Callback<?>) args[ - 1]; // 6
 (new CallbackRunnable(callback, callbackExecutor, errorHandler) {
  @Override public ResponseWrapper obtainResponse() {
   return (ResponseWrapper) invokeRequest(interceptorTape, methodInfo, args);
  }
 });
 
 return null; // Asynchronous methods should have return type of void.
}

The invoke method of RestHandler will be called when executing the request. As shown above, mainly because there are 6 points marked in the above code.

1. If the Object method is called, call it directly without processing.
2. Get the RestMethodInfo corresponding to the called Method through getMethodInfo. As mentioned earlier, when constructing the RestHandler object, a mapping of Method->RestMethodInfo is passed in, which is empty at the beginning.

static RestMethodInfo getMethodInfo(Map<Method, RestMethodInfo> cache, Method method) {
  synchronized (cache) {
   RestMethodInfo methodInfo = (method);
   if (methodInfo == null) {
    methodInfo = new RestMethodInfo(method);
    (method, methodInfo);
   }
   return methodInfo;
  }

In getMethodInfo, determine if the corresponding map does not exist, create this map and cache it as shown in the name

3. If it is a synchronous call (the data is returned directly in the interface, and does not pass Callback or Observe), directly call invokeRequest

4. If it is an asynchronous call, first record the intercept request through RequestInterceptorTape, and then record it and do actual intercept in the background thread, which will be mentioned later.

5. If it is an Observe request (RxJava), execute step 5, do not understand RxJava, skip it

6. If it is in Callback form, it will be executed by the thread pool.

Each Method in the interface has a corresponding RestMethodInfo, and the processing of Annotation information in the interface is here.

RestMethodInfo

private enum ResponseType {
  VOID,
  OBSERVABLE,
  OBJECT
}
RestMethodInfo(Method method) {
   = method;
  responseType = parseResponseType();
  isSynchronous = (responseType == );
  isObservable = (responseType == );
}

parseResponseType is called in the constructor. parseResponseType parses the method signature. According to the return value type of the method and the type of the last parameter, which ResponseType is the type of the method.

No matter which ResponseType is, it is ultimately called invokeRequest to execute the actual request. Next, look at the execution steps of invokeRequest in turn.

first stepIt is a method that calls () parsing calls. The method makes judgments and only parses on the first call. Because the object is cached after one parsing, and can be used directly when the same method is called next time.

 synchronized void init() {
  if (loaded) return;

  parseMethodAnnotations();
  parseParameters();

  loaded = true;
 }

Called separately in

  • parseMethodAnnotations(): parse the Annotation of all methods
  • parseParameters(): parse all parameters Annotation
for (Annotation methodAnnotation : ()) {
 Class<? extends Annotation> annotationType = ();
 RestMethod methodInfo = null;
 // Look for a @RestMethod annotation on the parameter annotation indicating request method.
 for (Annotation innerAnnotation : ()) {
  if ( == ()) {
   methodInfo = (RestMethod) innerAnnotation;
   break;
  }
 }
 ...
}

In parseMethodAnnotations, all the Annotations of the method are obtained and traversed:

  • For each Annotation, it will also get its Annotation to see if it is an Annotation annotated by RestMethod. If so, it means it is an annotation of type @GET and @POST. Then call parsePath to parse the requested Url, requestParam (the content after the question mark in the URL) and parameter names that need to be replaced in the Url (the part enclosed in braces in Url)
  • Looking for Headers Annotation to parse Header parameters
  • RequestType: SIMPLE, MULTIPART, FORM_URL_ENCODED

parseParameters parse request parameters, that is, the Annotation of the parameters, @PATH, @HEADER, @FIELD, etc.

Step 2It is RequestBuilder and Interceptor, these two are related, so look at it together.

RequestBuilder requestBuilder = new RequestBuilder(serverUrl, methodInfo, converter);
(args);
(requestBuilder);
Request request = ();

Let’s talk about RequestInterceptor first. It’s very effective. When executing a request, it intercepts the request to do some special processing, such as adding some additional request parameters.

/** Intercept every request before it is executed in order to add additional data. */
public interface RequestInterceptor {
 /** Called for every request. Add data using methods on the supplied {@link RequestFacade}. */
 void intercept(RequestFacade request);

 interface RequestFacade {
  void addHeader(String name, String value);
  void addPathParam(String name, String value);
  void addEncodedPathParam(String name, String value);
  void addQueryParam(String name, String value);
  void addEncodedQueryParam(String name, String value);
 }

 /** A {@link RequestInterceptor} which does no modification of requests. */
 RequestInterceptor NONE = new RequestInterceptor() {
  @Override public void intercept(RequestFacade request) {
   // Do nothing.
  }
 };
}

RequestInterceptor has only one method intercept, which receives a RequestFacade parameter. RequestFacade is an interface inside the RequestInterceptor. The method of this interface is to add request parameters, Query, Header, etc. You can probably see the role of RequestInterceptor. If RequestFacade represents a request-related data, its function is to add additional Header, Param and other parameters to this RequestFacade.

A subclass of RequestFacade is called RequestBuilder, which is used to process Request request parameters. In the invokeRequest, the intercept method will be called on the RequestBuilder to add additional parameters to the RequestBuilder.

There is a class called RequestInterceptorTape, which implements RequestFacade and RequestInterceptor, and its function is:

  • When used as a RequestFacade, it passes it as a parameter to a RequestInteceptor. When this RequestInterceptor calls its addHeader and other methods, it records these calls and parameters.
  • Then when used as a RequestInterceptor, the previously recorded method calls and parameters are reapplied to its intercept parameter RequestFacade

In this case, if it is determined that the call of the method is not a synchronous call, the parameters that the user sets to add to the interceptor set by the user are recorded to the RequestInterceptorTape, and then the parameters are actually added in the invokeRequest.

// Apply the interceptor synchronously, recording the interception so we can replay it later.
// This way we still defer argument serialization to the background thread.
final RequestInterceptorTape interceptorTape = new RequestInterceptorTape();
(interceptorTape);

() parse the actual parameters when calling the interface. Then generate a Request object through the build() method

Step 3Execute the request, Response response = ().execute(request);

Step 4It is to parse and distribute the request result. The result is returned when the request is successful. The ErrorHandler failed to parse and call the user a chance to customize the exception, but in the end, they are thrown into invoke() through the exception. If it is a synchronous call, the exception will be thrown directly. If it is a Callback call, it will callback.

CallbackRunnable

The request type includes synchronization request, Callback request, and Observable request. Let’s take a look at the Callback request:

Callback<?> callback = (Callback<?>) args[ - 1];
(new CallbackRunnable(callback, callbackExecutor, errorHandler) {
  @Override public ResponseWrapper obtainResponse() {
   return (ResponseWrapper) invokeRequest(interceptorTape, methodInfo, args);
  }
});

The last parameter of the function in the Callback request is an instance of Callback. httpExecutor is an Executor used to execute the Runnable request. We see that a CallbackRunnable execution is new here and its obtainResponse method is implemented. See the implementation:

abstract class CallbackRunnable<T> implements Runnable {
 private final Callback<T> callback;
 private final Executor callbackExecutor;
 private final ErrorHandler errorHandler;

 CallbackRunnable(Callback<T> callback, Executor callbackExecutor, ErrorHandler errorHandler) {
   = callback;
   = callbackExecutor;
   = errorHandler;
 }

 @SuppressWarnings("unchecked")
 @Override public final void run() {
  try {
   final ResponseWrapper wrapper = obtainResponse();
   (new Runnable() {
    @Override public void run() {
     ((T) , );
    }
   });
  } catch (RetrofitError e) {
   Throwable cause = (e);
   final RetrofitError handled = cause == e ? e : unexpectedError((), cause);
   (new Runnable() {
    @Override public void run() {
     (handled);
    }
   });
  }
 }

 public abstract ResponseWrapper obtainResponse();
} 

It is an ordinary Runnable. In the run method, it is first executed to execute the obtainResponse. From the name, it can be seen that the request is executed and the request is returned. From the previous example, it can be seen that the invokeRequest is executed, and the request is executed the same as in the synchronous call.

Immediately afterwards, a Runnable to callbackExecutor was submitted. When looking at Platform, I saw that callbackExecutor is returned through().defaultCallbackExecutor(). In Android, a message is sent to a Handler of the main thread.

It is worth noting that for synchronous calls, if you encounter an error, you will directly throw an exception, but for asynchronous calls, you will call()

Mime

Execute network requests, you need to send request parameters to the server, such as form data, uploaded files, etc., and you also need to parse the data returned by the server. These are encapsulated in Retrofit, which is located in the Mime package. Only when the data is encapsulated can you perform the data conversion uniformly by the specified Converter.

TypedInput and TypedOutput represent input and output data, both contain mimeType, and support reading in an InputStream or writing to an OutputStrem respectively.

/**
 * Binary data with an associated mime type.
 *
 * @author Jake Wharton (jw@)
 */
public interface TypedInput {

 /** Returns the mime type. */
 String mimeType();

 /** Length in bytes. Returns {@code -1} if length is unknown. */
 long length();

 /**
  * Read bytes as stream. Unless otherwise specified, this method may only be called once. It is
  * the responsibility of the caller to close the stream.
  */
 InputStream in() throws IOException;
}

/**
 * Binary data with an associated mime type.
 *
 * @author Bob Lee (bob@)
 */
public interface TypedOutput {
 /** Original filename.
  *
  * Used only for multipart requests, may be null. */
 String fileName();

 /** Returns the mime type. */
 String mimeType();

 /** Length in bytes or -1 if unknown. */
 long length();

 /** Writes these bytes to the given output stream. */
 void writeTo(OutputStream out) throws IOException;
}

TypedByteArray, internal data is a Byte array

 

private final byte[] bytes;

 @Override public long length() {
  return ;
 }

 @Override public void writeTo(OutputStream out) throws IOException {
  (bytes);
 }

 @Override public InputStream in() throws IOException {
  return new ByteArrayInputStream(bytes);
 }

TypedString, inherited from TypedByteArray, the internal representation is the same

public TypedString(String string) {
  super("text/plain; charset=UTF-8", convertToBytes(string));
 }

 private static byte[] convertToBytes(String string) {
  try {
   return ("UTF-8");
  } catch (UnsupportedEncodingException e) {
   throw new RuntimeException(e);
  }
 }

The same goes for the others, which is easy to understand from the name: TypedFile, MultipartTypedOutput, FormEncodedTypedOutput.

other

Retrofit encapsulates the input and output, sends data to the server through TypedOutput, and reads the data returned by the server through TypedInput.

MultipartTypedOutput supports file upload. When reading server data, if you require a direct return of unresolved Response, Restonse will be converted to TypedByteArray, so it cannot be a large file class

Retrofit supports different log levels, and when so, the Request and Response Body will be printed out, so if the file is included, it will not work.

Retrofit uses GsonConverter by default, so if you want to get the original data, don't resolve Retrofit. You either customize the Conveter or directly return the Response. Returning Response is also troublesome.

Overall, Retrofit looks very useful, but it is best to standardize the request to return data on the server. Otherwise, if the request successfully returns one data structure and the request fails to return another data structure, it is difficult to use Converter to parse, and the definition of the interface is not easy to define, unless all the responses are returned, or all the interfaces of the custom Converter return String

Jake Wharton said this on Twitter:

Gearing up towards a Retrofit 1.6.0 release and then branching so we can push master towards a 2.0 and fix long-standing design issues.

2.0 is about to be released, the internal API will be changed, and the interface should not be changed much

The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.