Xiaoyou said:

High force gehaowen, program yuan said after reading bragging force have talked ~

By Huo Binggan, Tencent

From: Tencent Youtest’s good gay friend — Tencent Bugly, there is a QR code at the end of the article

— — — — — — –

Android development, from the native HttpUrlConnection to the classic Apache HttpClient, and then to the front of these network infrastructure framework encapsulation, such as Volley, Async HttpClient, There are many Http related open source frameworks to choose from, among which Retrofit, which is open source by the well-known Square company, is popular for its simple interface configuration, powerful extension support, and elegant code structure. Because Square’s framework is as simple and elegant as ever, I’ve been wondering if the company is only looking for Virgos.

1. Getting to know Retrofit

From the word Retrofit, you wouldn’t be able to tell what it was for, and neither would I 🙂 escape.

Retrofitting refers to the addition of new technology or features to older systems.

– the From Wikipedia

So we know that the guy who goes by the name Retrofit is a version of “Plus.”

1.1 an overview of the Retrofit

Retrofit is an encapsulation of a RESTful HTTP Web request framework. Note that it is not said to be a Web request framework, mainly because the web request work is not done by Retrofit. Retrofit 2.0 comes with OkHttp built in. The former focuses on encapsulation of interfaces, while the latter focuses on efficient web requests.

Our application uses the Retrofit request network, essentially encapsulating the request parameters, headers, urls, etc., using the Retrofit interface layer, and then OkHttp does the rest of the request. After the server returns the data, OkHttp hands the raw results to Retrofit, which parses the results based on the user’s needs.

At this point, you’ll see that Retrofit is actually Retrofitting OkHttp.

1.2 Hello Retrofit

Talk is useless, don’t come to a piece of code intoxicated. Using Retrofit is very simple, first you need to add dependencies to your build.gradle:

The compile 'com. Squareup. Retrofit2: retrofit: 2.0.2'Copy the code

You must want to access GitHub’s API, so we’ll define an interface:

public interface GitHubService {  
  @GET("users/{user}/repos")
  Call> listRepos(@Path("user") String user);
}Copy the code

The listRepos method in the interface is the API we want to access:

https://api.github.com/users/{user}/repos

Where {user} is replaced by user, the first parameter of the method, when the request is initiated.

Ok, now that we have the interface, we need to construct Retrofit:

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);Copy the code

Here’s a service for you

Call> repos = service.listRepos("octocat");Copy the code

The code that makes the request is like this: the repos that you return is not really the result of data, it’s more like an instruction that you can execute at the right moment:

What feeling? Ever suddenly feel as if requesting an interface were as simple as accessing your own methods? Well, what we saw earlier is the official Retrofit demo. You think that’s enough? Poof… no way.

1.3 the Url configuration

Retrofit supported protocols including the GET/POST/PUT/DELETE/HEAD/PATCH, of course you also can directly use the HTTP request from definition. These protocols are configured as annotations, as we have seen with GET:

  @GET("users/{user}/repos")
  Call> listRepos(@Path("user") String user);Copy the code

