Retrofit使用详解

Catalogue
  1. 1 从okhttp的使用讲起
    1. 1.1 okHttp主要类
    2. 1.2 用法
      1. 1.2.1 HTTP GET
      2. 1.2.2 HTTP POST
  2. 2 Retrofit的使用
    1. 2.1 用法介绍
      1. 2.1.1 创建API接口
      2. 2.1.2 创建Retrofit实例
      3. 2.1.3 调用API接口
      4. 2.1.4 取消请求
      5. 2.1.5 retrofit注解
        1. (1)一般的get请求
        2. (2)动态url访问@PATH
        3. (3)查询参数的设置@Query@QueryMap
        4. (4)POST请求体方式向服务器传入json字符串@Body
        5. (5)表单的方式传递键值对@FormUrlEncoded + @Field@FieldMap
        6. (6)文件上传@Multipart + @Part@PartMap
        7. (7)下载文件
        8. (8)添加请求头@Header@Headers
    2. 2.2 配置OkHttpClient
  3. 3 结束语
  4. 参考资料

Retrofit是Square团队旗下一个知名的Android网络请求库,准确的说,它是由Square对okhttp再次封装所成。本章在详细的介绍Retrofit用法的同时,还会分析它的前世今生,包括okhttp介绍、源码分析、java注解的掌握以及Retrofit的二次封装知识。

1 从okhttp的使用讲起

前面说了Retrofit是在Okhttp的基础之上发展起来,那么要想全面的理解之,还是要把okhttp这块的用法搞懂的(不涉及源码),下面就从okhttp开始吧~

1.1 okHttp主要类

下面是okhttp的几大主要类,这一部分的内容就围绕它们进行。
OkHttpClient.java
Request.java
Call.java
RequestBody.java
Response.java

1.2 用法

1.2.1 HTTP GET

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package okhttp3.guide;

import java.io.IOException;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class GetExample {
OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
Request request = new Request.Builder()
.url(url)
.build();

Response response = client.newCall(request).execute();
return response.body().string();
}

public static void main(String[] args) throws IOException {
GetExample example = new GetExample();
String response = example.run("https://raw.github.com/square/okhttp/master/README.md");
System.out.println(response);
}
}

上面执行的流程是这样的:
第一步,在程序第9行处获取了okhttpclient的实例;
第二步,在程序第12行处,通过Request.Builder()构建一个request请求对象,这里可以配置许多网络请求所需的参数:url、header、post对象等。
第三步,在程序第16行处,通过Call类的execute()同步方法返回一个response响应对象,对象中包含了服务端返回的字符串信息。
至此,一个简单的Http Get流程算是完成了。这里要注意的是:
1、在第二步中,我们说通过Request.Builder()可以配置许多网络请求相关的信息,其实我们还可以通过 okHttpClient.newBuilder()方法配置一些更加全局的网络参数,比如:拦截器、超时时间等。
2、在第二步中提到配置的参数可以是post对象,它对应的方法是post(RequestBody body),那么有没有提供类似get(T params)的方法供请求参数的配置呢?答案是没有。所以如果需要在url中配置请求参数,还需要以拼接字符串的方式完成。
3、在第三步中除了能用Call类的execute()同步方法返回响应之外,还可以使用它的enqueue()方法异步返回响应信息,这里要看具体的使用场景了。

1.2.2 HTTP POST

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class PostExample {
public static final MediaType JSON
= MediaType.parse("application/json; charset=utf-8");

OkHttpClient client = new OkHttpClient();

String post(String url, String json) throws IOException {
RequestBody body = RequestBody.create(JSON, json);
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
return response.body().string();
}
}

String bowlingJson(String player1, String player2) {
return "{'winCondition':'HIGH_SCORE',"
+ "'name':'Bowling',"
+ "'round':4,"
+ "'lastSaved':1367702411696,"
+ "'dateStarted':1367702378785,"
+ "'players':["
+ "{'name':'" + player1 + "','history':[10,8,6,7,8],'color':-13388315,'total':39},"
+ "{'name':'" + player2 + "','history':[6,10,5,10,10],'color':-48060,'total':41}"
+ "]}";
}

public static void main(String[] args) throws IOException {
PostExample example = new PostExample();
String json = example.bowlingJson("Jesse", "Jake");
String response = example.post("http://www.roundsapp.com/post", json);
System.out.println(response);
}
}

