Android 主流开源框架(四)Retrofit 使用详解

文章首发于我的个人博客:wildma的博客,这里有更好的阅读体验,欢迎关注。

前言

最近有个想法——就是把 Android 主流开源框架进行深入分析,然后写成一系列文章,包括该框架的详细使用与源码解析。目的是通过鉴赏大神的源码来了解框架底层的原理,也就是做到不仅要知其然,还要知其所以然。

这里我说下自己阅读源码的经验,我一般都是按照平时使用某个框架或者某个系统源码的使用流程入手的,首先要知道怎么使用,然后再去深究每一步底层做了什么,用了哪些好的设计模式,为什么要这么设计。

系列文章:

更多干货请关注 AndroidNotes

一、Retrofit 介绍

前面的文章已经介绍了 OkHttp 的使用OkHttp 源码分析,不了解的强烈建议先看看这 2 篇文章。这篇介绍的是 Retrofit,它也是 Square 公司开源的网络框架,它的底层就是基于 OkHttp 实现的,不过它比 OkHttp 使用更方便,也更适合进行 RESTful API 格式的请求。

二、Retrofit 的使用

2.1 使用前准备

(1)加入网络权限 在 AndroidManifest.xml 文件中加入如下:

<uses-permission android:name="android.permission.INTERNET"/>

(2)添加 Retrofit 库的依赖
因为需要将服务器返回的 ResponseBody 转换成实体类,所以需要添加 Gson 库的依赖作为数据解析器。
最终在当前使用的 module 下的 build.gradle 中加入如下依赖:

// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.5.0'
// Gson
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'

2.2 简单的 GET 请求

