封装接口

首先,我们将将请求api封装成一个接口(interface), Retrofit 通过这个定义的interface生成一个具体的实现。
interface中进行接口api的定义,比如

1
2
3
4
public interface RepoService {
@GET("/users/{user}/repos")
Call<ResponseBody> listRepo(@Path("user") String user);
}

  • 其中 注解@GET 代表这个请求使用 GET方法

构造Retrofit

接着,我们构造Retrofit

1
2
RepoService repoService = HttpUtil_Github.getInstance().create(RepoService.class);
Call<ResponseBody> call = repoService.listRepo(user);
  • 当调用 listRepo(String user)这个方法时,传入的参数会填入到 注解中 {user}
  • HttpUtil_Github.getInstance这个方法去获取 Retrofit 的一个单例
    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
    private Retrofit retrofit;
    private static volatile HttpUtil_Github instance = null;
    public HttpUtil_Github() {
    retrofit = new Retrofit.Builder()
    .baseUrl(NetUtil.GITHUB)
    .addConverterFactory(GsonConverterFactory.create())
    .build();
    }
    //单例
    public static HttpUtil_Github getInstance(){
    HttpUtil_Github mInstance = instance;
    if (mInstance == null){
    synchronized (HttpUtil_Github.class){
    mInstance = instance;
    if (mInstance == null){
    mInstance = new HttpUtil_Github();
    instance = mInstance;
    }
    }
    }
    return mInstance;
    }
    public <T> T create(Class<T> service) {
    return retrofit.create(service);
    }

##发起请求

然后通过异步发起GET请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
try {
Log.d(TAG,""+call.request().url());
Log.d(TAG,response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
@Override public void onFailure(Call<ResponseBody> call, Throwable t) {
}
});

在创建 Retrofit 对象时,传入我们的服务器的地址 baseUrl,完整的请求路径就是通过这个baseUrl和我们注解中的地址已经传入的参数结合起来的。

这里讲一下url的配置
分为几种情况

  1. baseUrl使用根域名形式path使用绝对路径形式

    1
    2
    3
    baseUrl: https://api.github.com
    path: /users/{user}/repos
    url: https://api.github.com/users/tingya/repos
  2. baseUrl使用目录形式path使用相对路径形式

    1
    2
    3
    baseUrl: https://api.github.com/
    path: users/{user}/repos
    url: https://api.github.com/users/tingya/repos

    或者

    1
    2
    3
    baseUrl: https://api.github.com/users/
    path:{user}/repos
    url:https://api.github.com/users/tingya/repos
  3. baseUrl使用目录形式path使用绝对路径形式

    1
    2
    3
    baseUrl: https://api.github.com/users/
    path: /{user}/repos
    url: https://api.github.com/tingya/repos
  4. baseUrl使用文件形式path使用相对路径形式

    1
    2
    3
    baseUrl:https://api.github.com/users
    path:{user}/repos
    url: https://api.github.com/tingya/repos

    这种情况下会报错
    error: baseUrl must end in /: https://api.github.com/users

  5. path使用完全路径

    1
    2
    3
    baseUrl: https://api.github.com/users/
    path https://api.github.com/users/{user}/repos
    url: https://api.github.com/users/tingya/repos

当你使用完全路径时,Retrofit就会忽略掉你实例化它时通过baseUrl()方法传给它的host(接口前缀地址);
因为在实际项目中可能会有几个不同ip的host或者端口不同的host,通过这个方法可以避免实例化多个Retrofit对象

上面是最简单的GET请求的demo
下面来说说其他的请求方式以及参数的配置

GET方法

baseUrl 是 https://api.demo.com

@Path

这个注解用来替换相对路径中的值

1
2
@GET("/users/{user}/repos")
Call<ResponseBody> getMessage(@Path("user") String user);

通过Call<ResponseBody> call = repoService.getMessage("name");
最终的url等价于https://api.demo.com/user/name/repos

@Query 和 @QueryMap

这个注解是用来给GET方法传参的,只能用于GET方法

1
2
@GET("/users/name/repos")
Call<ResponseBody> getMessage(@Query("age") int age);

通过Call<ResponseBody> call = repoService.getMessage(20);
最终的url等价于https://api.demo.com/user/name/repos?age=20
当有N个参数时,我们总不能说去写N个@Query吧,如果遇到有些情况下只需要部分参数的时候,我们就得再去写一个interface,使用不同数量的参数,这样不利于我们代码的解耦
这个时候,我们应该使用 @QueryMap 这个注解

