Android Retrofit使用指南
TristaLomax
8年前
<p><img src="https://simg.open-open.com/show/a1126679cf313518dc31e0cb9b088acc.jpg" alt="Android Retrofit使用指南" width="846" height="256"></p> <p>Retrofit简介</p> <p><code>Retrofit</code>是大名鼎鼎的 <em>Square</em> 公司开源的适用于<code>Android</code>与<code>Java</code>的网络请求库,官方的介绍非常简短</p> <pre> <code class="language-java">A type-safe HTTP client for Android and Java</code></pre> <p><code>Retrofit</code>使用注解,能够极大的简化网络请求,在2.0版本默认使用<em>Square</em>自家的<code>OkHttp</code>作为底层<code>Http Client</code>,关于如何使用<code>OkHttp</code>配合<code>Retrofit</code>本文后面也会讲到。首先在<code>build.gradle</code>中加入</p> <pre> <code class="language-java"> compile 'com.squareup.retrofit2:retrofit:2.0.2'</code></pre> <p>定义一个网络请求:</p> <pre> <code class="language-java">public interface ZhiHuApi { @GET("users") Call<List<Repo>> listRepos(@Path("user") String user); }</code></pre> <p><code>Retrofit</code>将网络请求转变成了<code>Java interface</code>的形式,<code>interface</code>要获得实例调用<code>listRepos(String user)</code>,需要借助<code>Retrofit.java</code>这个类,通过<code>Retrofit.Builder</code>来配置<code>Retrofit</code>,再通过<code>retrofit.create(final Class<T> service)</code>获取接口的实例</p> <pre> <code class="language-java">class Factory { public static ZhiHuApi create() { Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://news-at.zhihu.com/") .build(); return retrofit.create(ZhiHuApi.class); } }</code></pre> <p><code>@GET</code>注解表示请求方式为<code>GET</code>,其中的字符串表示相对<code>URL</code>,而<code>scheme host port</code>被认为是<code>BaseUrl</code>设置到<code>Retrofit</code>中,注意<code>BaseUrl</code>需要以<code>/</code>结尾,这样每个接口定义的相对URL就不需要以<code>/</code>开始。如果在接口中定义的<code>URL</code>为全路径,将用这个全路径作为请求<code>URL</code>,<code>BaseUrl</code>将不起作用。<code>@Path</code>标识get请求的请求参数,上面的<code>listRepos</code>可以认为是在请求<code>http://news-at.zhihu.com/users?user=user</code><br> <code>@POST</code>注解表示请求方式为<code>POST</code>,通常和<code>@FormUrlEncoded</code>注解一起使用,在使用<code>@FormUrlEncoded</code>时可以使用<code>@Field</code>标识表单字段</p> <pre> <code class="language-java">@FormUrlEncoded @POST("user/login.do") Call<User> login(@Field("username") String userName, @Field("password") String password);</code></pre> <p>或者使用<code>@FieldMap</code>提交整个<code>map</code></p> <pre> <code class="language-java">@FormUrlEncoded @POST("user/login.do") Call<User> login(@FieldMap Map<String, String> formMap);</code></pre> <p>当然你也可以把整个表单封装为一个实体,使用<code>@Body</code>一次提交</p> <pre> <code class="language-java">@FormUrlEncoded @POST("user/login.do") Call<User> login(@Body User user);</code></pre> <p><code>Multipart</code>请求时使用<code>@Multipart</code>注解,用<code>@Part</code>标识每个<code>RequestBody</code></p> <pre> <code class="language-java">@Multipart @PUT("user/photo") Call<User> updateUser(@Part("photo") RequestBody photo, @Part("description") RequestBody description);</code></pre> <p>定义好请求之后就可以调用<code>call.enqueue()</code>来执行请求,需要传入<code>Callback<T></code>,其中<code>T</code>的类型编译器会根据<code>Call<T></code>中的类型来判断,<code>Retrofit</code>和其他网络请求库一样对于<em>Android</em>平台做了线程切换,请求在后台执行,<code>Callback<T></code>会回到<code>main (UI) thread</code>,如果是<code>Java</code>程序<code>Callback<T></code>会继续回到调用它的线程。</p> <pre> <code class="language-java">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) { } });</code></pre> <p>为请求添加请求头时使用<code>@Headers</code>,这里就不做举例,因为app中通常是每个请求都需要携带请求头,不建议在<code>Retrofit</code>定义请求时传入,而是使用<code>OkHttp</code>来实现统一请求头。</p> <h2>Converter</h2> <p><code>Retrofit</code>在默认情况下只能将<code>Http</code>的响应体反序列化到<code>OkHttp</code>的<code>ResponseBody</code>中,加入<code>Converter</code>可以将返回的数据直接格式化成你需要的样子,现有6个<code>Converter</code>可以直接使用:</p> <ul> <li>Gson: com.squareup.retrofit2:converter-gson</li> <li>Jackson: com.squareup.retrofit2:converter-jackson</li> <li>Moshi: com.squareup.retrofit2:converter-moshi</li> <li>Protobuf: com.squareup.retrofit2:converter-protobuf</li> <li>Wire: com.squareup.retrofit2:converter-wire</li> <li>Simple XML: com.squareup.retrofit2:converter-simplexml</li> <li>Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars</li> </ul> <p>之后在代码里加入(此处以<code>GsonConverterFactory</code>为例)</p> <pre> <code class="language-java"> Retrofit retrofit =new Retrofit.Builder() .baseUrl(Constants.BASE_HTTP_URL) .addConverterFactory(GsonConverterFactory.create()) .build();</code></pre> <p>返回的数据会使用<code>Gson</code>解析为对应传入的实体类,你也可以自定义<code>Converter</code>来实现更复杂的需求,只需要<code>extends Converter.Factory</code>然后重写</p> <pre> <code class="language-java"> @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 }</code></pre> <p><code>Retrofit</code>终归只是应用层的<code>api</code>,真正的执行器是<code>OkHttp</code>,较为复杂的需求都需要从执行层入手,可以做到<code>Retrofit</code>对外不变的多种自定义统一封装。</p> <h2>OkHttp配合Retrofit使用</h2> <p>前文已经提到在<code>Retrofit 2.0</code>中已经默认使用<code>OkHttp</code>作为网络请求执行器,关于<code>OkHttp</code>的优点简单提一下:(<a href="/misc/goto?guid=4959673778567258052">原文链接</a>)</p> <ul> <li>1.支持HTTP2/SPDY黑科技</li> <li>2.socket自动选择最好路线,并支持自动重连</li> <li>3.拥有自动维护的socket连接池,减少握手次数</li> <li>4.拥有队列线程池,轻松写并发</li> <li>5.拥有Interceptors轻松处理请求与响应(比如透明GZIP压缩,LOGGING)</li> <li>6.基于Headers的缓存策略</li> </ul> <p>想要使用<code>OkHttp</code>为<code>Retrofit</code>提供更高的定制性,给<code>Retrofit</code>设置自定义的<code>OkHttpClient</code>就可以了</p> <pre> <code class="language-java">Retrofit retrofit = new Retrofit.Builder() .baseUrl(Constants.BASE_HTTP_URL) .client(client) .build();</code></pre> <p>之后就是构建一个<code>OkHttpClient</code></p> <pre> <code class="language-java">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();</code></pre> <p>如果设置了<code>sslSocketFactory</code>却没有配置对应的<code>hostnameVerifier</code>,那么Https请求是无法成功的。上面用到两个<code>Interceptor</code>分别是<code>HeadInterceptor</code>和<code>HttpLoggingInterceptor</code>,分别是用来添加请求头和打印请求日志的拦截器,<code>OkHttp</code>支持自定义拦截器,例如下面代码自定义的<code>HeadInterceptor</code>为请求加入<code>Headers</code></p> <pre> <code class="language-java">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); } }</code></pre> <p>有时服务器会对<code>POST</code>提交的表单做参数校验,一种方式是在请求头里加入特定方式加密过的表单参数的<code>Map</code>,那么就需要先获取到请求的<code>Map</code>,通过<code>FormBody</code>可以实现</p> <pre> <code class="language-java">// 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)); } }</code></pre> <p><code>HttpLoggingInterceptor</code>是_Square_ 提供的请求信息日志打印工具类,如果需要可以在<code>build.gradle</code>中加入</p> <pre> <code class="language-java"> compile 'com.squareup.okhttp3:logging-interceptor:3.2.0'</code></pre> <p>可以根据不同情况配置日志输出的<code>Level</code>:</p> <ul> <li><strong>NONE</strong> 不输出日志</li> <li><strong>BASIC</strong> 只输出请求方式响应码等基本信息</li> <li><strong>HEADERS</strong> 只输出请求和响应的头部信息</li> <li><strong>BODY</strong> 输出请求和响应的头部和请求体信息</li> </ul> <p>另外如果遇到两个接口有相互依赖关系,必须请求完第一个接口拿到数据后才知道第二个请求的<code>URL</code>,通常我们会定义两个<code>Retrofit</code>,因为<code>Retrofit</code>的<code>BaseUrl</code>的统一配置的,不过现在可以通过实现动态<code>BaseUrl</code>来避免这个问题,先看<code>DynamicBaseUrlInterceptor</code>的代码</p> <pre> <code class="language-java">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); } }</code></pre> <p>在<code>BaseUrl</code>改变时只需要<code>setHost()</code>就可以让下次请求的<code>Baseurl</code>改变</p> <h2>Retrofit 与 RxJava 结合使用</h2> <p>本节需要对<code>RxJava</code>基本用法有了解,如果不了解可以忽略或者先去熟悉一下<code>RxJava</code>的<a href="/misc/goto?guid=4959631832560174430">wiki</a>,介绍的目的是因为两者结合使用确实很方便,关于<code>RxJava</code>之后会单独写。</p> <p><code>RxJava</code>是<code>Rx</code>(全称<code>Reactive Extensions</code>)家族中的一员,是最近很火的响应式编程库,官方对于它的解释很简单</p> <pre> <code class="language-java">RxJava – Reactive Extensions for the JVM – a library for composing asynchronous and event-based programs using observable sequences for the Java VM.</code></pre> <p>一个异步的基于事件的观察者序列,可以理解为扩展的观察者模式,在Android中使用<code>RxJava</code>需要引入两个<code>compile</code>,<code>RxAndroid</code>是专为Android平台打造来提供主线程切换等便利的工具项目。</p> <pre> <code class="language-java"> 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'</code></pre> <p><code>Retrofit</code>提供了<code>CallAdapterFactory</code>,它是一个知道如何将<code>call</code>实例转换成其他类型的工厂类,目前支持的有:</p> <ul> <li>RxJava</li> <li>Guava</li> <li>Java8</li> </ul> <p>这些和<code>Retrofit</code>本身都是分离的,需要单独引入<code>compile</code>例如</p> <pre> <code class="language-java">compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0'</code></pre> <p>在代码中配置<code>CallAdapterFactory</code></p> <pre> <code class="language-java">Retrofit retrofit =new Retrofit.Builder() .baseUrl(Constants.BASE_HTTP_URL) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .build();</code></pre> <p>之后就可以把请求的返回改为<code>Observable<T></code>了</p> <pre> <code class="language-java">@GET("users") Observable<List<Repo>> listRepos(@Path("user") String user);</code></pre> <p>请求时只需要</p> <pre> <code class="language-java">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) { } });</code></pre> <p>如果项需要服务器返回固定的格式用来定义一些业务上的错误如下</p> <pre> <code class="language-java">{ "state":0,//状态码,0为业务正常 "msg":"",//如果业务出错,携带错误信息 "data":{}//包含实际业务实体 }</code></pre> <p>需要定义统一的响应实体,根据<code>T</code>传入的类型来获取业务实体真实的类型</p> <pre> <code class="language-java">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; } }</code></pre> <p>请求中的泛型类型需要是<code>BaseResult<T></code></p> <pre> <code class="language-java">@GET("users") Observable<BaseResult<List<Repo>>> listRepos(@Path("user") String user);</code></pre> <p>调用时也会有改变,需要经过一次拆解统一返回,处理错误的过程</p> <pre> <code class="language-java">BaseNetwork.Factory.create(Foo.class) .listRepos("user") .flatMap(new NetworkResultFunc1<List<Repo>>()) .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(observer);</code></pre> <p><code>flatMap</code>需要传入<code>Func1<T, R></code>,<code>Func1<T, R></code>继承了<code>Function</code>,只有一个方法,将泛型参数列表的第一个转换为第二个返回,它可以将<code>Observable</code>做一个展开,并返回一个新的<code>Observable</code></p> <pre> <code class="language-java">public interface Func1<T, R> extends Function { R call(T t); }</code></pre> <p><code>NetworkResultFunc1<List<Repo>></code>实现了<code>Func1<T, R></code>,代码如下</p> <pre> <code class="language-java">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() } }); } }</code></pre> <p>如果state为0,则调用<code>subscriber.onNext()</code>向调用者返回数据,当state不等于0时意味着业务出错了,向<code>subscriber.onError()</code>中抛了一个<code>ApiException</code>,这样在<code>Observer</code>处会回调<code>onError()</code>终止整个事件流,调用者也能获得业务错误的相关信息。<code>ApiException</code>代码如下,就是一个自定义的<code>RuntimeException</code></p> <pre> <code class="language-java">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; } }</code></pre> <p>对于<code>Retrofit</code>的介绍就先到这里,相信看到这里,你已经能够在项目中优雅的使用<code>Retrofit</code>了。</p> <p><br> 文/<a href="/misc/goto?guid=4959673778685339668">winter1991</a>(简书)<br> </p> <p> </p>