Retrofit簡介
Retrofit
是大名鼎鼎的 Square 公司開源的適用於Android
與Java
的網絡請求庫,官方的介紹非常簡短
A type-safe HTTP client for Android and Java
Retrofit
使用註解,能夠極大的簡化網絡請求,在2.0版本默認使用Square自家的OkHttp
作爲底層Http Client
,關於如何使用OkHttp
配合Retrofit
本文後面也會講到。首先在build.gradle
中加入
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
定義一個網絡請求:
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
爲全路徑,將用這個全路徑作爲請求URL
,BaseUrl
將不起作用。@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
的響應體反序列化到OkHttp
的ResponseBody
中,加入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的緩存策略
想要使用OkHttp
爲Retrofit
提供更高的定製性,給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
分別是HeadInterceptor
和HttpLoggingInterceptor
,分別是用來添加請求頭和打印請求日誌的攔截器,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));
}
}
HttpLoggingInterceptor
是 Square 提供的請求信息日誌打印工具類,如果需要可以在build.gradle
中加入
compile 'com.squareup.okhttp3:logging-interceptor:3.2.0'
可以根據不同情況配置日誌輸出的Level
:
- NONE 不輸出日誌
- BASIC 只輸出請求方式響應碼等基本信息
- HEADERS 只輸出請求和響應的頭部信息
- BODY 輸出請求和響應的頭部和請求體信息
另外如果遇到兩個接口有相互依賴關係,必須請求完第一個接口拿到數據後才知道第二個請求的URL
,通常我們會定義兩個Retrofit
,因爲Retrofit
的BaseUrl
是統一配置的,不過現在可以通過實現動態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
基本用法有了解,如果不瞭解可以忽略或者先去熟悉一下RxJava
的wiki,介紹的目的是因爲兩者結合使用確實很方便,關於RxJava
之後會單獨寫。
RxJava
是Rx
(全稱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
需要引入兩個compile
,RxAndroid
是專爲 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
了。