1
2
@GET("/users/name/repos")
Call<ResponseBody> getMessage(@QueryMap Map<String,Object> map)

通过

1
2
3
4
Map<String,Object> map = new HashMap();
map.put("age",20);
map.put("sex","famale");
Call<ResponseBody> call = repoService.getMessage(map);

最终的url等价于https://api.demo.com/user/name/repos?age=20&sex=famale

POST方法

@Field

通过post方法提交表单

1
2
@Post("/user/name/repos")
Call<ResponseBody> getMessage(@Field("age") int age );

通过Call<ResponseBody> call = repoService.getMessage(20)
会往http请求的body中添加表单age=20

@FieldMap

当需要通过POST方法提交多个key=value时候,可以通过@FieldMap这个注解提交多个参数

1
2
@POST("")
Call<ResponseBody> getMessage(@FieldMap Map<String,Object> map)

1
2
3
4
Map<String,Object> map = new HashMap();
map.put("age",20);
map.put("sex","famale");
Call<ResponseBody> call = repoService.getMessage(map);

传json数据

比如需要传下面这样的json格式的数据

1
2
3
4
{
name:xxx,
age:20
}

写一个 JavaBean,存放数据
比如

1
2
3
4
5
6
7
8
public class UserInfo {
private String name;
private int age;
public String getName() { return name;}
public void setName(String name) { this.name = name;}
public int getAge() { return age;}
public void setAge(int age) { this.age = age;}
}

定义接口:

1
2
@POST("/user/name/repos")
Call<ResponseBody> getMessage(@Body UserInfo userInfo);

通过下面的方法去调用接口

1
2
3
4
UserInfo userInfo = new UserInfo();
userInfo.setAge(20);
userInfo.setName("user");
Call<ResponseBody> call = service.getMessage(userInfo);

传文件

定义接口

1
2
3
4
5
@Multipart //这个注解为http请求报文头添加 Content-Type: multipart/form-data; boundary=5b7b2ddf-bef2-4a32-ac21-e4662ea82771
//对应请求头 第一行
@POST("http://api.stay4it.com/v1/public/core/?service=user.updateAvatar")
Call<ResponseBody> upload(@Part("access_token") RequestBody token, //通过`@Part`这个注解,会帮我们在请求头中生成`Content-Disposition: form-data; name="access_token"`这样的请求格式
@Part MultipartBody.Part picture);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//设置 Content-Type
RequestBody requestFile = RequestBody.create(MediaType.parse("image/png"), file);
//设置 requestFile 的 Content-Disposition form-data; name="pic"; filename="icon_equipment.png"//`filename`是指想放在服务器的图片名字
MultipartBody.Part body = MultipartBody.Part.createFormData("pic", file.getName(), requestFile);
//上面这行中,`createFormData(a,b,c)`中的第一个参数是传`name`,相当于`@Part("access_token") RequestBody token`中的`access_token`,所以一般不能使用`@Part("text") MultipartBody.Part picture`,因为在构建MultipartBody.Part时,已经包含了`picture`的name在里面,其实这并不能运行😄
UploadFileService uploadFileService = HttpUtil_Gank.getInstance().create(UploadFileService.class);
Call<ResponseBody> call = uploadFileService.upload(
RequestBody.create(MediaType.parse("multipart/form-data"), "token value jai485789hqn485yhhwb "),//携带的文字信息
body);

下面是上面的请求组成的上传文件的http Post请求头
其中boundary指的是分隔符,用来分割不同的请求部分