Each of these annotations takes a parameter value to configure its path, such as users/{user}/repos in the example. We also noticed that when constructing Retrofit we also passed in a baseUrl(“https://api.github.com/”), The request’s full Url is consolidated with the annotated value (” path “) via baseUrl as follows:

  • pathIs the form of an absolute path:

    path = "/apath".baseUrl = "http://host:port/a/b"

    Url = "http://host:port/apath"
  • pathIs the relative path,baseUrlIs in table of contents form:

    path = "apath".baseUrl = "http://host:port/a/b/"

    Url = "http://host:port/a/b/apath"
  • pathIs the relative path,baseUrlIs in document form:

    path = "apath".baseUrl = "http://host:port/a/b"

    Url = "http://host:port/a/apath"
  • pathIs the full Url:

    path = "http://host:port/aa/apath".baseUrl = "http://host:port/a/b"

    Url = "http://host:port/aa/apath"

    You are advised to use the second configuration mode and use the same path as possible. If you mix multiple configurations in your code, just in time for your giddy day, you’ll get a bunch of bugs.

1.4 Parameter Types

When making a request, you need to pass in parameters, and Retrofit makes Http request parameters more straightforward and type-safe by annotating them.

1.4.1 Query & QueryMap

@GET("/list")
Call list(@Query("page") int page);Copy the code

Query is simply the Url ‘? Key-value after ‘, as in:

http://www.println.net/?cate=android

Here the cate=android is a Query, and we only need to add a parameter in the interface method when configuring it:

interface PrintlnServer{    
   @GET("/")    
   Call cate(@Query("cate") String cate);
}Copy the code

At this point, you might be thinking, “If I have a lot of queries, isn’t it tiring to write one by one?” And depending on different situations, some fields may not be passed, which is obviously inconsistent with the parameter requirements of the method. The fight version of QueryMap came out of nowhere, and it’s very simple to use, which I won’t go into.

1.4.2 Field & FieldMap

In fact, there are relatively many scenarios where POST is used. Most server-side interfaces need to do encryption, authentication and verification, but GET obviously cannot meet this requirement. The scenario of using POST to submit forms is even more needed.

   @FormUrlEncoded
   @POST("/")   
   Call example(
       @Field("name") String name,
       @Field("occupation") String occupation);Copy the code

We just need to define the interface above. We declare the form items with Field, so submitting the form is as straightforward as a normal function call.

Wait, you said you have an indeterminate number of form items? Or are there a lot of terms that you can’t be bothered to write? Field also has a group fight version – FieldMap, so try it out

1.4.3 Part & PartMap

This is for uploading files. HttpClient (HttpClient) : HttpClient (HttpClient) : HttpClient (HttpClient) : But now, with Retrofit, mom doesn’t have to worry about uploading files anymore

public interface FileUploadService {  
    @Multipart
    @POST("upload")    
Call upload(@Part("description") RequestBody description,
                              @Part MultipartBody.Part file);
}Copy the code

If you need to upload a file, define an interface method, as we did before. Note that the method will no longer have @formurlencoded annotation, but @multipart, and then just add Part to the parameters. You may ask, what is the difference between Part and Field? In fact, in terms of function, it is nothing more than that the client sends a request to the server to carry parameters in different ways, and the former can carry more diversified parameter types, including data flow. Because of this, we can upload files in this way. Here is how to use this interface:

For my experiment, I uploaded a file with just one line of text:

Visit me: http://www.println.netCopy the code

So let’s go to the server and see what our request looks like:

HEADERS



FORM/POST PARAMETERS

description: This is a descriptionCopy the code

RAW BODY



We see that the contents of the file we uploaded appear in the request. If you need to upload multiple files, declare multiple Part parameters, or try PartMap.

1.5 Converter, enrich your input and return types

1.5.1 RequestBodyConverter

In 1.4.3, I showed you how to upload files using Retrofit, which is actually a process. It’s still a little less concise, we’re just providing a file for uploading, but we’ve constructed three objects:

Oh, my God. There must be something wrong. Retrofit actually allows us to define our own input and return types, but if those types are special, we also need to prepare corresponding Converters, and because of Converters, Retrofit is very flexible in terms of input and return types.

Let’s change the Service code a bit:

public interface FileUploadService {  
    @Multipart
    @POST("upload")    
    Call upload(@Part("description") RequestBody description,        

        @Part("aFile") File file);
}Copy the code

Now we have changed the input type to the familiar File. If you send a request like this, the server will receive the result that will make you cry…

RAW BODY



The server receives a path to a file, and it must feel

Retrofit didn’t know what to do when it realized that the actual entry it received was a File. It was a toString, and it was a JsonString. ). .

Equesterequestbodyconverter FileRequestBodyConverter

static class FileRequestBodyConverterFactory extends Converter.Factory { @Override public Converter requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) { return new FileRequestBodyConverter(); } } static class FileRequestBodyConverter implements Converter { @Override public RequestBody convert(File file) throws IOException { return RequestBody.create(MediaType.parse("application/otcet-stream"), file); }}Copy the code

Remember to configure Retrofit when creating it:

addConverterFactory(new FileRequestBodyConverterFactory())Copy the code

That way, the contents of our files can be uploaded. Here, check out the results:

RAW BODY



