使用Retrofit

Retrofit 实际上并不能说是一个网络请求框架,它其实是对 okHttp 这个网络请求框架在接口层面的封装,网络请求还是交给 okHttp 做的,就好像 HttpClient 和 Volley 的关系一样。Retrofit 对 Header、Url、请求参数等信息进行封装,交给 okHttp 去做网络请求,okHttp 从服务器获得的请求结果交给 Retrofit 去进行解析,所以经常有说 okHttp + Retrofit 这样搭配使用。

依赖
compile 'com.squareup.retrofit2:retrofit:2.1.0'

因为 Retrofit2.X 里面内部导入了 okHttp3,所以可以不用在导入 okHttp 的包。

使用 Retrofit 进行网络请求的步骤

1.获得 Retrofit 实例,可进行某些功能的配置(响应结果类型转化、拦截器拦截请求日志等);
2.创建请求接口,在该接口内创建返回 Call 对象的相应请求方法(在方法内使用注解,静态/动态设置请求参数、请求方式等);
3.Retrofit 实例调用 create (请求接口)获得接口对象,调用相应请求方法获得 Call 对象,Call 对象调用同步/异步请求方法发出请求,获得响应结果;

获得 Retrofit 实例

基本上,生成一个 Retrofit 实例,需要配置三块内容:
1.baseUrl:.baseUrl(),传入请求地址的根目录,通常传入的是 String ,也可以传入 HttpUrl 对象,其实传入的 String 最后还是会生成一个 HttpUrl 对象;
2.OkHttpClient 对象:.client(OkHttpClient client)/callFactory(okhttp3.Call.Factory factory),其实前者只是后者的方便写法,前者实际上内部实现还是调用后者,而设置拦截器查看日志、设置Header等等,都是在构造 OkHttpClient 对象的时候设置好的,怎么构建 OKHttpClient 对象,可以看我这一篇博客:使用okHttp 里面的相关内容
3.Converter.Factory 对象:.addConverterFactory(Converter.Factory factory),对相应结果中的数据做类型转换,Retrofit 提供了很多数据类型的 ConverterFactory,直接导入即可使用,譬如:

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

当然,也可以继承 Converter.Factory 去自定义需要的 Factory 。
注意:Converter.Factory 对象可以添加多个,但添加的顺序是有影响的,按照retrofit的逻辑,是从前往后进行匹配,如果匹配上,就忽略后面的,直接使用。
eg:当 Retrofit 试图反序列化一个 proto 格式,它其实会被当做 JSON 来对待。所以 Retrofit 会先要检查 proto buffer 格式,然后才是 JSON。所以要先添加 ProtoConverterFactory,然后是 GsonConverterFactory。

好了,看一下用代码创建 Retrofit 实例:

// 请求地址的根目录
String BASE_URL= "http://gank.avosapps.com/api/data/";
        
// 构建做好相关配置的 OkHttpClient 对象
OkHttpClient okHttpClient = new OkHttpClient();

// 获得 Converter.Factory 对象
GsonConverterFactory gsonConverterFactory =  GsonConverterFactory.create();
        
// 获得 Retrofit 实例
Retrofit retrofit = new Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(okHttpClient)
        .addConverterFactory(gsonConverterFactory)
        .build();

Retrofit 还可以有其它配置,但这里不展开说了。

创建请求接口

这是Retrofit 使用上和 OKHttp 最不一样的地方,重点是使用到了注解。

同样地,看一下最常用的 Get 请求、Post 请求是怎么做的。

Get 请求

既然已经在获得 Retrofit 实例的时候传入了根目录,所以,在请求的时候就可以直接写完整请求地址除根目录外的其它部分,根据不同的情况,Retrofit 给我们提供了下面几种注解:

@Path
使用 @Path 可以动态地访问不同的url,举个例子:
简书一篇文章的url是这样的:

http://www.jianshu.com/p/08ad8934ad2e

http://www.jianshu.com 是根目录,p 是文章目录文件夹(猜测),08ad8934ad2e是文章 ID。那么很显然只要传入文章 ID,就可以请求对应的文章,文章 ID 不是拼接的参数而是路径的一部分,那么就可以使用 @Path 了:

/**
 * 请求接口
 * Created by Eman on 2016/12/12.
 */
public interface TestApiService {

    // 完整目录
    // http://www.jianshu.com/p/08ad8934ad2e
    