上面是一个向服务端发送Post请求的例子,仔细观察发现它和Get部分的不同之处主要在于第11行的post(body)配置方法。参数body是我们需要上传到服务器的数据,他是RequestBody类型的对象,该对象由RequestBody.create(MediaType contentType, T t)方法生成。MediaType代表消息内容的类型,具体可翻阅Media TypesMIME 参考手册这两篇文章。后面的T对象由MediaType的类型决定,可以看出RequestBody.create是一个重载方法。
对于RequestBody,api还提供了它的两个实现类:FormBody、MultipartBody,分别对应表单和文件的上传。
当然,OkHttp在实际应用中可能还会涉及到需要重复验证的问题,具体请结合这篇文章以及掘金网的这篇文章看看。

2 Retrofit的使用

有了前面okhttp的使用经验,再来说Retrofit的使用就简单了。Retrofit其实质就是对okHttp的封装,使用面向接口的方式进行网络请求,利用动态生成的代理类封装了网络接口。Retrofit非常适合于 RESTful 风格的请求,使用注解的方式提供功能,对于注解不了解的同学,还可以结合这篇文章看看。

RESTful特征:
URL定位资源,用HTTP动词(GET,POST,DELETE,DETC)描述操作。

2.1 用法介绍

2.1.1 创建API接口

在retrofit中通过一个Java接口作为http请求的api接口。

1
2
3
4
5
//定以接口
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}

2.1.2 创建Retrofit实例

1
2
3
4
5
6
7
8
9
/**获取实例*/
Retrofit retrofit = new Retrofit.Builder()
//设置OKHttpClient,如果不设置会提供一个默认的
.client(new OkHttpClient())
//设置baseUrl
.baseUrl("https://api.github.com/")
//添加Gson转换器
.addConverterFactory(GsonConverterFactory.create())
.build();

注意:
1.retrofit2.0后:BaseUrl要以/结尾;@GET 等请求不要以/开头;@Url: 可以定义完整url,不要以 / 开头。
2.addConverterFactory提供Gson支持,可以添加多种序列化Factory,但是GsonConverterFactory必须放在最后,否则会抛出异常。

2.1.3 调用API接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
GitHubService service = retrofit.create(GitHubService.class);

//同步请求
//https://api.github.com/users/octocat/repos
Call<List<Repo>> call = service.listRepos("octocat");
try {
Response<List<Repo>> repos = call.execute();
} catch (IOException e) {
e.printStackTrace();
}

//不管同步还是异步,call只能执行一次。否则会抛 IllegalStateException
Call<List<Repo>> clone = call.clone();

//异步请求
clone.enqueue(new Callback<List<Repo>>() {
@Override
public void onResponse(Response<List<Repo>> response, Retrofit retrofit) {
// Get result bean from response.body()
List<Repo> repos = response.body();
// Get header item from response
String links = response.headers().get("Link");
/**
* 不同于retrofit1 可以同时操作序列化数据javabean和header
*/
}

@Override
public void onFailure(Throwable throwable) {
showlog(throwable.getCause().toString());
}
});

2.1.4 取消请求

我们可以终止一个请求。终止操作是对底层的httpclient执行cancel操作。即使是正在执行的请求,也能够立即终止。

1
call.cancel();

2.1.5 retrofit注解

  • 方法注解,包含@GET、@POST、@PUT、@DELETE、@PATH、@HEAD、@OPTIONS、@HTTP。
  • 标记注解,包含@FormUrlEncoded、@Multipart、@Streaming。
  • 参数注解,包含@Query、@QueryMap、@Body、@Field,@FieldMap、@Part,@PartMap。
  • 其他注解,包含@Path、@Header、@Headers、@Url。

(1)一般的get请求

1
2
3
4
public interface IWeatherGet {
@GET("GetMoreWeather?cityCode=101020100&weatherType=0")
Call<Weather> getWeather();
}

可以看到有一个getWeather()方法,通过@GET注解标识为get请求,@GET中所填写的value和baseUrl组成完整的路径,baseUrl在构造retrofit对象时给出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Retrofit retrofit = new Retrofit.Builder()
/**http://weather.51wnl.com/weatherinfo/GetMoreWeather?cityCode=101020100&weatherType=0*/
//注意baseurl要以/结尾
.baseUrl("http://weather.51wnl.com/weatherinfo/")
.addConverterFactory(GsonConverterFactory.create())
.build();
IWeatherGet weather = retrofit.create(IWeatherGet.class);
Call<Weather> call = weather.getWeather();
call.enqueue(new Callback<Weather>() {
@Override
public void onResponse(Response<Weather> response, Retrofit retrofit) {
Weather weather = response.body();
WeatherInfo weatherinfo = weather.weatherinfo;
showlog("weather="+weatherinfo.toString());
}

@Override
public void onFailure(Throwable throwable) {
showlog(throwable.getCause().toString());
}
});

