Retrofit使用指南

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了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344

推荐阅读更多精彩内容