    /**
     * 请求简书文章的 API
     * @param articleId 文章 ID
     * @return Call
     */
    @GET("p/{articleId}")
    Call<ArticleBean> article(@Path("articleId") String articleId);
}

/**
 * 简书文章实体
 * Created by Eman on 2016/12/12.
 */
public class ArticleBean {
    // 文章标题
    String title;
    // 文章内容
    String content;
}

这里一步步来解析:
① @GET表示请求方式,Retrofit 支持的请求方式还有 @Post、@Delete、@Put 等,()里面的内容是除根目录外的剩余路径;
② 请求接口的方法一定要返回 Call<T> 对象,T 是 Retrofit 对响应内容进行数据类型转换后的数据实体,上面例子的就是ArticleBean。

接下来就可以说一下 @Path 了:
① 动态获取的部分路径用{}包含起来;
② @Path(XXX) 里面的 XXX 要最好和 {XXX} 保持一致(不一致其实也没问题);
③ @Path 可以用于任何请求方式,包括 Post,Put,Delete 等等。

@Url
使用全路径复写 baseUrl,适用于非统一 baseUrl 的场景。意思是,我们在创建 Retrofit 实例的时候传入了请求地址的根目录,但有时候偏偏有些请求地址根本不是 baseUrl 下的,那么就可以使用 @Url 这个注解:

// 原BaseUrl
// http://www.jianshu.com/

// 现在需要的Url
// http://www.jianshu.com/writer#/notebooks/8255432/notes/7513289
    
/**
 * 请求编辑简书文章的API
 * @param url 编辑简书文章的请求地址
 * @return Call
 */
@GET
Call<WriteArticleBean> writeArticle(@Url String url);

注意:@Url 这个注解同样可以给 @POST、@PUT、@DELETE 这几种请求使用。

@Query
这个注解是用来完成 Get 请求的传参的,继续用获取简书文章作为例子,假设文章 ID 是请求参数:

// 完整目录
// http://www.jianshu.com/p?userId=2653577186&articleId=08ad8934ad2e
    
/**
 * 请求简书文章的API
 * @param userId用户 ID
 * @param articleId 文章 ID
 * @return Call
 */
@GET("p")
Call<ArticleBean> article(@Query("userId") int userId, @Query("articleId") int articleId);

这样就可以实现 Get 请求的传参了,不过需要注意的是:
① “?”不用写进去了;
② 一个@Query 对应一个参数,注意参数名和参数类型;
③ 如果请求参数为非必填,也就是说即使不传该参数,服务端也可以正常解析,那么,请求方法定义处还是需要 完整的 Query 注解,某次请求如果不需要传该参数的话,只需填充 null 即可。

@QueryMap
虽然 @Query 就可以传参了,但如果有多个请求参数,很难说不会写错,所以可以用 @QueryMap,直接传入一个包含了多个请求参数的 Map:

// 完整目录
// http://www.jianshu.com/p?userId=2653577186&articleId=08ad8934ad2e
    
/**
 * 请求简书文章的API
 * @param Map 请求参数集合
 * @return Call
 */
@GET("p")
Call<ArticleBean> article(@QueryMap Map<String, Object> params);

基本上 Get 请求使用这几种注解就足够了,Post 请求面对的情况更多,来看一下 Post 请求。

Post请求

首先,Post 请求在不要求请求参数的时候和 Get 请求是一样的,只是需要把注解换成 @Post,同样使用 @Path 也是一样的。所以来看 Post 请求各种需要请求参数的情况。

@Field

/**
 * 简书登录 API
 * @param username 用户名
 * @param password 密码
 * @return Call
 */
@FormUrlEncoded
@POST("login/")
Call<UserBean> login(@Field("username") String username, @Field("password") String password);

同样的,也可以把请求参数放在一起,使用注解 @FieldMap

@FieldMap

/**
 * 简书登录 API
 * @param params 请求参数集合
 * @return Call
 */
@FormUrlEncoded
@POST("login/")
Call<UserBean> login(@FieldMap HashMap<String, String> params);

注意:
① @Field 和 @FieldMap 都属于表单传值,要加上 @FormUrlEncoded ,它将会自动将请求参数的类型调整为application/x-www-form-urlencoded;
② @Field 将每一个请求参数都存放至请求体中,还可以添加 encoded 参数,该参数为 boolean 型,具体的用法为:

@Field(value = "username", encoded = true) String username

encoded 参数为 true 的话,key-value-pair 将会被编码,即将中文和特殊字符进行编码转换。

