zoukankan      html  css  js  c++  java
  • Retrofit 2使用要点梳理:小白进阶回忆录

    本博客为作者原创,如需转载请注明原博客出处: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方法请求网络数据 --> 在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
  • 相关阅读:
    40 修改了机器名,portal重装后,还需要做什么?
    39 路径分析小练习
    38 空间查询小练习
    面向对象一些概念简述
    Js函数基本介绍
    JS中的变量和数据类型
    js基础语法
    Css样式优先级
    对响应式布局的理解和认知
    关于Flex布局
  • 原文地址:https://www.cnblogs.com/wondertwo/p/5838528.html
Copyright © 2011-2022 走看看