(2)动态url访问@PATH

上面说的@GET注解是将baseUrl和@GET中的value组成完整的路径。有时候我们可以将路径中某个字符串设置为不同的值来请求不同的数据,这时候怎么办呢?
譬如:
可以通过retrofit提供的@PATH注解非常方便的完成上述需求。

1
2
3
4
public interface IWeatherPath {
@GET("{info}?cityCode=101020100&weatherType=0")
Call<Weather> getWeather(@Path("info") String info);
}

可以看到我们定义了一个getWeather方法,方法接收一个info参数,并且我们的@GET注解中使用{info}?cityCode=101020100&weatherType=0声明了访问路径,这里你可以把{info}当做占位符,而实际运行中会通过@PATH(“info”)所标注的参数进行替换。

(3)查询参数的设置@Query@QueryMap

文章开头提过,retrofit非常适用于restful url的格式,那么例如下面这样的url:

1
2
3
4
5
//用于访问上海天气
http://weather.51wnl.com/weatherinfo/GetMoreWeather?cityCode=101020100&weatherType=0

//用于访问北京天气
http://weather.51wnl.com/weatherinfo/GetMoreWeather?cityCode=101010100&weatherType=0

即通过传参方式使用不同的citycode访问不同城市的天气,返回数据为json字符串。我们可以通过@Query注解方便的完成,我们再次在接口中添加一个方法:

1
2
3
4
5
6
7
8
public interface IWeatherQuery {
@GET("GetMoreWeather")
Call<Weather> getWeather(@Query("cityCode") String cityCode, @Query("weatherType") String weatherType);
}
/**省略retrofit的构建代码*/
Call<Weather> call = weather.getWeather("101020100", "0");
//Call<Weather> call = weather.getWeather("101010100", "0");
/**省略call执行相关代码*/

当我们的参数过多的时候我们可以通过@QueryMap注解和map对象参数来指定每个表单项的Key,value的值,同样是上面的例子,还可以这样写:

1
2
3
4
5
6
7
8
9
10
public interface IWeatherQueryMap {
@GET("GetMoreWeather")
Call<Weather> getWeather(@QueryMap Map<String,String> map);
}
//省略retrofit的构建代码
Map<String, String> map = new HashMap<String, String>();
map.put("cityCode", "101020100");
map.put("weatherType", "0");
Call<Weather> call = weather.getWeather(map);
//省略call执行相关代码

(4)POST请求体方式向服务器传入json字符串@Body

我们app很多时候跟服务器通信,会选择直接使用POST方式将json字符串作为请求体发送到服务器,那么我们看看这个需求使用retrofit该如何实现。

1
2
3
4
5
6
7
public interface IUser {
@POST("add")
Call<List<User>> addUser(@Body User user);
}
/省略retrofit的构建代码
Call<List<User>> call = user.addUser(new User("watson", "male", "28"));
//省略call执行相关代码

可以看到其实就是使用@Body这个注解标识我们的参数对象即可,那么这里需要考虑一个问题,retrofit是如何将user对象转化为字符串呢?将实例对象根据转换方式转换为对应的json字符串参数,这个转化方式是GsonConverterFactory定义的。
对应okhttp,还有两种requestBody,一个是FormBody,一个是MultipartBody,前者以表单的方式传递简单的键值对,后者以表单的方式上传文件可以携带参数,retrofit也二者也有对应的注解,下面继续~

(5)表单的方式传递键值对@FormUrlEncoded + @Field@FieldMap

这里我们模拟一个登录的方法,添加一个方法:

1
2
3
4
5
6
7
8
public interface IUser {
@FormUrlEncoded
@POST("login")
Call<User> login(@Field("username") String username, @Field("password") String password);
}
//省略retrofit的构建代码
Call<User> call = user.login("watson", "123");
//省略call执行相关代码