The content of the File was successfully uploaded. Of course, there are still some problems. The Converter that directly uses Retrofit can’t do this, mainly because we can’t convert File to multipartbody. Part directly through Converter. If we wanted to do this, we could make a few modifications to Retrofit’s source code, which we’ll talk about later.

1.5.2 ResponseBodyConverter

We gave you a simple example of how to customize RequestBodyConverter, and Retrofit also supports customizing ResponseBodyConverter.

Let’s look at the interface we defined:

public interface GitHubService {  
   @GET("users/{user}/repos")
  Call> listRepos(@Path("user") String user);
}Copy the code

The return value is of type List, and the original return must be a string (or byte stream). Where does this return type come from? First of all that is, making the API returns a Json string, that is to say, we need to use the Json serialization to get the List, which is actually GsonResponseBodyConverter used.

The problem is that if the requested Json string does not correspond to the return value type, for example:

Json string returned by the interface:

{"err":0, "content":"This is a content.", "message":"OK"}Copy the code

Return value type

class Result{ int code; // Equivalent to err String body; // equivalent to content String MSG; // equivalent to message}Copy the code

Wow, some people are going to say, are you crazy enough to go against the server? Ha ha, I’m just an example, and in a production environment, can you guarantee that this won’t happen?

In this case, Gson, no matter how awesome he is, can only cry in silence. He does not know how capricious the field mapping relationship is. Ok, now let’s customize a Converter to solve this problem!

Of course, don’t forget to add the Converter when building Retrofit so that we can happily have the interface return a Result object.

Attention!!!!! Retrofit relies heavily on the type of object to be converted when selecting the appropriate Converter, and when adding the Converter, pay attention to the inclusion of the supported types and their order.

2, Retrofit principle analysis

The previous section covered the basic uses and concepts of Retrofit, so if your goal is to learn how to use it, you can skip the rest.

But I knew you weren’t the kind to dabble! In this section, we’ll focus on the magic behind Retrofit

2.1 Who actually completes the interface request processing?

All this time, we’ve only seen interfaces that we define ourselves, such as:

public interface GitHubService {  
  @GET("users/{user}/repos")
  Call> listRepos(@Path("user") String user);
}Copy the code

And the real I use certainly can’t be interface ah, who is this mysterious guy? It’s actually a proxy object created by Retrofit. Here’s a little bit about Java dynamic proxies:

In short, when we call githubService. listRepos, we are actually calling the invocationHandler. invoke method here ~~

2.2 Send a complete request processing process

We’ve already seen that Retrofit constructs an OkHttpCall for us. In fact, each OkHttpCall corresponds to a request. It performs the most basic network request, and the Call we see in the interface’s return is an OkHttpCall by default. If we add a custom callAdapter, it ADAPTS OkHttp to the return value we need and returns it to us.

Let’s look at the Call interface:

public interface Call extends Cloneable {  
  
  Response execute() throws IOException;  
  
  void enqueue(Callback callback);  
  boolean isExecuted();  
  void cancel();  
  boolean isCanceled();  
  Call clone();  
  
  Request request();
}Copy the code

When we use interfaces, you will remember this:

Call> repos = service.listRepos("octocat");
List data = repos.execute();Copy the code

This repos is essentially an OkHttpCall instance, and execute is to initiate a network request.

OkHttpCall.execute



We see that OkHttpCall also encapsulates okHttp3. Call. In this method, we use okHttp3. Call to initiate an attack, well, to initiate a request. I won’t expand on OkHttp here.

ParseResponse mainly does the conversion from okHttp3.response to retrofit.response, and also handles the parsing of the original return:

Response parseResponse(okhttp3.Response rawResponse) throws IOException { ResponseBody rawBody = rawResponse.body(); try { T body = serviceMethod.toResponse(catchingBody); return Response.success(body, rawResponse); } catch (RuntimeException e) { catchingBody.throwIfCaught(); throw e; }}Copy the code

At this point, we have the data we want

2.3 Results fit, do you want to use RxJava?

We already mentioned the CallAdapter. By default, it doesn’t do anything with the OkHttpCall instance:

final class DefaultCallAdapterFactory extends CallAdapter.Factory { static final CallAdapter.Factory INSTANCE = new DefaultCallAdapterFactory(); @Override public CallAdapter get(Type returnType, Annotation[] annotations, Retrofit retrofit) { ... Omit some code without mercy... return new CallAdapter>() { ... Leave out some code... @Override public Call adapt(Call call) { return call; }}; }}Copy the code

Now the requirement is that I want to plug into RxJava and make the interface return Observable:

