Retrofit使用指南

作者: winter1991 | 来源:发表于2016-05-24 16:37 被阅读9282次
    xml.application {
    config.countNotTranslucentSingleTop.times {
                  activity(
                          "${name}": "${applicationID}.${infix}N1STPNTS${it}",
                          "${cfg}": "${cfgV}",
                          "${exp}": "${expV}",
                          "${ori}": "${oriV}",
                          "${theme}": "${themeNTS}",
                          "${launchMode}": "singleTop")
              }
    }
    
    Retrofit.jpg

    Retrofit简介

    Retrofit是大名鼎鼎的 Square 公司开源的适用于AndroidJava的网络请求库,官方的介绍非常简短

    A type-safe HTTP client for Android and Java
    

    Retrofit使用注解,能够极大的简化网络请求,在2.0版本默认使用Square自家的OkHttp作为底层Http Client,关于如何使用OkHttp配合Retrofit本文后面也会讲到。首先在build.gradle中加入

      compile 'com.squareup.retrofit2:retrofit:2.0.2'
    

    定义一个网络请求:

    public interface ZhiHuApi {
    @GET("users")
    Call<List<Repo>> listRepos(@Path("user") String user);
    }
    

    Retrofit将网络请求转变成了Java interface的形式,interface要获得实例调用listRepos(String user),需要借助Retrofit.java这个类,通过Retrofit.Builder来配置Retrofit,再通过retrofit.create(final Class<T> service)获取接口的实例

    class Factory {
    
      public static ZhiHuApi create() {
        Retrofit retrofit = new Retrofit.Builder()
                        .baseUrl("http://news-at.zhihu.com/")
                        .build();
                return retrofit.create(ZhiHuApi.class);
            }
    }
    

    @GET注解表示请求方式为GET,其中的字符串表示相对URL,而scheme host port被认为是BaseUrl设置到Retrofit中,注意BaseUrl需要以/结尾,这样每个接口定义的相对URL就不需要以/开始。如果在接口中定义的URL为全路径,将用这个全路径作为请求URLBaseUrl将不起作用。@Path标识get请求的请求参数,上面的listRepos可以认为是在请求http://news-at.zhihu.com/users?user=user
    @POST注解表示请求方式为POST,通常和@FormUrlEncoded注解一起使用,在使用@FormUrlEncoded时可以使用@Field标识表单字段

    @FormUrlEncoded
    @POST("user/login.do")
    Call<User> login(@Field("username") String userName, @Field("password") String password);
    

    或者使用@FieldMap提交整个map

    @FormUrlEncoded
    @POST("user/login.do")
    Call<User> login(@FieldMap Map<String, String> formMap);
    

    当然你也可以把整个表单封装为一个实体,使用@Body一次提交

    @FormUrlEncoded
    @POST("user/login.do")
    Call<User> login(@Body User user);
    

    Multipart请求时使用@Multipart注解,用@Part标识每个RequestBody

    @Multipart
    @PUT("user/photo")
    Call<User> updateUser(@Part("photo") RequestBody photo, @Part("description") RequestBody description);
    

    定义好请求之后就可以调用call.enqueue()来执行请求,需要传入Callback<T>,其中T的类型编译器会根据Call<T>中的类型来判断,Retrofit和其他网络请求库一样对于Android 平台做了线程切换,请求在后台执行,Callback<T>会回到main (UI) thread,如果是Java程序Callback<T>会继续回到调用它的线程。

    ZhiHuApi zhiHuApi = BaseNetwork.Factory.create(ZhiHuApi.class);
            Call<User> call = zhiHuApi.login("username", "pwd");
            call.enqueue(new Callback<User>() {
                @Override
                public void onResponse(Call<User> call, Response<User> response) {
    
                }
    
                @Override
                public void onFailure(Call<User> call, Throwable t) {
    
                }
            });
    

    为请求添加请求头时使用@Headers,这里就不做举例,因为app中通常是每个请求都需要携带请求头,不建议在Retrofit定义请求时传入,而是使用OkHttp来实现统一请求头。

    Converter

    Retrofit在默认情况下只能将Http的响应体反序列化到OkHttpResponseBody中,加入Converter可以将返回的数据直接格式化成你需要的样子,现有6个Converter可以直接使用:

    • 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

    之后在代码里加入(此处以GsonConverterFactory为例)

      Retrofit retrofit =new Retrofit.Builder()
                        .baseUrl(Constants.BASE_HTTP_URL)
                        .addConverterFactory(GsonConverterFactory.create())
                        .build();
    

    返回的数据会使用Gson解析为对应传入的实体类,你也可以自定义Converter来实现更复杂的需求,只需要extends Converter.Factory然后重写

      @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
      }
    

    Retrofit终归只是应用层的api,真正的执行器是OkHttp,较为复杂的需求都需要从执行层入手,可以做到Retrofit对外不变的多种自定义统一封装。

    OkHttp配合Retrofit使用

    前文已经提到在Retrofit 2.0中已经默认使用OkHttp作为网络请求执行器,关于OkHttp的优点简单提一下:(原文链接)

    • 1.支持HTTP2/SPDY黑科技
    • 2.socket自动选择最好路线,并支持自动重连
    • 3.拥有自动维护的socket连接池,减少握手次数
    • 4.拥有队列线程池,轻松写并发
    • 5.拥有Interceptors轻松处理请求与响应(比如透明GZIP压缩,LOGGING)
    • 6.基于Headers的缓存策略

    想要使用OkHttpRetrofit提供更高的定制性,给Retrofit设置自定义的OkHttpClient就可以了

    Retrofit retrofit = new Retrofit.Builder()
                      .baseUrl(Constants.BASE_HTTP_URL)
                      .client(client)
                      .build();
    

    之后就是构建一个OkHttpClient

    OkHttpClient client = new OkHttpClient.Builder()
               // 向Request Header添加一些业务相关数据,如APP版本,token
               .addInterceptor(new HeadInterceptor())
               //日志Interceptor,可以打印日志
               .addInterceptor(logging)
               // 连接超时时间设置
               .connectTimeout(10, TimeUnit.SECONDS)
               // 读取超时时间设置
               .readTimeout(10, TimeUnit.SECONDS)
               // 失败重试
               .retryOnConnectionFailure(true)
               // 支持Https需要加入SSLSocketFactory
               .sslSocketFactory(sslSocketFactory)
               // 信任的主机名,返回true表示信任,可以根据主机名和SSLSession判断主机是否信任
               .hostnameVerifier(new HostnameVerifier() {
                   @Override
                   public boolean verify(String hostname, SSLSession session) {
                       return true;
                   }
               })
               // 使用host name作为cookie保存的key
               .cookieJar(new CookieJar() {
                   private final HashMap<HttpUrl, List<Cookie>> cookieStore = new HashMap<>();
    
                   @Override
                   public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                       cookieStore.put(HttpUrl.parse(url.host()), cookies);
                   }
    
                   @Override
                   public List<Cookie> loadForRequest(HttpUrl url) {
                       List<Cookie> cookies = cookieStore.get(HttpUrl.parse(url.host()));
                       return cookies != null ? cookies : new ArrayList<Cookie>();
                   }
               })
               .build();
    

    如果设置了sslSocketFactory却没有配置对应的hostnameVerifier,那么Https请求是无法成功的。上面用到两个Interceptor分别是HeadInterceptorHttpLoggingInterceptor,分别是用来添加请求头和打印请求日志的拦截器,OkHttp支持自定义拦截器,例如下面代码自定义的HeadInterceptor为请求加入Headers

    public class HeadInterceptor implements Interceptor {
    
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request originalRequest = chain.request();
            Request compressedRequest = originalRequest.newBuilder()
                    .headers(Headers.of(getHeaders()))
                    .build();
            return chain.proceed(compressedRequest);
        }
    }
    

    有时服务器会对POST提交的表单做参数校验,一种方式是在请求头里加入特定方式加密过的表单参数的Map,那么就需要先获取到请求的Map,通过FormBody可以实现

    // if the server needs to verify post params, use this to get post params;
      RequestBody oidBody = originalRequest.body();
      Map<String, String> params = new HashMap<>();
      if (oidBody instanceof FormBody) {
        FormBody formBody = (FormBody) oidBody;
        for (int i = 0; i < formBody.size(); i++) {
            params.put(formBody.encodedName(i), formBody.encodedValue(i));
        }
      }
    

    HttpLoggingInterceptorSquare 提供的请求信息日志打印工具类,如果需要可以在build.gradle中加入

      compile 'com.squareup.okhttp3:logging-interceptor:3.2.0'
    

    可以根据不同情况配置日志输出的Level

    • NONE 不输出日志
    • BASIC 只输出请求方式响应码等基本信息
    • HEADERS 只输出请求和响应的头部信息
    • BODY 输出请求和响应的头部和请求体信息

    另外如果遇到两个接口有相互依赖关系,必须请求完第一个接口拿到数据后才知道第二个请求的URL,通常我们会定义两个Retrofit,因为RetrofitBaseUrl是统一配置的,不过现在可以通过实现动态BaseUrl来避免这个问题,先看DynamicBaseUrlInterceptor的代码

    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);
        }
    }
    

    BaseUrl改变时只需要setHost()就可以让下次请求的Baseurl改变

    Retrofit 与 RxJava 结合使用

    本节需要对RxJava基本用法有了解,如果不了解可以忽略或者先去熟悉一下RxJavawiki,介绍的目的是因为两者结合使用确实很方便,关于RxJava之后会单独写。

    RxJavaRx(全称Reactive Extensions)家族中的一员,是最近很火的响应式编程库,官方对于它的解释很简单

    RxJava – Reactive Extensions for the JVM – a library for composing asynchronous and event-based programs using observable sequences for the Java VM.
    

    一个异步的基于事件的观察者序列,可以理解为扩展的观察者模式,在 Android 中使用RxJava需要引入两个compileRxAndroid是专为 Android 平台打造来提供主线程切换等便利的工具项目。

       compile 'io.reactivex:rxandroid:1.2.0'
       // Because RxAndroid releases are few and far between, it is recommended you also
       // explicitly depend on RxJava's latest version for bug fixes and new features.
       compile 'io.reactivex:rxjava:1.1.5'
    

    Retrofit提供了CallAdapterFactory,它是一个知道如何将call实例转换成其他类型的工厂类,目前支持的有:

    • RxJava
    • Guava
    • Java8

    这些和Retrofit本身都是分离的,需要单独引入compile例如

    compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0'
    

    在代码中配置CallAdapterFactory

    Retrofit retrofit =new Retrofit.Builder()
                       .baseUrl(Constants.BASE_HTTP_URL)
                       .addConverterFactory(GsonConverterFactory.create())
                       .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                       .build();
    

    之后就可以把请求的返回改为Observable<T>

    @GET("users")
    Observable<List<Repo>> listRepos(String user);
    

    请求时只需要

    BaseNetwork.Factory.create(Foo.class)
                    .listRepos("user")
                    .observeOn(AndroidSchedulers.mainThread())//观察者所在的线程
                    .subscribeOn(Schedulers.io())//请求执行的线程
                    //如果正常执行会顺序调用onNext,onCompleted,如果出错则会调用onError
                    .subscribe(new Observer<List<Repo>>() {
                        @Override
                        public void onCompleted() {
    
                        }
    
                        @Override
                        public void onError(Throwable e) {
    
                        }
    
                        @Override
                        public void onNext(List<Repo> list) {
    
                        }
                    });
    

    如果需要配合服务器返回固定的格式,通过状态码判断业务是否出错,如果出错获取错误信息,类似如下格式

    {
      "state":0,//状态码,0为业务正常
      "msg":"",//如果业务出错,携带错误信息
      "data":{}//包含实际业务实体
    }
    

    需要定义统一的响应实体,根据T传入的类型来获取业务实体真实的类型

    public class BaseResult<T> {
        private int state;
        private String msg;
        private T data;
    
        public int getState() {
            return state;
        }
    
        public void setState(int state) {
            this.state = state;
        }
    
        public String getMsg() {
            return msg;
        }
    
        public void setMsg(String msg) {
            this.msg = msg;
        }
    
        public T getData() {
            return data;
        }
    
        public void setData(T data) {
            this.data = data;
        }
    }
    

    请求中的泛型类型需要是BaseResult<T>

    @GET("users")
    Observable<BaseResult<List<Repo>>> listRepos(@Path("user") String user);
    

    调用时也会有改变,需要经过一次拆解统一返回,处理错误的过程

    BaseNetwork.Factory.create(Foo.class)
                    .listRepos("user")
                    .flatMap(new NetworkResultFunc1<List<Repo>>())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribeOn(Schedulers.io())
                    .subscribe(observer);
    

    flatMap需要传入Func1<T, R>,Func1<T, R>继承了Function,只有一个方法,将泛型参数列表的第一个转换为第二个返回,它可以将Observable做一个展开,并返回一个新的Observable

    public interface Func1<T, R> extends Function {
        R call(T t);
    }
    

    NetworkResultFunc1<List<Repo>>实现了Func1<T, R>,代码如下

    public class NetworkResultFunc1<T> implements Func1<BaseResult<T>, Observable<T>> {
    
        @Override
        public Observable<T> call(final BaseResult<T> tBaseResult) {
            return Observable.create(new Observable.OnSubscribe<T>() {
                @Override
                public void call(Subscriber<? super T> subscriber) {
                    int state = tBaseResult.getState();
                    String msg = tBaseResult.getMsg();
                    switch (state) {
                        case 0://if success, return data to client
                            subscriber.onNext(tBaseResult.getData());
                            break;
                        case 1000://if this means error
                            subscriber.onError(new ApiException(state, msg));
                            break;
                    }
                    subscriber.onCompleted();//no error, will execute onCompleted()
                }
            });
        }
    }
    

    如果state为0,则调用subscriber.onNext()向调用者返回数据,当state不等于0时意味着业务出错了,向subscriber.onError()中抛了一个ApiException,这样在Observer处会回调onError()终止整个事件流,调用者也能获得业务错误的相关信息。ApiException代码如下,就是一个自定义的RuntimeException

    public class ApiException extends RuntimeException {
        private int state;
        private String msg;
        public ApiException(int state, String msg) {
            this.state = state;
            this.msg = msg;
        }
    
        public int getState() {
            return state;
        }
    
        public String getMsg() {
            return msg;
        }
    }
    

    对于Retrofit的介绍就先到这里,相信看到这里,你已经能够在项目中优雅的使用Retrofit了。

    相关文章

      网友评论

      • 全球顶级卧底:原谅我,我也看不懂,重来没有自己封装过联网的框架 想自己试一试结果看不懂 基本上能看懂 自己封装就是问题了
      • 谦谦行者:请问在设置了readTimeOut和connectTimeOut后,假如请求url超时了,程序会执行Retrofit的哪一个回调方法呢?
      • 麦地数蚂蚁:想问问 如果使用delete 或者patch请求的时候 是否和post请求一样 需要添加@FormUrlEncoded 如果一个项目中的请求需要公共参数的话 在哪里封装进去比较好点
      • 年伟:我也有点凌乱,多读几遍吧。还有重写resposebodyconvert和requestbodyconvert怎么重写
      • b6c7a3ded5ff:真心不错,看来好几遍,最近准备再新项目用这些技术,有空了能写一篇简单的封装吗?
      • 健康早餐:有一个地方写错了,
        public interface ZhiHuApi {
        @get("users")
        Call<List<Repo>> listRepos(@Path("users") String user);
        }
        其中@GET("{users}"),因为你参数传过来路径了,所以要加上{},然后参数名字打错了
        winter1991:@帅气的昵称啊123 谢谢指正
      • Serven_:楼主,有个问题想问下,我们服务端有进行加密处理,需要对请求头,请求体进行加密,实在不知道在哪边进行处理,求指点求demo
        winter1991:@Serven_ 这里有个当时写的demo,位置在common/network/interceptor/中,看下HeadInterceptor ,加入一句originalRequest.url() 就能获得url了,地址https://github.com/QingWinter/Douban
        Serven_:@winter1991 重新看了一遍楼主的文章,有了一些新的启示,但还是不是很明白怎么去添加,我们的请求头是需要根据每一个接口的Url来动态生成的,同样请求体也是,跪求楼主写个Demo,邮箱295526916@qq.com,万分感谢
        winter1991:@Serven_ 在OkHttpClient里加入拦截器,上文提到有请求头拦截器,可以获得请求参数和构建请求头
      • 8b695f82fa3e:可惜啊rxjava还是理解不了。。现在单独用retrofit…
        winter1991: @ShineYoung RxJava后面我会尝试写几篇,到时再看看。
      • 0be91fe8454b:写得挺好的,想问问put方式这些是否可以使用
        0be91fe8454b:@winter1991,好的,谢谢
        winter1991:@YoungKen 可以的,Multipart就是用的PUT :blush:
      • IIIIII美美IIIIII:我优雅的全程看不懂:stuck_out_tongue_closed_eyes:
        凤鸣游子:@IIIIII美美IIIIII 我也不懂啊.
        IIIIII美美IIIIII:@winter1991 不是,问题在我,我不懂这些语言:blush:
        winter1991:@IIIIII美美IIIIII 写的很难懂吗

      本文标题:Retrofit使用指南

      本文链接:https://www.haomeiwen.com/subject/qciarttx.html