可以很容易看出,这个请求体是多个相同的部分组成的:
每一个部分都是以–-加分隔符开始的,然后是该部分内容的描述信息(Content-Disposition),然后一个回车,然后是描述信息的具体内容;
如果传送的内容是一个文件的话,那么还会包含文件名信息(Content-Disposition: form-data; name="pic"; filename="icon_equipment.png"),以及文件内容的类型(Content-Type)。下面的第三个小部分其实是一个文件体的结构,最后会以–分割符–结尾,表示请求体结束。
—- Android Retrofit 实现文字(参数)和多张图片一起上传

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
D/OkHttp: --> POST http://api.stay4it.com/v1/public/core/?service=user.updateAvatar http/1.1
D/OkHttp: Content-Type: multipart/form-data; boundary=ed6fdfed-e436-4096-a70a-3a7b9657c933
D/OkHttp: Content-Length: 1100
D/OkHttp: Host: api.stay4it.com
D/OkHttp: Connection: Keep-Alive
D/OkHttp: Accept-Encoding: gzip
D/OkHttp: User-Agent: okhttp/3.4.1
D/OkHttp: --ed6fdfed-e436-4096-a70a-3a7b9657c933
D/OkHttp: Content-Disposition: form-data; name="access_token"
D/OkHttp: Content-Transfer-Encoding: binary
D/OkHttp: Content-Type: multipart/form-data; charset=utf-8
D/OkHttp: Content-Length: 33
D/OkHttp: token value jai485789hqn485yhhwb
D/OkHttp: --ed6fdfed-e436-4096-a70a-3a7b9657c933
D/OkHttp: Content-Disposition: form-data; name="pic"; filename="icon_equipment.png"
D/OkHttp: Content-Type: image/png
D/OkHttp: Content-Length: 658
D/OkHttp: �PNG
D/OkHttp: 
D/OkHttp: ������IHDR������F������F������F���������PLTE������|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|�������������䰺������������������⥰؄��~�����,) )������tRNS����b����V�_LG���/,���J������IDATXì�˒�0��N@P�֜�����pԲjR��̷`y�!�M���R�L�U���T��hY�H��8+���SP8�"�f�}j�Cr�H�"'�*z��,�r �+b����]Y`����S7�y�s�Q�����&d�Iڙ5F�[��/��0}=*'�Xe��hx��RxJE�Q o1Q �M��3�:�`��*cvp�|�ke�X�T]�8[�eo^��3���y��uݸf��>w�D�1��9>>h)I23�����r��I�g��b�(���@@%� �4"��)�&����Uw~�p�u������G�-ZMD�� �"JGc����Ș �dL����C���DO������ B&���h"� �$΋r�,�ꈑk\���k _J�Y���*��U�Qܭ���������IEND�B`�
D/OkHttp: --ed6fdfed-e436-4096-a70a-3a7b9657c933--
D/OkHttp: --> END POST (1100-byte body)
D/OkHttp: <-- 200 OK http://api.stay4it.com/v1/public/core/?service=user.updateAvatar (63ms)
D/OkHttp: Server: nginx/1.4.6 (Ubuntu)
D/OkHttp: Date: Tue, 18 Oct 2016 02:05:47 GMT
D/OkHttp: Content-Type: application/json
D/OkHttp: X-Frame-Options: SAMEORIGIN
D/OkHttp: Transfer-Encoding: chunked
D/OkHttp: Proxy-Connection: Keep-alive
10-18 10:05:45.387 28938-1993/me.ppting.gank D/OkHttp: {"ret":200,"msg":"有心课堂,传递给你的不仅仅是技术✈️","data":[{"url":"uploads/icon_equipment.png","filename":"icon_equipment.png"}]}
10-18 10:05:45.387 28938-1993/me.ppting.gank D/OkHttp: <-- END HTTP (150-byte body)

如果是传多个文件,则不能像上面上传一个文件使用传MultipartBody.Part对象的方法,而是使用@PartMap这个注解,同理,传多个文件其实就是在请求头中添加多个由分隔符隔开的部分,每隔部分都需要有

1
2
Content-Disposition:form-data;name="pic";filename="filename"
Content-Type: image/png

等信息

可以将接口中的第二个参数@Part改为@PartMap Map<String, RequestBody> params
namefilename 存到 params中的第一个参数中
定义接口

1
2
3
@Multipart //这个注解为http请求报文头添加 Content-Type: multipart/form-data; boundary=5b7b2ddf-bef2-4a32-ac21-e4662ea82771
@POST("http://api.stay4it.com/v1/public/core/?service=user.updateAvatar")
Call<ResponseBody> uploadMore(@PartMap Map<String, RequestBody> map);

通过下面的代码添加请求文件并调用接口

1
2
3
4
5
6
7
Map<String, RequestBody> map = new HashMap<>();
for (File file : fileList) {
map.put(file.getName()+"\";filename=\""+file.getName(),RequestBody.create(MediaType.parse("image/png"),file));
}
UploadMoreFileService uploadMoreFileService = HttpUtil_Gank.getInstance().create(UploadMoreFileService.class);
Call<ResponseBody> call = uploadMoreFileService.uploadMore(map);

