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 之后,放进去讲。