看起来也很简单,通过@POST指明url,添加FormUrlEncoded,然后通过@Field添加参数即可。
当我们有很多个表单参数时也可以通过@FieldMap注解和Map对象参数来指定每个表单项的Key,value的值。

1
2
3
4
5
6
7
8
9
10
11
public interface IUser {
@FormUrlEncoded
@POST("login")
Call<User> login(@FieldMap Map<String,String> fieldMap);
}
//省略retrofit的构建代码
Map<String, String> propertity = new HashMap<String, String>();
positories.put("name", "watson");
positories.put("password", "123");
Call<User> call = user.login(propertity);
//省略call执行相关代码

(6)文件上传@Multipart + @Part@PartMap

1.下面先看一下单文件上传,依然是再次添加个方法:

1
2
3
4
5
public interface IUser {
@Multipart
@POST("register")
Call<User> registerUser(@Part MultipartBody.Part photo, @Part("username") RequestBody username, @Part("password") RequestBody password);
}

这里@MultiPart的意思就是允许多个@Part了,我们这里使用了3个@Part,第一个我们准备上传个文件,使用了MultipartBody.Part类型,其余两个均为简单的键值对。

1
2
3
4
5
File file = new File(Environment.getExternalStorageDirectory(), "icon.png");
RequestBody photoRequestBody = RequestBody.create(MediaType.parse("image/png"), file);
MultipartBody.Part photo = MultipartBody.Part.createFormData("photos", "icon.png", photoRequestBody);

Call<User> call = user.registerUser(photo, RequestBody.create(null, "abc"), RequestBody.create(null, "123"));

这里感觉略为麻烦。不过还是蛮好理解~~多个@Part,每个Part对应一个RequestBody。
注:这里还有另外一个方案也是可行的:

1
2
3
4
5
public interface ApiInterface {
@Multipart
@POST ("/api/Accounts/editaccount")
Call<User> editUser (@Header("Authorization") String authorization, @Part("photos\"; filename=\"icon.png") RequestBody file , @Part("FirstName") RequestBody fname, @Part("Id") RequestBody id);
}

这个value设置的值不用看就会觉得特别奇怪,然而却可以正常执行,原因是什么呢?
当上传key-value的时候,实际上对应这样的代码:

1
builder.addPart(Headers.of("Content-Disposition", "form-data; name=\"" + key + "\""), RequestBody.create(null, params.get(key)));

也就是说,我们的@Part转化为了

1
Headers.of("Content-Disposition", "form-data; name=\"" + key + "\"")

这么一看,很随意,只要把key放进去就可以了。但是,retrofit2并没有对文件做特殊处理,文件的对应的字符串应该是这样的

1
Headers.of("Content-Disposition", "form-data; name="photos";filename="icon.png"");

与键值对对应的字符串相比,多了个\”; filename=\”icon.png,就因为retrofit没有做特殊处理,所以你现在看这些hack的做法

1
2
3
4
5
6
@Part("photos\"; filename=\"icon.png")
==> key = photos\"; filename=\"icon.png

form-data; name=\"" + key + "\"
拼接结果:==>
form-data; name="photos"; filename="icon.png"

因为这种方式文件名写死了,我们上文使用的的是@Part MultipartBody.Part file,可以满足文件名动态设置。

2.如果是多文件上传呢?

1
2
3
4
5
public interface IUser {
@Multipart
@POST("register")
Call<User> registerUser(@PartMap Map<String, RequestBody> params, @Part("password") RequestBody password);
}

这里使用了一个新的注解@PartMap,这个注解用于标识一个Map,Map的key为String类型,代表上传的键值对的key(与服务器接受的key对应),value即为RequestBody,有点类似@Part的封装版本。

1
2
3
4
5
6
7
File file = new File(Environment.getExternalStorageDirectory(), "local.png");
RequestBody photo = RequestBody.create(MediaType.parse("image/png", file);
Map<String, RequestBody> map = new HashMap<>(String, RequestBody);
map.put("photos\"; filename=\"icon.png", photo);
map.put("username", RequestBody.create(null, "abc"));

Call<User> call = user.registerUser(map, RequestBody.create(null, "123"));

可以看到,可以在Map中put进一个或多个文件,键值对等,当然你也可以分开,单独的键值对也可以使用@Part,这里又看到设置文件的时候,相对应的key很奇怪,例如上例”photos\”; filename=\”icon.png”,前面的photos就是与服务器对应的key,后面filename是服务器得到的文件名,ok,参数虽然奇怪,但是也可以动态的设置文件名,不影响使用。

(7)下载文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GET("download")
Call<ResponseBody> downloadTest();

Call<ResponseBody> call = user.downloadTest();
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
InputStream is = response.body().byteStream();
//save file
}

