小白进阶回忆录:Retrofit2要点梳理

本博客为作者原创,如需转载请注明原博客出处:WONDER'TWO

0X00 写在前面


相信做过Android网络请求的同学都绕不开Volley,Retrofit,OkHttp这几座大山,至于他们的前世姻缘以及孰优孰劣,不在本博客的讨论范围。如题,这篇博客主要介绍一个小白(其实就是我自己)的Retrofit2进阶之路,会结合一个开发实例介绍5节内容:

  • Retrofit2 HTTP请求方法注解的字段说明
  • Call<T>响应结果的处理问题
  • Retrofit2+RxJava实现开发效率最大化
  • 自定义OkHttp Interceptor实现日志输出,保存和添加Cookie
  • 自定义ResponseConverter,自定义HTTP请求注解

先来回顾一下Retrofit2在项目中的完整使用流程:创建Bean类 --> 创建接口形式的http请求方法 --> 通过Retrofit.builder()创建接口对象并调用http方法请求网络数据 --> 在RxJavaObservable(被观察者)中异步处理请求结果!

那么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 请求:

  1. addCallAdapterFactory(rxJavaCallAdapterFactory) 方法指定使用RxJava 作为CallAdapter ,需要传入一个RxJavaCallAdapterFactory对象:CallAdapter.Factory rxJavaCallAdapterFactory = RxJavaCallAdapterFactory.create()
  2. addConverterFactory(gsonConverterFactory) 方法指定 Gson 作为解析Json数据的ConverterConverter.Factory gsonConverterFactory = GsonConverterFactory.create()
  3. 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 方法请求网络数据 --> 在RxJavaObservable 中异步处理请求结果!

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 来实现的,重写了responseBodyConverterrequestBodyConverter 这两个方法,代码只有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请求接口GuokrServicegetGuokrs()方法返回值的泛型类型,如果返回值类型是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%。

参考文献

  1. Retrofit官方文档
  2. Getting started with Retrofit 2
  3. Consuming APIs with Retrofit
  4. Retrofit 2.0: The biggest update yet on the best HTTP Client Library for Android
  5. Retrofit使用指南
  6. Square全家桶正传——Retrofit使用及配合RxJava实现最大效率开发
  7. Retrofit使用OkHttp保存和添加cookie
  8. 如何使用Retrofit请求非Restful API
  9. Retrofit请求参数注解字段说明
  10. OkHttp-->Interceptors
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容