上面的map中的第一个参数是为了在Content-Disposition: form-data; name="pic"; filename="icon_equipment.png"中拼接 name="name";filename="filename",当多文件上传时,多个文件的name对应的value应该设为不同的值,所以这里我们取了文件的名字作为name的value。
写到这里我也有点疑问,那为什么不能模仿只传一个文件的时候那样使用MultipartBody.Part呢,于是我试了一下
定义接口

1
2
3
@Multipart
@POST("http://api.stay4it.com/v1/public/core/?service=user.updateAvatar")
Call<ResponseBody> uploadMore(@PartMap Map<String,MultipartBody.Part> map);

然后

1
2
3
4
5
6
7
8
9
10
11
12
13
UploadMoreFileService uploadMoreFileService = HttpUtil_Gank.getInstance().create(UploadMoreFileService.class);
RequestBody requestFile1 = RequestBody.create(MediaType.parse("image/png"),firstFile);
RequestBody requestFile2 = RequestBody.create(MediaType.parse("image/png"),secondFile);
MultipartBody.Part part1 = MultipartBody.Part.createFormData("pic", firstFile.getName(), requestFile1);
MultipartBody.Part part2 = MultipartBody.Part.createFormData("pic", secondFile.getName(), requestFile2);
Map<String, MultipartBody.Part> map = new HashMap<>();
map.put("",part1);
map.put("",part2);
Call<ResponseBody> call = uploadMoreFileService.uploadMore(map);

但是发现会报错@PartMap values cannot be MultipartBody.Part. Use @Part List<Part> or a different value type instead. (parameter #1)

添加拦截器

1
2
3
4
5
6
7
8
9
10
11
12
Request original = chain.request();
HttpUrl originalHttpUrl = original.url();
HttpUrl url = originalHttpUrl.newBuilder()
.addQueryParameter("apikey", "your-actual-api-key")
.build();
Request.Builder requestBuilder = original.newBuilder()
.url(url);
Request request = requestBuilder.build();
return chain.proceed(request);

自定义 Converter

建一个继承Converter.Factory的Factory类

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
37
38
public class StringConverterFactory extends Converter.Factory{
public static StringConverterFactory create() {
return new StringConverterFactory();
}
//{@link Body @Body}, {@link Part @Part}, and {@link PartMap @PartMap}
//以上面这几个注解的request可以在这里拦截到并进行处理 而其他的则是用下面的方法
@Override
public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
Log.d("StringConverterFactory"," type "+type.toString());
return super.requestBodyConverter(type, parameterAnnotations, methodAnnotations, retrofit);
}
@Override
public Converter<?, String> stringConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
Log.d("StringConverterFactory","stringConverter");
return super.stringConverter(type, annotations, retrofit);
}
@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
//如果不是我们想要处理的类型,则返回null,不进行处理
if (type != String.class){
return null;
}
return new StringResponseBodyConverter();//这里进行处理
}
}
public class StringResponseBodyConverter implements Converter<ResponseBody,String>{
//将 ResponseBody 转为 String
@Override public String convert(ResponseBody value) throws IOException {
Log.d("StringResponseBody ","StringResponseBody");
return value.string();
}
}

Converter是用来将ResponseBody进行转化的我们需要的类
CallAdapter是用来转化返回类型的,比如将CallObservable

分享一个小tip

在QQ群里讨论Retrofit的时候,别人分享的一个小技巧
当项目中的大多数接口需要token但有些不需要的时候,可以在@Headers中定义Authorization,并给其一个参数,例如true or false 然后在Interceptor中进行判断并进行处理
例如

1
2
3
@Headers({"\"Cache-Control\":max-age=0","Authorization:true"})
@GET("4/start-image/{width}*{height}")
Observable<StartImageInfo> getSplashImage(@Path("width") int width, @Path("height") int height);

然后在自定义的Interceptor中去取得Authorization的值
request.headers().get("Authorization"),然后进行处理

参考文章

Retrofit使用指南

Android Retrofit 实现文字(参数)和多张图片一起上传

深入浅出 Retrofit,这么牛逼的框架你们还不来看看

Android Retrofit 2.0 使用-补充篇

Android 你必须了解的网络框架Retrofit2.0

Retrofit 2.0 超能实践(三),轻松实现多文件/图片上传