  public interface GitHub {    
    @GET("/repos/{owner}/{repo}/contributors")
    Observable> contributors(        
       @Path("owner") String owner,        
       @Path("repo") String repo);
  }Copy the code

Is that ok? An OkHttpCall can be converted into an Observable by providing an Adapter. The Retrofit developers had this in mind for a long time and provided us with an Adapter:

RxJavaCallAdapterFactory

We just need to add it when constructing Retrofit:

addCallAdapterFactory(RxJavaCallAdapterFactory.create())Copy the code

This allows our interface to work in RxJava fashion.

Okay, take a break, have a cigarette…

To clarify how the RxJavaCallAdapterFactory works, let’s first look at the CallAdapter interface:

The code is commented in detail. In short, we just need to implement the CallAdapter class to provide the specific adaptation logic and implement the corresponding Factory to register the current CallAdapter into Retrofit. In the factory. get method, return the current CallAdapter by type. With that in mind, let’s look at RxJavaCallAdapterFactory:

RxJavaCallAdapterFactory provides more than one type of Adapter, but the principles are much the same, and interested readers can refer to the source code for themselves.

At this point, we have a clear understanding of the CallAdapter mechanism.

3. Several advanced gameplay

We’ve covered a lot of stuff up front. But, excavator students, do you think this is enough? Of course not!

3.1 Continue simplifying the Interface for uploading files

In 1.5.1 we tried to simplify the use of the File upload interface. Although we already provided a File -> RequestBody Converter, due to Retrofit’s limitations, We still can’t get as much flexibility as we could by constructing multipartBody. Part directly. What should I do? Of course is a Hack ~ ~

First, define our needs:

  • The content-type of the file needs more flexibility and should not be written to Converter. If possible, it is better to map the content-type according to the file extension, such as image.png -> image/ PNG.
  • Filename can normally be carried in the requested data.

To this end, I added a complete set of parameter resolution schemes:

1. Add the Converter for any type conversion. This step will allow us to directly convert the input type to multipartBody. Part:

public interface Converter { ... abstract class Factory { ... public Converter arbitraryConverter(Type originalType, Type convertedType, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit){ return null; }}}Copy the code

Note that the Retrofit class also needs to add corresponding methods:

public Converter arbitraryConverter( Type orignalType,Type convertedType, Annotation[] parameterAnnotations, Annotation[] methodAnnotations) { return nextArbitraryConverter(null, orignalType, convertedType, parameterAnnotations, methodAnnotations); } public Converter nextArbitraryConverter(Converter.Factory skipPast, Type type, Type convertedType, Annotation[] parameterAnnotations, Annotation[] methodAnnotations) { checkNotNull(type, "type == null"); checkNotNull(parameterAnnotations, "parameterAnnotations == null"); checkNotNull(methodAnnotations, "methodAnnotations == null"); int start = converterFactories.indexOf(skipPast) + 1; for (int i = start, count = converterFactories.size(); i < count; i++) { Converter.Factory factory = converterFactories.get(i); Converter converter = factory.arbitraryConverter(type, convertedType, parameterAnnotations, methodAnnotations, this); if (converter ! = null) { return (Converter) converter; } } return null; }Copy the code

2. The specific implementation of arbitraryConverter is given:



3. The interface to the statement, @ Part don’t incoming parameters, this Retrofit in ServiceMethod. Builder. Analytical Part parseParameterAnnotation method, The argument we passed in is assumed to be of type multipartBody. Part (we’ll actually convert it ourselves later). So when parsing, we take our Converter definition and construct a ParameterHandler:

. } else if (MultipartBody.Part.class.isAssignableFrom(rawParameterType)) { return ParameterHandler.RawPart.INSTANCE; } else { Converter converter = retrofit.arbitraryConverter(type, MultipartBody.Part.class, annotations, methodAnnotations); if(converter == null) { throw parameterError(p, "@Part annotation must supply a name or use MultipartBody.Part parameter type."); } return new ParameterHandler.TypedFileHandler((Converter) converter); }...Copy the code
static final class TypedFileHandler extends ParameterHandler{ private final Converter converter; TypedFileHandler(Converter converter) { this.converter = converter; } @Override void apply(RequestBuilder builder, TypedFile value) throws IOException { if(value ! = null){ builder.addPart(converter.convert(value)); }}}Copy the code