@Body
如果请求参数不是基本数据类型,譬如想直接上传一个 JSON,或者一个封装好的实体类对象(与后台协商好,将一堆请求参数封装进一个对象里面简直太方便),就可以使用这个注解,直接把对象通过ConverterFactory转化成对应的参数:

/**
 * 简书登录 API
 * @param user 用户实体
 * @return Call
 */
@POST("login/")
Call<UserBean> login(@Body User user);

@Part
如果想实现上传更多不同类型的请求参数数据呢?譬如文件的上传,请看:

/**
 * 简书上传图片 API
 * @param imgName 图片名
 * @param description 图片描述
 * @param imgFile 图片文件
 * @return Call
 */
@Multipart
@POST("p/unload")
Call<ArticleBean> upload(@Part("imgName") String imgName,
                         @Part("description") RequestBody description,
                         @Part MultipartBody.Part imgFile);

这里要注意了:
① @Multipart 表示允许使用多个 @Part;
② 每一个 @Part 对应的是一个 key-value,value 可以是任何值,譬如上面例子的 String,但最好是 RequestBody,就算 description 的内容是String,那也要构造出一个 RequestBody 再放进请求方法内;eg:

// description 内容
String description = "It is the description";
// 构造成 RequestBody
RequestBody qbDescription = RequestBody.create(MediaType.parse(multipart/form-data), description);

③ File 不是直接使用 RequestBody,而是使用它的子类 MultipartBody 的内部类 Part,对应 @Part;eg:

// 1.获得File对象
File file = new File(Environment.getExternalStorageDirectory(), "icon.png");
// 2.构造RequestBody对象
RequestBody qbImgFile = RequestBody.create(MediaType.parse("image/png"), file);
// 3.添上 key,key为 String 类型,代表上传的键值对的 key 
// (与服务器接受的 key 对应),value 是我们构造的 MultipartBody.Part对象
MultipartBody.Part imgFile = MultipartBody.Part
                               .createFormData("imgFile", "icon.png", qbImgFile);

这里就有个疑惑了?不是一个 @Part 对应一个 RequestBody 吗?那到了第二步就应该可以了才对,那是因为 retrofit2 并没有对文件做特殊处理,具体分析可以看鸿洋大神的Retrofit2 完全解析 探索与okhttp之间的关系里面的4.3.1点;

@PartMap

/**
 * 简书上传图片 API
 * @param params part集合
 * @return Call
 */
@Multipart
@POST("p/unload")
Call<ArticleBean> upload(@PartMap Map<String, RequestBody> params);

注意:这里可以看到,value 是 RequestBody,那么文件又怎么办呢?不是 MultipartBody.Part,如果看了上面说明为什么文件不用 RequestBody 就明白了,构造好 key 就没问题了:

···
// 创建Map<String, RequestBody>对象
Map<String, RequestBody> map = new HashMap();
// 1.获得File对象
File file = new File(Environment.getExternalStorageDirectory(), "icon.png");
// 2.构造RequestBody对象
RequestBody qbImgFile = RequestBody.create(MediaType.parse("image/png"), file);
// 构造上传文件对应的 key
String key = "imgFile" + ""; filename="" + file.getName();
map.put(key, qbImgFile);
···

第一个 " 前面拼接的是服务器的 key,第二个 " 后面拼接的是上传的文件的文件名。

当然,多文件上传也可以不用 @PathMap 的方式,使用 @Part 有多少个文件要上传,就构建多少个 MultipartBody 去对应多少个 @Path,但这样上传文件的请求方法里面参数就不确定了,如果每次固定上传三个、五个,我觉得直接用 @Path ,而不是去拼接 key ,代码看起来好很多。

获得接口对象,调用请求方法发出请求

上面搞定了获得 Retrofit 实例,创建了请求接口,并说了 Get 请求和 Post 请求的各种情况,那么,接下来就是到怎么用它们去发起请求了。

TestApiService test = retrofit.create(TestApiService.class);

就这么简单!接下来只要设置好请求参数,调用 TestApiService 里面的各个请求方法就可以发出请求了:

// 使用了@Path 的 Get 请求,获得Call对象
String articleId = "08ad8934ad2e";
Call<ArticleBean> call = test.article(articleId);

