本博客为作者原创,如需转载请注明原博客出处:http://www.cnblogs.com/wondertwo/p/5838528.html(博客园)/http://www.jianshu.com/p/dd2804030b89(简书)
0X00 写在前面
相信做过Android网络请求的同学都绕不开Volley,Retrofit,OkHttp这几座大山,至于他们的前世姻缘以及孰优孰劣,不在本博客的讨论范围。如题,这篇博客主要介绍一个小白(其实就是我自己)的Retrofit2进阶之路,会结合一个开发实例介绍5节内容:
- Retrofit2 HTTP请求方法注解的字段说明
Call<T>
响应结果的处理问题- Retrofit2+RxJava实现开发效率最大化
- 自定义OkHttp Interceptor实现日志输出,保存和添加Cookie
- 自定义ResponseConverter,自定义HTTP请求注解
先来回顾一下Retrofit2
在项目中的完整使用流程:创建Bean类 --> 创建接口形式的http
请求方法 --> 通过Retrofit.builder()
创建接口对象并调用http
方法请求网络数据 --> 在RxJava
的Observable
(被观察者)中异步处理请求结果!
那么Retrofit2 Http 请求方法注解有那么多字段,都代表什么含义呢?添加请求头或者大文件上传的请求方法该怎么写呢?这将在第二节介绍。另外,Retrofit2基本用法的网络响应结果是一个Call<T>
,那么怎样在Android中解析Call<T>
呢?将在第二节介绍。第三节根据Retrofit2使用流程介绍了一个实践项目是怎样使用Retrofit2+RxJava
做网络请求。第四节和四五节是Retrofit实现一些复杂需求的必杀技,介绍了自定义OkHttp Interceptor实现日志输出,保存和添加Cookie;自定义ResponseConverter,自定义HTTP请求注解等内容。
0X01 Retrofit2 HTTP请求方法注解的字段说明
从Retrofit2的官方文档来看,Retrofit2 进行网络请求的URL分为两部分:BaseURL和relativeURL。BaseURL需要以/
结尾, 一般不需要变化直接定义即可,当然在特殊的情况下,比如后一次网络访问URL需要从前一次访问结果中获取相关参数,那么就需要动态的操作URL,这种用法会在第五节进行介绍;relativeURL与每次请求的参数相关,所以每个request 方法都需要 http annotation 来提供请求的relativeURL,Retrofit2内置的注解有五个:GET, POST, PUT, DELETE, and HEAD.
这些注解在使用时涉及到哪些相关的字段呢?我从参考文献的博客中引用了一张图:
可以看到,有URL请求参数,Query
参数这些简单网络请求参数;同时还支持用@Header
添加请求头;POST
请求中常用@FormUrlEncoded
提交表单,并用@Field
定义表单域;@MultiPart
文件上传并用@Part
定义请求体。来看一个具体的例子(摘自Retrofit2官方文档):
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
Retrofit2把网络请求定义成接口的形式,如上是一个GET请求,@Path
表示一个占位符,@Path
中的变量必须与@GET
变量中{
和 }
中间的部分一致。下面是一个POST请求,@FormUrlEncoded
用于提交一个表单,@Field
定义了表单的name和value。更多详细的用法详见Retrofit2官方文档API Declaration ,另外Retrofit请求参数注解字段说明 这篇博客介绍的比较详细可作参考:
public interface GitHubService {
@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);
}
0X02 Call<T>
响应结果的处理
细心的你有木有发现,发现官方文档中给出的请求方法示例,返回结果都是 Call<List<User>>
这种形式?没错!这就是Retrofit2最原始的网络请求用法,官方文档上介绍的很简洁,可以在 call<T>
响应对象上做异步或者同步的操作,每个 call<T>
对象只能用一次,要想多次使用可以调用 clone()
方法来克隆出多个 call
对象以供更多操作使用。因为Retrofit2 是一个类型安全的Java和Android网络请求库,所以以上的操作对 Java 网络请求也是适用的。
针对JVM而言,网络请求和结果处理会放在同一个线程中执行,那么在Android中,我们怎样处理请求结果对象 call
呢?官方文档也给出了答案,我们都知道Android中网络请求这类耗时操作都是放在工作线程(即worker thread)来执行的,然后在主线程(也即 UI thread)处理网络请求结果,自然Retrofit2也不例外,由于Retrofit2抛弃了饱受诟病的Apache HttpClient底层只依赖OkHttp3.0,网络访问层的操作都会交由OkHttp来完成,而OkHttp不仅拥有自动维护的socket连接池,减少握手次数,而且还拥有队列线程池,可以轻松写并发,同时还支持socket自动选择最好路线,并支持自动重连,OkHttp的优点远不止于此,所以Retrofit2选择用OkHttp作为网络请求执行器是一个再明智不过的决定。
如果你想异步的执行网络请求,最简单的就是在Activity或者Fragment中View控件的监听器中进行网络访问,并通过call.enqueue()
处理请求结果,并更新UI,下面是一个小demo:
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
GitHubService gitHubService = GitHubService.retrofit.create(GitHubService.class);
final Call<List<Contributor>> call =
gitHubService.repoContributors("square", "retrofit");
call.enqueue(new Callback<List<Contributor>>() {
@Override
public void onResponse(Call<List<Contributor>> call, Response<List<Contributor>> response) {
final TextView textView = (TextView) findViewById(R.id.textView);
textView.setText(response.body().toString());
}
@Override
public void onFailure(Call<List<Contributor>> call, Throwable t) {
final TextView textView = (TextView) findViewById(R.id.textView);
textView.setText("Something went wrong: " + t.getMessage());
}
});
}
});
如果你需要在工作线程中执行网络请求,而不是在一个Activity或者一个Fragment中去执行,那么也就意味着,你可以在同一个线程中同步的去执行网络请求,使用call.execute()
方法来处理请求结果即可,代码如下:
try {
Response<User> response = call.execute();
} catch (IOException e ){
// handle error
}
0X03 Retrofit2+RxJava实现开发效率最大化
Retorfit是支持RxJava,Guava,Java8 等等一系列扩展的,关于RxJava这个网红我就不做介绍了,RactiveX项目对 JVM 的扩展,你可以把它当做一个超级强大的异步事件处理库,可他的NB之处远不止于此,至少做Android的都应该听过他的鼎鼎大名,不熟悉的可以去看看RxJava Wiki!!而这里Retrofit2+RxJava
组合就可以实现开发效率的大幅提升,至于怎样提升的?对比一下你以前写的网络请求的代码量就知道了!结合一个实践项目的源码来分析,这里是请求果壳网最新的100条文章数据,返回结果为Json,首先build.gradle
添加依赖:
compile 'io.reactivex:rxandroid:1.1.0' // RxAndroid
compile 'io.reactivex:rxjava:1.1.0' // 推荐同时添加RxJava
compile 'com.squareup.retrofit2:retrofit:2.1.0' // Retrofit网络处理
compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0' // Retrofit的rx解析库
compile 'com.squareup.retrofit2:converter-gson:2.1.0' // Retrofit的gson库
compile 'com.squareup.okhttp3:okhttp:3.2.0' // OkHttp3
第一步,定义服务器Json数据对应的POJO类,这里我们可以偷一下懒可以直接通过jsonschema2pojo 这个网站自动生成POJO类,就不用我们手动去写了,然后copy到项目目录的bean包下。接着便是定义HTTP请求方法了,以接口的形式定义,如下:
// 服务器数据对应的实体类
public class Guokr {
// 定义序列化后的名字
public @SerializedName("ok") Boolean response_ok;
// 定义序列化后的名字
public @SerializedName("result") List<GuokrResult> response_results;
public static class GuokrResult {
public int id;
public String title;
public String headline_img_tb; // 用于文章列表页小图
public String headline_img; // 用于文章内容页大图
public String link;
public String author;
public String summary;
}
}
// HTTP请求方法
public interface GuokrService {
@GET("handpick/article.json")
Observable<Guokr> getGuokrs(@Query("retrieve_type") String type,
@Query("category") String category,
@Query("limit") int limit,
@Query("ad") int ad);
}
其中 Observable<Guokr>
是RxJava中的被观察者,对应请求结果Call<T>
。只是因为Retrofit提供了非常强大的CallAdapterFactory
完美兼容了RxJava
这个超级大网红,才导致我们平常看到的写法是这样的。第二步, 需要通过Retrofit.builder()
创建 GuokrService
接口对象,通过接口对象执行 getGuokrs
方法进行网络访问,代码如下:
// 封装 GuokrService 请求
public static GuokrService getGuokrService() {
if (guokrService == null) {
Retrofit retrofit = new Retrofit.Builder()
.client(mClient)
.baseUrl("http://apis.guokr.com/")
.addCallAdapterFactory(rxJavaCallAdapterFactory)
.addConverterFactory(gsonConverterFactory)
.build();
guokrService = retrofit.create(GuokrService.class);
}
return guokrService;
}
// 默认加载最新的100条数据
subscription = RetrofitClient.getGuokrService().getGuokrs("by_since", "all", 100, 1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Guokr>() {
@Override
public void onCompleted() {
Log.e(TAG, "--------completed-------");
}
@Override
public void onError(Throwable e) {
Log.e(TAG, "--------error-------");
Log.e(TAG, e.getMessage());
}
@Override
public void onNext(Guokr guokr) {
if (guokr.response_ok) {
List<Guokr.GuokrResult> guokrResults = guokr.response_results;
List<GuokrItem> guokrItems = new ArrayList<>(guokrResults.size());
for (Guokr.GuokrResult result : guokrResults) {
GuokrItem item = new GuokrItem();
item.headline_img_tb = result.headline_img_tb;
item.title = result.title;
item.id = result.id;
item.headline_img = result.headline_img;
item.summary = result.summary;
guokrItems.add(item);
}
mAdapter.addAll(guokrItems);
mAdapter.notifyDataSetChanged();
});
注意到封装 GuokrService 请求:
addCallAdapterFactory(rxJavaCallAdapterFactory)
方法指定使用RxJava
作为CallAdapter
,需要传入一个RxJavaCallAdapterFactory
对象:CallAdapter.Factory rxJavaCallAdapterFactory = RxJavaCallAdapterFactory.create()
;addConverterFactory(gsonConverterFactory)
方法指定Gson
作为解析Json数据的Converter
:Converter.Factory gsonConverterFactory = GsonConverterFactory.create()
;client(mClient)
方法指定网络执行器为OkHttp
如下创建一个默认的OkHttp对象传入即可:OkHttpClient mClient = new OkHttpClient()
;
而加载网络数据这个链式调用就是RxJava最大的特色,用在这里逻辑就是,被观察者Observable<Guokr>
订阅观察者Observer<Guokr>
,当服务器一有response,观察者就会立即处理response result。因为RxJava
最大的亮点就是异步,可以很方便的切换当前任务所在的线程,并能对事件流进行各种Map变换,比如压合、转换、缓存等操作。这里是最基本的用法,被观察者直接把事件流订阅到观察者,中间没有做转换处理。
到此网络访问就完成了,是不是很简洁?简洁就对了,那是因为太多东西Retrofit2和RxJava甚至是OkHttp都帮我们做好了!再回顾一下整个网络访问流程:创建Bean类 --> 创建接口形式的http
请求方法 --> 通过Retrofit.builder()
创建接口对象并调用http
方法请求网络数据 --> 在RxJava
的Observable
中异步处理请求结果!
0X04 自定义OkHttp Interceptor实现日志输出,保存和添加Cookie
在Retrofit2做网络请求的第二步,我们需要通过Retrofit.builder()
方法来创建Retrofit
对象,其中client(mClient)
这个方法指定一个OkHttpClient
客户端作为请求的执行器,需要传入一个OkHttpClient
对象作为参数,那么在这里,我们就可以进行一些OkHttp相关的操作,比如自定义Interceptor
,通过自定义Interceptor
可以实现网络请求日志的分级输出,可以实现保存和添加Cookie这些功能,当然,这些功能的实现都是基于OkHttp,所以要对OkHttp有一定的了解才能灵活运用。
Retrofit使用指南-->OkHttp配合Retrofit使用 这篇博客在OkHttp配合Retrofit使用这一节,关于OkHttpClient添加HttpLoggingInterceptor
进行日志输出,以及如何设置SslSocketFactory做了详细的说明,有兴趣的同学可以参考!值得注意的是,如果后一次请求的URL,需要从前一次请求结果数据中获取,这时候就需要动态的改变BaseURL,也可通过自定义Interceptor
来实现这一需求,在BaseURL改变时,只需要setHost()
就可以让下次请求的BaseURL改变,代码如下:
public class DynamicBaseUrlInterceptor implements Interceptor {
private volatile String host;
public void setHost(String host) {
this.host = host;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
if (!TextUtils.isEmpty(host)) {
HttpUrl newUrl = originalRequest.url().newBuilder()
.host(host)
.build();
originalRequest = originalRequest.newBuilder()
.url(newUrl)
.build();
}
return chain.proceed(originalRequest);
}
}
那么怎样在通过OkHttp保存和添加Cookie呢?其实实现原理和上面添加日志拦截器差不多,只是添加的Intercepter
不同而已,其实就是自定义了一个Interceptor
接口实现类,接收和保存返回结果中的Cookie,或者添加Cookie,最后,在创建OkHttp实例的时候,传入以上Interceptor
实现类的对象即可。Retrofit使用OkHttp保存和添加cookie这篇博客讲的很好,可以作为参考!
简而言之,以上这Retorfit2些高级运用都是基于定制化OkHttp来实现的,如果想玩得很溜就必须对OkHttp了解一二,推荐看这篇博客OkHttp3源码分析综述!最起码需要弄清楚OkHttpClient自定义Interceptor这一块内容,推荐看OkHttp Github Wiki --> Interceptors!
0X05 自定义ResponseConverter,自定义HTTP请求注解
默认情况下,Retrofit会把HTTP响应体反序列化到OkHttp的ResponseBody
中,加入Converter可以将返回的数据直接格式化成你需要的样子,Retrofit提供了如下6个Converter可以直接使用,使用前需要加上相应的Gradle依赖:
- 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
在前面Retrofit2+RxJava实例中,我们指定GsonConverterFactory作为解析Json数据的Converter,当面对更复杂的需求时,仍然可以通过继承Converter.Factory
来自定义Converter,只需要重写以下这两个方法即可:
@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
Retrofit retrofit) {
//your own implements
}
@Override
public Converter<?, RequestBody> requestBodyConverter(Type type,
Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
//your own implements
}
我们不妨来看看GsonConverterFactory
源码,果然GsonConverterFactory
也是继承Converter.Factory
来实现的,重写了responseBodyConverter
和 requestBodyConverter
这两个方法,代码只有70多行还是很简洁的,如下:
public final class GsonConverterFactory extends Converter.Factory {
/**
* Create an instance using a default {@link Gson} instance for conversion. Encoding to JSON and
* decoding from JSON (when no charset is specified by a header) will use UTF-8.
*/
public static GsonConverterFactory create() {
return create(new Gson());
}
/**
* Create an instance using {@code gson} for conversion. Encoding to JSON and
* decoding from JSON (when no charset is specified by a header) will use UTF-8.
*/
public static GsonConverterFactory create(Gson gson) {
return new GsonConverterFactory(gson);
}
private final Gson gson;
private GsonConverterFactory(Gson gson) {
if (gson == null) throw new NullPointerException("gson == null");
this.gson = gson;
}
@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
Retrofit retrofit) {
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
return new GsonResponseBodyConverter<>(gson, adapter);
}
@Override
public Converter<?, RequestBody> requestBodyConverter(Type type,
Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
return new GsonRequestBodyConverter<>(gson, adapter);
}
}
这里需要详细解释一下TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type))
中的TypeAdapter<?>
,TypeAdapte
是Gson提供的自定义Json解析器,Type就是HTTP请求接口GuokrService
中getGuokrs()
方法返回值的泛型类型,如果返回值类型是Call<T>
,那么这里的Type就是泛型类型 T ,如果返回值类型是Observable<List<Guokr>>
,那么Type就是List<Guokr>
;关于Gson的详细用法可以参考:你真的会用Gson吗?Gson使用指南(四)。
我们看到responseBodyConverter
方法返回的是一个GsonResponseBodyConverter
对象,跟进去看一下GsonResponseBodyConverter
源码,也很简单,源码 如下:
final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
private final Gson gson;
private final TypeAdapter<T> adapter;
GsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {
this.gson = gson;
this.adapter = adapter;
}
@Override public T convert(ResponseBody value) throws IOException {
JsonReader jsonReader = gson.newJsonReader(value.charStream());
try {
return adapter.read(jsonReader);
} finally {
value.close();
}
}
}
我们看到GsonResponseBodyConverter<T>
实现了Converter<ResponseBody, T>
,重写了convert(ResponseBody value)
方法,这就给我们提供了一个思路:自定义Converter关键一步就是要实现Converter<ResponseBody, T>
接口并且重写convert(ResponseBody value)
方法,具体重写的代码我就不贴出来了,可以参考如何使用Retrofit请求非Restful API 这篇博客自定义Converter的做法!
另外,如果需求更复杂,需要我们自定义HTTP请求方法的注解,又该怎么做呢?我们还注意到GsonConverterFactory
类的重写方法responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit)
中的Annotation[] methodAnnotations
这个参数,对的,或许你已经猜到了,这就是我们在HTTP请求接口方法中定义的注解,先看 @GET
注解的源码,如下:
/** Make a GET request. */
@Documented
@Target(METHOD)
@Retention(RUNTIME)
public @interface GET {
String value() default "";
}
那我们自定义注解的思路也就有了,模仿上面 @GET
注解写一个 @WONDERTWO
注解即可。这里我点到即止,主要是提供一种思路,具体实现仍然可以参考上面提到的 如何使用Retrofit请求非Restful API 这篇博客自定义HTTP请求注解的做法!
0X06 写在后面
有一个结论说的是在网络上,只有 1% 的用户贡献了内容,10% 的用户比较活跃,会评论和点赞,剩下的都是网络透明人,他们只是默默地在看,既不贡献内容,也不点赞。这篇文章希望能让你成为网络上贡献内容的 TOP 1%。如果暂时做不到,那就先点个赞吧,成为活跃的 10%。
参考文献
- Retrofit官方文档
- Getting started with Retrofit 2
- Consuming APIs with Retrofit
- Retrofit 2.0: The biggest update yet on the best HTTP Client Library for Android
- Retrofit使用指南
- Square全家桶正传——Retrofit使用及配合RxJava实现最大效率开发
- Retrofit使用OkHttp保存和添加cookie
- 如何使用Retrofit请求非Restful API
- Retrofit请求参数注解字段说明
- OkHttp-->Interceptors