4. Now look at our interface declaration:

   public interface FileUploadService {     
     @Multipart
     @POST("upload")     
     Call upload(@Part("description") RequestBody description,
                               @Part TypedFile typedFile);
   }Copy the code

And how to use:

Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://www.println.net/") .addConverterFactory(new TypedFileMultiPartBodyConverterFactory()) .addConverterFactory(GsonConverterFactory.create()) .build(); FileUploadService service = retrofit.create(FileUploadService.class); TypedFile typedFile = new TypedFile("aFile", filename); String descriptionString = "This is a description"; RequestBody description = RequestBody.create( MediaType.parse("multipart/form-data"), descriptionString); Call call = service.upload(description, typedFile); call.enqueue(...) ;Copy the code

So far, we’ve taken Retrofit to the next level by lighting up our own custom file uploading skills!

3.1.2 Mock Server

In the development process, we often encounter the unstable situation of the server, testing the development environment, this is inevitable. So we needed to be able to simulate network requests to debug our client logic, which Retrofit naturally supports.

Retrofit provides a MockServer feature that allows customization of interface data returns with little change to the client’s original code. We added the following dependencies in our own project:

The compile 'com. Squareup. Retrofit2: retrofit - mock: 2.0.2Copy the code

Let’s take a look at the official demo, which defines a GituHb API.

  public interface GitHub {    
  @GET("/repos/{owner}/{repo}/contributors")
    Call> contributors(        
       @Path("owner") String owner,        
       @Path("repo") String repo);
  }Copy the code

This is the interface we’re going to request. How do we Mock it?

MockGitHub implements all of the interfaces we need to request from GitHub. When we call GitHub’s API, we will actually access the MockGitHub methods:

2. Build the Mock Server object:

 
 Retrofit retrofit = new Retrofit.Builder()
     .baseUrl(SimpleService.API_URL)
     .build(); 
 
 
 NetworkBehavior behavior = NetworkBehavior.create();
 MockRetrofit mockRetrofit = new MockRetrofit.Builder(retrofit)
     .networkBehavior(behavior)
     .build();

 BehaviorDelegate delegate = mockRetrofit.create(GitHub.class);
 MockGitHub gitHub = new MockGitHub(delegate);Copy the code

3. Using Mock Server:

Call> contributors = gitHub.contributors(owner, repo); .Copy the code

In other words, we can create a Mock data source and use the Mock Server to return the written data.

The question is, this doesn’t exactly mimic the parsing flow of web requests. How can I implement a Mock Server through Retrofit if I can only provide raw JSON strings?

Time is not early, I will not obscene development, directly push the tower ~

So far this article has been focused on Retrofit, with little mention of OkHttp, but OkHttp has an interceptor mechanism, which means that we can arbitrarily check all Retrofit requests that are about to be sent or are being sent, and tamper with it. So we just need to find the interface we want and customize our own return result. Here’s an example:

So we’ll intercept that MOLECULE’S API and customize it to go back.

4, summary

Retrofit is very powerful, and this article, through rich examples and source code mining, shows how powerful and extensible Retrofit is, so that even if it doesn’t work for your needs, you can easily modify it because the code is written beautifully.

Also, I’ve written two posts about my Retrofit hacks before, so feel free to do so

  1. Android afternoon tea: Hack Retrofit of the enhanced parameters (http://www.println.net/post/Android-Hack-Retrofit, please copy the link to the browser to open)
  2. Android afternoon tea: Hack Retrofit (2) Mock Server (ditto) http://www.println.net/post/Android-Hack-Retrofit-Mock-Server open way

The Hack after Retrofit code see lot (https://github.com/enbandari/HackRetrofit)).

Pay attention to Tencent micro signal (WXutest), there are fresh technology dry goods every week!