@Override
public void onFailure(Call<ResponseBody> call, Throwable t){}
});

(8)添加请求头@Header@Headers

@Header:header处理,不能被互相覆盖,所有具有相同名字的header将会被包含到请求中。

1
2
3
4
//静态设置Header值
@Headers("Authorization: authorization")
@GET("widget/list")
Call<User> getUser()

@Headers 用于修饰方法,用于设置多个Header值。

1
2
3
4
5
6
@Headers({
"Accept: application/vnd.github.v3.full+json",
"User-Agent: Retrofit-Sample-App"
})
@GET("users/{username}")
Call<User> getUser(@Path("username") String username);

还可以使用@Header注解动态的更新一个请求的header。必须给@Header提供相应的参数,如果参数的值为空header将会被忽略,否则就调用参数值的toString()方法并使用返回结果。

1
2
3
//动态设置Header值
@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)

2.2 配置OkHttpClient

很多时候,比如你使用retrofit需要统一的log管理,缓存管理,给每个请求添加统一的header等,这些都应该通过okhttpclient去操作。Retrofit 2.0 底层依赖于okHttp,所以需要使用okHttp的Interceptors来对所有请求进行拦截。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
OkHttpClient client = new OkHttpClient();
client.networkInterceptors().add(new Interceptor() {
@Override
public com.squareup.okhttp.Response intercept(Chain chain) throws IOException {
com.squareup.okhttp.Response response = chain.proceed(chain.request());

// Do anything with response here

return response;
}
});
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
...
.client(client) //传入自己定义的client
.build();

或许你需要更多的配置,你可以单独写一个OkhttpClient的单例生成类,在这个里面完成你所需的所有的配置,然后将OkhttpClient实例通过方法公布出来,设置给retrofit。

1
2
3
Retrofit retrofit = new Retrofit.Builder()
.callFactory(OkHttpUtils.getClient())
.build();

callFactory方法接受一个okhttp3.Call.Factory对象,OkHttpClient即为一个实现类。

拓展知识
从 Android P 开始,应用禁止使用网络明文传输,也就是说 Http 链接不能在 Android P 的应用上使用了,必须使用 Https 类型 Url 作为网络请求地
址。

3 结束语

这篇文章是Android网络编程的入门,如果要深入理解网络编程相关的知识,建议结合具体需求理解 OkHttp 和 Retrofit 的源码。

另外,扔物线老师的 Hencoder plus 课程对于网络编程相关的知识讲解得很透彻,也可以去参考。同时,笔者写的Android 问题汇总这篇文章也有分析网络部分难理解的知识点,可以结合起来看。

总之,网络编程知识学习路线前后分为两大块:
1、网络理论知识。可以参考 Hencoder plus 课程网络相关知识和笔者的Android问题汇总中网络相关讲解。
2、网络编程实操。根据这篇文章+官方文档+必要的源码理解(源码理解可以参考扔物线老师的 Hencoder plus 课程,但课程中没有涵盖全部,其它的源码知识要依据自己需求去研究)。

下面是 Retrofit 源码阅读的几个切入点:
1、掌握整体实现逻辑。
2、动态代理的使用。
3、Converter 的使用原理,如:GsonConverter。
4、CallAdapter 的使用原理,如 RxJava2CallAdapter。
5、线程切换。
6、如何取消网络请求。
7、Retrofit与协程的配合使用,这个在笔者的 [Kotlin 使用笔记](https://lianjiehao.github.io/2019/08/28/Kotlin%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/) 这篇文章中有提及,可以配合着理解。

下面是 Okhttp 源码阅读的几个切入点:
1、掌握整体实现逻辑( RealCall 是关键)。
2、OkHttpClient 中各个属性的含义。
3、线程切换。
4、getResponseWithinterceptorChain() 的实现原理。
5、各个 Interceptor 负责的功能。

最后,推荐一个非常火的网络调试工具:stetho。利用它你可以非常方便地查看应用的本地数据库、SharePerence、网络请求等数据。这里有一篇关于它的使用指南


参考资料

Retrofit
OkHttp
Android Retrofit框架解析