// Call调用异步请求
call.enqueue(new Callback<ArticleBean>() {
    @Override
    public void onResponse(Call<ArticleBean> call, Response<ArticleBean> response) {
        // 请求成功
        String result = response.body().string();
        System.out.println("异步请求结果: " + result);
    }
    @Override
    public void onFailure(Call<ArticleBean> call, Throwable t) {
        // 请求失败
        String error = t.getMessage();
        System.out.println("请求出错: " + error);
    }
});

// Call 调用同步请求
Response<ArticleBean> response = call.excute();
if(response.isSuccessful()) {
    System.out.println("同步请求成功");
} else {
    System.out.println("同步请求失败");
}

上面那么多个例子,请求的方式都是一样的,构造好请求调用方法需要的请求参数,通过请求方法获得 Call 对象,然后 Call 对象调用异步或者同步的请求方法获得响应,然后处理响应就好。

自定义Converter

这一块我还是建议看一下鸿洋大神的Retrofit2 完全解析 探索与okhttp之间的关系里面的第4.4点。

Header

这里要特别说一下在 Retrofit 里面添加 Header,使用注解 @Header(动态添加)、@Headers(静态添加)、自定义拦截器定义 Header 并在 okHttpClient 里面添加。

@Header

/**
 * 请求简书文章的 API
 * @param articleId 文章 ID
 * @param authoId 验证 ID
 * @return Call
 */
//动态设置Header值
@GET("p/{articleId}")
Call<ArticleBean> article(@Path("articleId") String articleId, @Header("authoId") String authoId);

@Headers

/**
 * 请求简书文章的 API
 * @param articleId 文章 ID
 * @return Call
 */
//静态设置Header值,这里authorizationId就是上面方法里传进来变量的值
@Headers("authoId: authorizationId")
@GET("p/{articleId}")
Call<ArticleBean> article(@Path("articleId") String articleId);

// 设置多个Header值
@Headers({
    "Accept: application/vnd.github.v3.full+json", 
    "User-Agent: TestApp"
})

**自定义拦截器定义 Header **

public class RequestInterceptor implements Interceptor { 
    @Override 
    public Response intercept(Chain chain) throws IOException { 
        Request original = chain.request(); 
        Request request = original.newBuilder() 
                   .header("User-Agent", "TestApp") 
                   .header("Accept", "application/vnd.github.v3.full+json")
                   .method(original.method(), original.body()) 
                   .build(); 
        return chain.proceed(request); 
    }
}

在构造 okHttpClient 对象的时候添加进去就好。

在缓存中使用

// 设置 单个请求的 缓存时间
@Headers("Cache-Control: max-age=640000")
@GET("p/{articleId}")
Call<ArticleBean> article(@Path("articleId") String articleId);

其实缓存策略主要在 okHttpClient 里面就可以设置了,具体的请看我这一篇博客:使用okHttp 里面的相关内容,但是现在 Retrofit 里面有 @Headers 设置单个请求缓存,就可以将缓存策略进一步优化(起码拦截器实现缓存的缺点就不存在了),这里参考了这篇文章内关于缓存的部分

// 离线读取本地缓存,在线获取最新数据(读取单个请求的请求头,亦可统一设置)
private Interceptor cacheInterceptor() {
        return new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();

                if (!AppUtil.isNetworkReachable(sContext)) {
                    request = request.newBuilder()
                            //强制使用缓存
                            .cacheControl(CacheControl.FORCE_CACHE)
                            .build();
                }

                Response response = chain.proceed(request);

                if (AppUtil.isNetworkReachable(sContext)) {
                    //有网的时候读接口上的@Headers里的配置,你可以在这里进行统一的设置
                    String cacheControl = request.cacheControl().toString();
                    Logger.i("has network ,cacheControl=" + cacheControl);
                    return response.newBuilder()
                            .header("Cache-Control", cacheControl)
                            .removeHeader("Pragma")
                            .build();
                } else {
                    int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
                    Logger.i("network error ,maxStale="+maxStale);
                    return response.newBuilder()
                            .header("Cache-Control", "public, only-if-cached, max-stale="+maxStale)
                            .removeHeader("Pragma")
                            .build();
                }

            }
        };
    }

同样地,将这个拦截器在 okHttpClient 创建的时候添加上就OK了。

拦截器查看日志

这里也请看我这一篇博客:使用okHttp 里面的相关内容,同样是自定义好拦截器之后添加到 okHttpClient中。

Retrofit 的基本使用就记录到这里,当然还有一个 CallAdapterFactory 的内容,这个我打算学习 RxJava 之后,放进去讲。

推荐阅读更多精彩内容