这里使用 postman 提供的 GET 接口进行演示。(Postman Echo

(1)创建一个实体类,用于接收服务器返回的数据:

public class PostmanGetBean {
    private String url;
    // 其余字段省略,具体看 demo。
}

(2)创建一个接口,用于定义网络请求:

public interface PostmanService {
    @GET("get")
    Call<PostmanGetBean> testGet();
}

可以看到,这里有一个 testGet() 方法,方法上面的注解 @GET 表示 GET 请求,注解里面的 “get” 会与后面的 baseUrl 拼接成完整的路径。例如 baseUrl 为 https://postman-echo.com/,则完整的路径为 https://postman-echo.com/get。这里建议 baseUrl 以 /(斜线)结尾,注解中的 path 统一不要以 /(斜线)开头,因为这种方式看起来比较直观。
(3)创建 Retrofit 的实例:

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://postman-echo.com/")// baseUrl
        .addConverterFactory(GsonConverterFactory.create())// 解析json数据
        .build();

(4)创建网络请求接口的实例,并调用接口中的方法获取 Call 对象:

PostmanService service = retrofit.create(PostmanService.class);
Call<PostmanGetBean> call = service.testGet();

(5)进行网络请求

call.enqueue(new Callback<PostmanGetBean>() {
    @Override
    public void onResponse(Call<PostmanGetBean> call, Response<PostmanGetBean> response) {
        System.out.println(response.body().getUrl());
    }

    @Override
    public void onFailure(Call<PostmanGetBean> call, Throwable t) {

    }
});

打印结果:

https://postman-echo.com/get

示例源码:RetrofitActivity-testGetRequest

三、Retrofit 注解说明

Retrofit 中使用了大量的注解,这里将这些注解分成 3 类。

3.1 第一类:网络请求方法

分别是 @GET、@POST、@PUT、@DELETE、@PATH、@HEAD、@OPTIONS 和 @HTTP,前 7 个分别对应 HTTP 中的网络请求方法,都接收一个字符串与 baseUrl 组成完整的 URL,也可以不指定,通过 @HTTP 注解设置。最后一个 @HTTP 注解可以用来替换前面 7 个注解,以及其他扩展功能。
这里主要讲下 @HTTP 注解,其他注解与 @GET 注解类似。

@HTTP 注解示例:
@HTTP 注解有 3 个属性:method、path 与 hasBody,上面说了这个注解可以用来替换前面 7 个注解,所以就替换一下前面讲到 GET 请求中的 @GET 注解吧。

这里只需要修改接口即可,其他不变:

public interface PostmanService {
    @HTTP(method = "GET", path = "get", hasBody = false)
    Call<PostmanGetBean> testHTTP();
}

运行结果:
与 @GET 注解示例一样。
示例源码:RetrofitActivity-testHTTP

3.2 第二类:标记

3.2.1 @FormUrlEncoded 注解

简介: 表示请求体是一个 Form 表单。
示例:
这里使用 postman 提供的 POST 接口进行演示。

单个键值对传:
(1)创建实体类:

public class PostmanPostBean {
    // 字段与重写 toString() 方法省略,具体看 demo
}

(2)创建接口:

public interface PostmanService {
    @POST("post")
    @FormUrlEncoded
    Call<PostmanPostBean> testFormUrlEncoded1(@Field("username") String name, @Field("password") String password);
}

可以看到,这里使用了 @Field 注解,它属于第三类注解,用来向 Post 表单传入键值对,其中 username 表示键,name 表示值。

(3)发起请求:

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://postman-echo.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .build();

PostmanService service = retrofit.create(PostmanService.class);
Call<PostmanPostBean> call = service.testFormUrlEncoded1("wildma", "123456");
call.enqueue(new Callback<PostmanPostBean>() {
    @Override
    public void onResponse(Call<PostmanPostBean> call, Response<PostmanPostBean> response) {
        System.out.println(response.body().getForm().toString());
    }

    @Override
    public void onFailure(Call<PostmanPostBean> call, Throwable t) {

    }
});

运行结果:

FormEntity{username='wildma', password='123456'}

示例源码:RetrofitActivity-testFormUrlEncoded1

传入一个 Map 集合:
向 Post 表单传入键值对除了上面一个个传,还可以使用注解 @FieldMap 传一个 Map 集合,如下:

(1)创建接口:

public interface PostmanService {
    @POST("post")
    @FormUrlEncoded
    Call<PostmanPostBean> testFormUrlEncoded2(@FieldMap Map<String, Object> map);
}

(3)发起请求:

// 省略创建 Retrofit 的实例代码
Map<String, Object> map = new HashMap<>();
map.put("username", "wildma");
map.put("password", "123456");
Call<PostmanPostBean> call = service.testFormUrlEncoded2(map);
// 省略网络请求代码

示例源码:RetrofitActivity-testFormUrlEncoded2

3.2.2 @Multipart 注解

简介: 表示请求体是一个支持文件上传的 Form 表单。
示例:
这里使用 YESAPI 提供的图片上传接口进行演示。

单文件上传:
(1)创建实体类:

public class UploadImgBean {
    // 字段与重写 toString() 方法省略,具体看 demo
}

(2)创建接口:

public interface FileUploadService {
    @POST("?service=App.CDN.UploadImg")
    @Multipart
    Call<UploadImgBean> testFileUpload1(@Part MultipartBody.Part file, @Part("app_key") RequestBody appKey);
}

可以看到,这里使用了 @Part 注解,它属于第三类注解,用于表单字段,适用于有文件上传的情况。这里使用了@Part 的两种类型,MultipartBody.Part 表示上传一个文件,RequestBody 表示传一个键值对,其中 app_key 表示键,appKey 表示值。

(3)发起请求:

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://hn216.api.yesapi.cn/")
        .addConverterFactory(GsonConverterFactory.create())
        .build();

RequestBody appKey = RequestBody.create(null, "替换成你在 YESAPI 上获取的 appKey");
// test.png 为 SD 卡跟目录下的文件,需要提前放好
File file = new File(Environment.getExternalStorageDirectory(), "test.png");
RequestBody requestBody = RequestBody.create(MediaType.parse("image/png"), file);
// 构建 MultipartBody.Part,其中 file 为服务器约定好的 key,test.png 为文件名称
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", "test.png", requestBody);

FileUploadService service = retrofit.create(FileUploadService.class);
Call<UploadImgBean> call = service.testFileUpload1(filePart, appKey);
call.enqueue(new Callback<UploadImgBean>() {
    @Override
    public void onResponse(Call<UploadImgBean> call, Response<UploadImgBean> response) {
        System.out.println(response.body().toString());
    }

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

运行结果:

UploadImgBean{ret=200, data=DataEntity{err_code=0, err_msg='', url='http://cd7.yesapi.net/xxx.png'}, msg='当前小白接口:App.CDN.UploadImg'}

示例源码:RetrofitActivity-testFileUpload1

多文件上传:
如果想上传多个文件,则可以使用注解 @PartMap 传一个键值对为 <String, RequestBody> 的 Map 集合,如下:

(1)创建接口:

public interface FileUploadService {
    @POST("?service=App.CDN.UploadImg")
    @Multipart
    Call<UploadImgBean> testFileUpload2(@PartMap Map<String, RequestBody> map);
}

(1)发起请求:

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://hn216.api.yesapi.cn/")
        .addConverterFactory(GsonConverterFactory.create())
        .build();

RequestBody appKey = RequestBody.create(null, "替换成你在 YESAPI 上获取的 appKey");
// test.png 为 SD 卡跟目录下的文件,需要提前放好
File file = new File(Environment.getExternalStorageDirectory(), "test.png");
RequestBody requestBody = RequestBody.create(MediaType.parse("image/png"), file);
Map<String, RequestBody> requestBodyMap = new HashMap<>();
requestBodyMap.put("app_key", appKey);
// 加入一个文件,其中 file 为服务器约定好的 key,test.png 为文件名称
requestBodyMap.put("file\"; filename=\"test.png", requestBody);
// 有更多文件,则继续 put()...

FileUploadService service = retrofit.create(FileUploadService.class);
Call<UploadImgBean> call = service.testFileUpload2(requestBodyMap);
call.enqueue(new Callback<UploadImgBean>() {
    @Override
    public void onResponse(Call<UploadImgBean> call, Response<UploadImgBean> response) {
        System.out.println(response.body().toString());
    }

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

示例源码:RetrofitActivity-testFileUpload2

3.2.3 @Streaming 注解

简介: 表示响应体的数据用流的形式返回,如果没有使用该注解,默认会把数据全部载入内存,之后获取数据就从内存中读取,所以该注解一般用在返回数据比较大的时候,例如下载大文件。
示例:
这里使用下载我的博客头像( https://wildma.github.io/medias/avatars/avatar.jpg ) 进行演示。

(1)下载文件不需要创建一个实体类,直接用 ResponseBody 来接收服务器返回的数据。(后面的示例为了方便演示,也不再解析成实体类,直接用 ResponseBody 来接收服务器返回的原始数据)

(2)创建接口:

public interface FileDownloadService {
    @Streaming
    @GET("medias/avatars/avatar.jpg")
    Call<ResponseBody> testFileDownload();
}

这里使用了 @Streaming 注解用来表示响应体的数据用流的形式返回。

(3)发起请求:

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://wildma.github.io/")
        .addConverterFactory(GsonConverterFactory.create())
        .build();

FileDownloadService service = retrofit.create(FileDownloadService.class);
Call<ResponseBody> call = service.testFileDownload();
call.enqueue(new Callback<ResponseBody>() {
    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
        InputStream is = response.body().byteStream();
        // 保存文件...
    }

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

    }
});

示例源码:RetrofitActivity-testFileDownload

3.3 第三类:网络请求参数

3.3.1 @Header、@Headers 与 @HeaderMap 注解

简介: @Header 与 @HeaderMap 用于添加不固定值的请求头,@Headers 用于添加固定值的请求头。@Header 与 @HeaderMap 是作为请求方法的参数传入,@Headers 则直接添加到请求方法上。
示例:

// @Header
@GET("headers")
Call<ResponseBody> testHeader(@Header("token") String token);

// @Headers
@Headers("token: 123")
@GET("headers")
Call<ResponseBody> testHeaders();

// @Headers 多个请求头
@Headers({"token: 123", "sign: 456"})
@GET("headers")
Call<ResponseBody> testHeaders2();

// @HeaderMap
@GET("headers")
Call<ResponseBody> testHeaderMap(@HeaderMap Map<String, String> map);

示例源码:RetrofitActivity-testHeader()、testHeaders()、testHeaders2()

3.3.2 @Body 注解

简介: @Body 用于非表单请求体。很多时候后台要求前端传一个 json 字符串的请求体,这时候我们可以使用 @Body 注解来轻松实现,因为该注解可以直接传一个实体类,发起请求的过程中会把该实体类转换成 json 字符串的请求体传给后台。
示例:
(1)创建接口:

public interface PostmanService {
    @POST("post")
    Call<ResponseBody> testBody(@Body TestBodyBean testBodyBean);
}

(2)发起请求:

// 省略创建 Retrofit 的实例代码
TestBodyBean bean = new TestBodyBean();
bean.setUsername("wildma");
bean.setPassword("123456");

PostmanService service = retrofit.create(PostmanService.class);
Call<ResponseBody> call = service.testBody(bean);
// 省略网络请求代码

示例源码:RetrofitActivity-testBody()

3.3.3 @Field 与 @FieldMap 注解

简介: 用于向 Post 表单传入键值对。
示例:
具体使用前面讲 @FormUrlEncoded 注解的时候已经讲过了。
示例源码:RetrofitActivity-testFormUrlEncoded1()、testFormUrlEncoded2()

3.3.4 @Part 与 @PartMap 注解

简介: 用于表单字段,适用于有文件上传的情况。
示例:
具体使用前面讲 @Multipart 注解的时候已经讲过了。
示例源码:RetrofitActivity-testFileUpload1()、testFileUpload2()

3.3.5 @Query 与 @QueryMap 注解

简介: 用于表单字段,功能与 @Field、@FiledMap 一样,区别在于 @Query、@QueryMap 的数据体现在 URL 上,而 @Field、@FiledMap 的数据体现在请求体上,但生成的数据是一样的。
示例:
(1)创建接口:

public interface PostmanService {
    @GET("get")
    Call<ResponseBody> testQuery(@Query("username") String username);
}

(2)发起请求:

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://postman-echo.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .build();

PostmanService service = retrofit.create(PostmanService.class);
Call<ResponseBody> call = service.testQuery("wildma");
// 省略网络请求代码

上面的 baseUrl 为 https://postman-echo.com/,@GET 注解中的部分 URL 为 “get”,最终完整 URL 如果没用 @Query 注解应该是 https://postman-echo.com/get,用了注解就变成 https://postman-echo.com/get?username=wildma 了。

@QueryMap 注解则对应 Map 集合,接口如下:

public interface PostmanService {
    @GET("get")
    Call<ResponseBody> testQueryMap(@QueryMap Map<String, String> params);
}

发起请求的代码就不贴出来了,传一个对应的 Map 集合进来即可。

示例源码:RetrofitActivity-testQuery()、testQueryMap()

3.3.6 @QueryName 注解

简介: 用于没有值的查询参数,该注解实际项目中很少用到,功能与 @Query、@QueryMap 类似,参数都拼接在 URL 上,但是 @Query、@QueryMap 在 URL 上是以键值对拼接的,而 @QueryName 只是拼接键,没有值。
示例:
(1)创建接口:

public interface PostmanService {
    @GET("get")
    Call<ResponseBody> testQueryName(@QueryName String... filters);
}

注解后面可跟 String filter,也可跟 String... filters,其中后者是可变长参数,可以传多个参数也可不传参数。
(2)发起请求:

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://postman-echo.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .build();

PostmanService service = retrofit.create(PostmanService.class);
Call<ResponseBody> call = service.testQueryName("wildma","tom");
// 省略网络请求代码

上面最终拼接的 URL 为 https://postman-echo.com/get?wildma&tom

示例源码:RetrofitActivity-testQueryName()

3.3.7 @Path 注解

简介: @Path 用于设置 URL 地址的缺省值。
示例:
这里使用官方提供的 API,即获取指定用户的仓库列表进行演示。
(1)创建接口:

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

(2)发起请求:

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

GitHubService service = retrofit.create(GitHubService.class);
Call<ResponseBody> call = service.testPath("wildma");
// 省略网络请求代码

可以看到,@GET 注解里面的 “users/{user}/repos” 中有一个 “{user}”,这个就是 URL 地址的缺省值, listRepos() 方法中的 @Path("user") String user 表示传入的 urse 就是用来替换上面的 {user} 的。所以最终完整的 URL 为 https://api.github.com/users/wildma/repos

示例源码:RetrofitActivity-testPath()

3.3.8 @Url 注解

简介: @Url 用于动态设置一个完整的 URL。
示例:
(1)创建接口:

public interface PostmanService {
    @GET()
    Call<ResponseBody> testUrl(@Url String url);
}

(2)发起请求:

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

PostmanService service = retrofit.create(PostmanService.class);
Call<ResponseBody> call = service.testUrl("https://postman-echo.com/get");
// 省略网络请求代码

可以看到,baseUrl() 与 testUrl() 都设置了一个 URL,但由于 @Url 注解标识的 URL 是动态设置的,所以最终以 testUrl() 中设置的为准,也就是最终使用的是 https://postman-echo.com/get

示例源码:RetrofitActivity-testUrl()

四、设置自定义的 OkHttpClient

在创建 Retrofit 的实例的时候可以通过 client() 方法设置自定义的 OkHttpClient,自定义 OkHttpClient 可以设置统一的 header,添加 log 拦截器、Cookie 等。这里就讲下怎么设置统一的 header 吧!
(1)创建 OkHttpClient 的时候通过添加拦截器,然后在拦截器的 intercept() 方法中设置统一的 header:

OkHttpClient okHttpClient = new OkHttpClient.Builder().addInterceptor(new Interceptor() {
    @Override
    public okhttp3.Response intercept(Chain chain) throws IOException {
        Request originalRequest = chain.request();
        Request request = originalRequest.newBuilder()
                .header("token", "123")
                .header("sign", "456")
                .build();
        return chain.proceed(request);
    }
}).build();

(2)通过 client() 方法设置自定义的 OkHttpClient:

Retrofit retrofit = new Retrofit.Builder()
        .client(okHttpClient)// 设置自定义的 OkHttpClient
        .baseUrl("https://postman-echo.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .build();

示例源码:RetrofitActivity-testCustomOkHttpClient

五、关于 Converter

Retrofit 默认用 ResponseBody 来接收服务器返回的数据,如果想要转换成对应的实体类,那么在创建 Retrofit 的实例的时候可以通过 addConverterFactory() 方法设置一个数据解析器,数据解析器有多种选择,Retrofit 文档中就提供了很多种:

  • Gson: com.squareup.retrofit2:converter-gson
  • Jackson: com.squareup.retrofit2:converter-jackson
  • Moshi: com.squareup.retrofit2:converter-moshi
  • Protobuf: com.squareup.retrofit2:converter-protobuf
  • Wire: com.squareup.retrofit2:converter-wire
  • Simple XML: com.squareup.retrofit2:converter-simplexml
  • Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars

除了文档提供的这几种,其实还有一种常用的:
fastjson:'org.ligboy.retrofit2:converter-fastjson-android

这里使用 Gson 进行演示。
(1)创建接口:

public interface PostmanService {
    @GET("get")
    Call<PostmanGetBean> testGet();
}

这里直接用实体类 PostmanGetBean 替换 ResponseBody。
(2)发起请求:

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://postman-echo.com/")
        .addConverterFactory(GsonConverterFactory.create())// 添加 Gson 解析器
        .build();
// 省略网络请求代码

这里添加了 Gson 作为数据解析器。

示例源码:RetrofitActivity-testGet

六、关于 CallAdapter

前面创建接口的时候,发现接口中的方法返回类型都是 Call,如果想要返回其他类型,那么在创建 Retrofit 的实例的时候可以通过 addCallAdapterFactory() 方法设置一个 CallAdapter,Retrofit 提供了如下 CallAdapter:

  • guava:com.squareup.retrofit2:adapter-guava
  • Java8:com.squareup.retrofit2:adapter-java8:2.0.2
  • rxjava:com.squareup.retrofit2:adapter-rxjava

这里使用 RxJava 进行演示。
(1)添加相关依赖:

// 支持 rxjava2
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.5.0'
// rxjava2
compile 'io.reactivex.rxjava2:rxjava:2.2.13'
compile 'io.reactivex.rxjava2:rxandroid:2.1.1'

(2)创建接口:

@GET("get")
Observable<ResponseBody> testCallAdapter();

这里使用 Observable 替换 Call。
(3)发起请求:

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://postman-echo.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())// 设置 RxJava 作为当前的 CallAdapter
        .build();

PostmanService service = retrofit.create(PostmanService.class);
Observable<ResponseBody> observable = service.testCallAdapter();
observable.subscribeOn(Schedulers.io())               // 在 IO 线程进行网络请求
        .observeOn(AndroidSchedulers.mainThread())  // 在主线程处理请求结果
        .subscribe(new Observer<ResponseBody>() {
            @Override
            public void onSubscribe(Disposable d) {
            }

            @Override
            public void onNext(ResponseBody responseBody) {
                try {
                    System.out.println(responseBody.string());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void onError(Throwable e) {
            }

            @Override
            public void onComplete() {
            }
        });

这里设置 RxJava 作为当前的 CallAdapter,并且调用 Observable 的相关方法进行网络请求与请求结果的处理。
示例源码:RetrofitActivity-testCallAdapter

七、源码

Retrofit 的使用 demo

推荐阅读更多精彩内容