什么是 Retrofit ?
Retrofit是Square開發(fā)的一個Android和Java的REST客戶端庫。這個庫非常簡單并且具有很多特性,相比其他的網(wǎng)絡(luò)庫,更容易讓初學(xué)者快速掌握。它可以處理GET、POST、PUT、DELETE…等請求,還可以使用picasso加載圖片。
常用注解
Retrofit 2.0底層依賴OkHttp實現(xiàn),也就是說Retrofit本質(zhì)上就是對OkHttp的更進一步封裝。Retrofit和其它Http庫最大區(qū)別在于通過大范圍使用注解簡化Http請求。
Retrofit使用注解來描述HTTP請求:
- URL參數(shù)的替換和query參數(shù)的支持
- 對象轉(zhuǎn)化為請求體(如:JSON,protocol buffers等)
- 多重請求體和文件上傳
Retrofit中的注解大體分為以下幾類:用于標注請求方式的注解、用于標記請求頭的注解、用于標記請求參數(shù)的注解。其實,任何一種Http庫都提供了相關(guān)的支持,無非在retrofit中是用注解來簡化。
請求方法注解
該類型的注解用于標注不同的http請求方式,主要有以下幾種:
| 注解 | 說明 |
|---|---|
| @GET | 表明這是get請求 |
| @POST | 表明這是post請求 |
| @PUT | 表明這是put請求 |
| @DELETE | 表明這是delete請求 |
| @PATCH | 表明這是一個patch請求,該請求是對put請求的補充,用于更新局部資源 |
| @HEAD | 表明這是一個head請求 |
| @OPTIONS | 表明這是一個option請求 |
| @HTTP | 通用注解,可以替換以上所有的注解,其擁有三個屬性:method,path,hasBody |
這里不再對其使用做什么說明,官網(wǎng)的示例已經(jīng)寫的非常不錯。平時開發(fā)中我們也只用到了get和post,這是件很悲傷的事情,實際上這些請求方法各自有各自的使用場景。
最容易混淆的是put,post,patch這三者,簡單的說,post表示新增,put可以理解為完整替換,而patch則是更新資源。順便來看看官方定義:
- POST to create a new resource when the client cannot predict the identity on the origin server (think a new order)
- PUT to override the definition of a specified resource with what is passed in from the client
- PATCH to override a portion of a specified resource in a predictable and effectively transactional way (if the entire patch cannot be performed, the server should not do any part of it)
接下來我們來重點說說@HTTP:
@HTTP注解很少用到,這里用個簡單的例子來說明下:我們當前存在獲取驗證碼的請求:
@GET("mobile/capture")
Call<ResponseBody> getCapture(@Query("phone") String phone);
用@HTTP代替后:
@HTTP(method = "get", path = "mobile/capture", hasBody = false)
Call<ResponseBody> getCapture(@Query("phone") String phone);
請求頭注解
該類型的注解用于為請求添加請求頭。
| 注解 | 說明 |
|---|---|
| @Headers | 用于添加固定請求頭,可以同時添加多個。通過該注解添加的請求頭不會相互覆蓋,而是共同存在 |
| @Header | 作為方法的參數(shù)傳入,用于添加不固定值的Header,該注解會更新已有的請求頭 |
首先來看@Headers的示例:
//使用@Headers添加單個請求頭
@Headers("Cache-Control:public,max-age=120")
@GET("mobile/active")
Call<ResponseBody> getActive(@Query("id") int activeId);
//使用@Headers添加多個請求頭
@Headers({
"User-Agent:android"
"Cache-Control:public,max-age=120",
})
@GET("mobile/active")
Call<ResponseBody> getActive(@Query("id") int activeId);
接下來來看@Header的示例:
@GET("mobile/active")
Call<ResponseBody> getActive(@Header("token") String token,@Query("id") int activeId);
可以看出@Header是以方法參數(shù)形勢傳入的,想必你現(xiàn)在能理解@Headers和@Header之間的區(qū)別了。
請求和響應(yīng)格式注解
該類型的注解用于標注請求和響應(yīng)的格式。
| 名稱 | 說明 |
|---|---|
| @FormUrlEncoded | 表示請求發(fā)送編碼表單數(shù)據(jù),每個鍵值對需要使用@Field注解 |
| @Multipart | 表示請求發(fā)送multipart數(shù)據(jù),需要配合使用@Part |
| @Streaming | 表示響應(yīng)用字節(jié)流的形式返回.如果沒使用該注解,默認會把數(shù)據(jù)全部載入到內(nèi)存中.該注解在在下載大文件的特別有用 |
請求參數(shù)類注解
該類型的注解用來標注請求參數(shù)的格式,有些需要結(jié)合上面請求和響應(yīng)格式的注解一起使用。
| 名稱 | 說明 |
|---|---|
| @Body | 多用于post請求發(fā)送非表單數(shù)據(jù),比如想要以post方式傳遞json格式數(shù)據(jù) |
| @Filed | 多用于post請求中表單字段,Filed和FieldMap需要FormUrlEncoded結(jié)合使用 |
| @FiledMap | 和@Filed作用一致,用于不確定表單參數(shù) |
| @Part | 用于表單字段,Part和PartMap與Multipart注解結(jié)合使用,適合文件上傳的情況 |
| @PartMap | 用于表單字段,默認接受的類型是Map |
@Headers
@Headers("Cache-Control: max-age=64000")
@GET("active/list")
Call<List<Active>> ActiveList();
當然@Header也支持同時設(shè)置多個:
@Headers({
"version:1.0.0"
"Cache-Control: max-age=64000"
})
@GET("active/list")
Call<List<Active>> ActiveList();
@Header
@GET("user")
Call<User> getUserInfo(@Header("token") token)
@Body
根據(jù)轉(zhuǎn)換方式將實例對象轉(zhuǎn)換為相應(yīng)的字符串作為請求參數(shù)傳遞。比如在很多情況下,你可能需要以post的方式上傳json格式的數(shù)據(jù)。那么該怎么來做呢?
我們以一個登錄接口為例,該接口接受以下格式的json數(shù)據(jù):
{“password”:”abc123456”,”username”:”18611990521”}
首先建立請求實體,為了區(qū)別其他實體,通常來說約定以Post為后綴.
public class User{
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
}
然后定義該請求api:
@POST("mobile/login")
Call<ResponseBody> login(@Body User user);
retrofit默認采用json轉(zhuǎn)化器,因此在我們發(fā)送數(shù)據(jù)的時候會將LogintPost對象映射成json數(shù)據(jù),這樣發(fā)送出的數(shù)據(jù)就是json格式的。另外,如果你不確定這種轉(zhuǎn)化行為,可以強制指定retrofit使用Gson轉(zhuǎn)換器:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://www.test.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
更詳細的內(nèi)容參照retrofit官網(wǎng),另外關(guān)于retrofit的轉(zhuǎn)換器我會在另一節(jié)中進行詳細的分析。
@Filed & @FiledMap
@Filed通常多用于Post請求中以表單的形勢上傳數(shù)據(jù),這對任何開發(fā)者來說應(yīng)該都是很常見的。
@POST("mobile/register")
Call<ResponseBody> registerDevice(@Field("id") String registerid);
@FileMap和@Filed的用途相似,但是它用于不確定表單參數(shù)個數(shù)的情況下。
@Part & @PartMap
多用于Post請求實現(xiàn)文件上傳功能。關(guān)于這兩者的具體使用參考下文的文件上傳。
在這里我們來解釋一下@Filed和@Part的區(qū)別。
兩者都可以用于Post提交,但是最大的不同在于@Part標志上文的內(nèi)容可以是富媒體形勢,比如上傳一張圖片,上傳一段音樂,即它多用于字節(jié)流傳輸。而@Filed則相對簡單些,通常是字符串鍵值對。
@Path
關(guān)于@Path沒什么好說,官網(wǎng)解釋已經(jīng)足夠清楚了。這里著重提示:
{占位符}和PATH只用在URL的path部分,url中的參數(shù)使用Query和QueryMap 代替,保證接口定義的簡潔
異步VS同步
任何一個任務(wù)都可以被分為異步任務(wù)或者同步任務(wù),和其它大多數(shù)的請求框架一樣,retrofit也分為同步請求和異步請求。在retrofit是實現(xiàn)這兩者非常簡單:
同步調(diào)用
同步請求需要借助retrofit提供的execute()方法實現(xiàn)。
public void get() throws IOException {
Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.github.com/").build();
GitHubApi api = retrofit.create(GitHubApi.class);
Call<ResponseBody> call = api.contributorsBySimpleGetCall(mUserName, mRepo);
Response<ResponseBody> response = call.execute();
if (response.isSuccessful()) {
ResponseBody responseBody = response.body();
//處理成功請求
}else{
//處理失敗請求
}
}
以上的代碼會阻塞線程,因此你不能在安卓的主線程中調(diào)用,不然會面臨NetworkOnMainThreadException。如果你想調(diào)用execute方法,請在后臺線程執(zhí)行。
異步調(diào)用
異步請求需要借助retrofit提供的enqueue()方法實現(xiàn)。(從這個方法名中你可以看出之該方法實現(xiàn)的是將請求加入請求隊列)。像async-http一樣,同樣你需要在enqueue()方法中為其最終結(jié)果提供相應(yīng)的回調(diào),以實現(xiàn)結(jié)果的處理。
public void get() {
Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.github.com/").build();
GitHubApi api = retrofit.create(GitHubApi.class);
Call<ResponseBody> call = api.contributorsBySimpleGetCall(mUserName, mRepo);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
//處理請求成功
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
//處理請求失敗
}
});
}
不難發(fā)現(xiàn)retrofit中實現(xiàn)同步和異步是如此的方便,僅僅通過提供請求的不同執(zhí)行方法(execute()和enqueue())便可成功的實現(xiàn)的請求執(zhí)行方式和請求類型的解耦,實在是棒極了。
到目前為止,使用retrofit的多是在Android上,此時我們關(guān)注多事異步請求,畢竟Android中并不允許你在主線程去做一些耗時任務(wù)。
無論是同步請求還是異步請求,我們都希望這兩種請求是可控的,通常來說是分為三個方面:開始請求,結(jié)束請求以及查詢請求的執(zhí)行狀態(tài)。上面的同步請求和異步請求屬于開始請求這方面,那么結(jié)束請求和查詢請求呢?
我們發(fā)現(xiàn)無論是同步請求還是異步請求,返回給我們的都是Call接口的實例。我們稍微看一下該接口:
public interface Call<T> extends Cloneable {
//執(zhí)行同步請求
Response<T> execute() throws IOException;
//執(zhí)行異步請求
void enqueue(Callback<T> callback);
//該請求是否在執(zhí)行過程中
boolean isExecuted();
//取消當前執(zhí)行的請求
void cancel();
//該請求是否被執(zhí)行
boolean isCanceled();
//和java中的clone()方法含義一樣,通常利用該請求實現(xiàn)重復(fù)請求
Call<T> clone();
//獲取原始請求
Request request();
}
通過上面的代碼不難看出,Call接口提供了我們上面所說的執(zhí)行請求,查詢請求狀態(tài)以及結(jié)束請求。
移除請求
看完上面的Call對象之后,我們知道要想取消一個請求(無論異步還是同步),則只需要在響應(yīng)的Call對象上調(diào)用其cancel()對象即可。
public void cancle(){
Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.github.com/").build();
GitHubApi api = retrofit.create(GitHubApi.class);
Call<ResponseBody> call = api.contributorsBySimpleGetCall(mUserName, mRepo);
Response<ResponseBody> response = call.execute();
if (response.isSuccessful()) {
ResponseBody responseBody = response.body();
//處理成功請求
}else{
//處理失敗請求
}
...
//取消相關(guān)請求
call.cancel();
}
多次請求
個別情況下我們可能需要一個請求執(zhí)行多次。但是我們在retrofit中,call對象只能被調(diào)用一次,這時候該怎么辦?
這時候我們可以利用Call接口中提供的clone()方法實現(xiàn)多次請求。
public void multi_async_get() {
Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.github.com/").build();
GitHubApi api = retrofit.create(GitHubApi.class);
Call<ResponseBody> call = api.contributorsBySimpleGetCall(mUserName, mRepo);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
//請求成功
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
//請求失敗
}
});
call.clone().enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
//請求成功
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
//請求失敗
}
});
}
關(guān)于請求
上面我們簡單的介紹了retrofit中注解,但是我并不準備像入門教程一樣去舉例說明。這里我們只對大家經(jīng)常有困惑的地方做說明:
提交json格式數(shù)據(jù)
很多情況下,我們需要上傳json格式的數(shù)據(jù)。比如當我們注冊新用戶的時候,因為用戶注冊時的數(shù)據(jù)相對較多,并可能以后會變化,這時候,服務(wù)端可能要求我們上傳json格式的數(shù)據(jù)。此時就要@Body注解來實現(xiàn)。
首先定義請求實體RegisterPost:
public class RegisterPost {
private String username;
private int age;
...
}
接下來定義請求方法:
@POST("mobile/register")
Call register1(@Body RegisterPost post);
這樣我們就能夠上傳json格式的數(shù)據(jù)了。
上傳文件
retrofit中的實現(xiàn)文件上傳也是非常簡單的。這里我們以圖片上傳為例。
單張圖片上傳
retrofit 2.0的上傳和以前略有不同,需要借助@Multipart注解、@Part和MultipartBody實現(xiàn)。
首先定義上傳接口
@Multipart
@POST("mobile/upload")
Call<ResponseBody> upload(@Part MultipartBody.Part file);
然后來看看如何調(diào)用該方法。和調(diào)用其他請求稍有不同,這里我們需要構(gòu)建MultipartBody對象:
File file = new File(url);
//構(gòu)建requestbody
RequestBody requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), file);
//將resquestbody封裝為MultipartBody.Part對象
MultipartBody.Part body = MultipartBody.Part.createFormData("file", file.getName(), requestFile);
這樣,我們就可以方便的進行上傳圖片了。
多張圖片上傳
如果有很多張圖片要上傳,我們總不能一張一張的來吧?好吧,我們來看看如果進行多文件(圖片)上傳。
在retrofit中提供了@PartMap注解,借助該對象,我們可以實現(xiàn)多文件的上傳。同樣我們來看看具體文件的定義
@Multipart
@POST("upload/upload")
Call<ResponseBody> upload(@PartMap Map<String, MultipartBody.Part> map);
和單文件上傳的唯一區(qū)別就是將@Part注解換成了@PartMap注解。這意味我們可以以Map的形式進行多文件上傳。具體如何調(diào)用相信你已經(jīng)明白。
圖文混傳
無論是多文件上傳還是單文件上傳,本質(zhì)上個都是借助@Multipart注解和MultipartBody來實現(xiàn)的。
這和其他網(wǎng)絡(luò)請求框架的實現(xiàn)原理并無本質(zhì)區(qū)別,但retrofit在圖文上傳方面得天獨厚的優(yōu)勢。比如我們在注冊時候既要傳用戶文本信息又要上傳圖片,結(jié)合上面的用戶注冊來做說明:
@Multipart
@POST("")
Call<ResponseBody> register(@Body RegisterPost post,@Part MultipartBody.Part image);
該注冊接口實現(xiàn)了用戶注冊信息和用戶頭像的同時上傳,其調(diào)用無非就是結(jié)合我們上文提到的json數(shù)據(jù)上傳以及單張圖上傳。
文件下載
很多時候,我們可能需要暫時下載文件,但是又不希望引入其他的下載庫,那么如何retrofit實現(xiàn)下載呢?同樣,我們還是以下載圖片為例
首先定義api接口如下:
@GET
Call<ResponseBody> downloadPicture(@Url String fileUrl);
關(guān)鍵就是獲取到ResponseBody對象。我們來看獲取到ResponseBody之后的處理:
InputStream is = responseBody.byteStream();
String[] urlArr = url.split("/");
File filesDir = Environment.getExternalStorageDirectory();
File file = new File(filesDir, urlArr[urlArr.length - 1]);
if (file.exists()) file.delete();
不難發(fā)現(xiàn)這里的關(guān)鍵就是通過ResponseBody對象獲取字節(jié)流,最后將其保存下來即可。實現(xiàn)下載就是這么簡單。
這里需要注意的是如果下載的文件較大,比如在10m以上,那么強烈建議你使用@Streaming進行注解,否則將會出現(xiàn)IO異常.
@Streaming
@GET
Observable<ResponseBody> downloadPicture(@Url String fileUrl);
攔截器Interceptors使用
熟悉OkHttp的童鞋對Interceptors一定不會陌生。而Retrofit 2.0 底層強制依賴okHttp,所以可以使用okHttp的攔截器Interceptors 來對所有請求進行再處理。同樣來說,我們經(jīng)常使用攔截器實現(xiàn)以下功能:
- 設(shè)置通用Header
- 設(shè)置通用請求參數(shù)
- 攔截響應(yīng)
- 統(tǒng)一輸出日志
- 實現(xiàn)緩存
下面我們以上各自使用的場景給出相應(yīng)的代碼說明:
設(shè)置通用Header
在App api接口設(shè)計中,我們往往需要客戶端在請求方法時,攜帶appid,appkey,timestamp,signature及version等header。你可能會問前邊不提到的@Headers不也同樣可以做到這事情么?在方法很少的情況下,或者個別請求方法需要的情況下使用@Headers來添加當然可以,但是如果要為所有請求方法都添加還是借助攔截器使用更為方便。直接看代碼:
public static Interceptor getRequestHeader() {
Interceptor headerInterceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Request.Builder builder = originalRequest.newBuilder();
builder.header("appid", "1");
builder.header("timestamp", System.currentTimeMillis() + "");
builder.header("appkey", "zRc9bBpQvZYmpqkwOo");
builder.header("signature", "dsljdljflajsnxdsd");
Request.Builder requestBuilder =builder.method(originalRequest.method(), originalRequest.body());
Request request = requestBuilder.build();
return chain.proceed(request);
}
};
return headerInterceptor;
}
你會發(fā)現(xiàn)在設(shè)置header的時候,我們有兩種方法可選擇:addHeader()和header()。切莫混淆兩者之間的區(qū)別:
使用addHeader()不會覆蓋之前設(shè)置的header,若使用header()則會覆蓋之前的header
統(tǒng)一輸出請求日志
在開發(fā)調(diào)試階段,我們希望看到每個請求的詳細信息,在release時關(guān)閉這些消息。
得益于retrofit和okhttp的良好設(shè)計,可以方便的通過添加Log攔截器來實現(xiàn),這里我們使用到OkHttp中的HttpLoggingInterceptor攔截器。
在retrofit 2.0中要使用日志攔截器,首先添加依賴:
compile 'com.squareup.okhttp3:logging-interceptor:3.1.2'
然后創(chuàng)建日志攔截器
public static HttpLoggingInterceptor getHttpLoggingInterceptor() {
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
return loggingInterceptor;
}
攔截服務(wù)器響應(yīng)
通常來說,我們多利用攔截器來實現(xiàn)對請求的攔截。但是在很多的情況下我們需要從響應(yīng)中獲取響應(yīng)的Headers中獲取指定的header,比如在有些功能中我們需要服務(wù)端會給出我們某個活動的起始時間,需要我們客戶端來判斷當然活動是否可以執(zhí)行。這時候,我們顯然不能利用客戶端本地的時間(有條原則叫做永遠不要相信客戶端的時間),這時候就需要服務(wù)端在將服務(wù)器的時間傳給我們。為了方便,通常時間服務(wù)器的時間戳放在每個響應(yīng)Header當中。
那么我們該怎么拿到這個時間戳呢?攔截器可以非常容易的幫助我們解決這個問題。這里我們假設(shè)服務(wù)器在任何一個響應(yīng)的Header中都添加了time,我們要做的就是通過攔截器來獲取到Header,具體見代碼:
public static Interceptor getResponseHeader() {
Interceptor interceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
String timestamp = response.header("time");
if (timestamp != null) {
//獲取到響應(yīng)header中的time
}
return response;
}
};
return interceptor;
}
通過上面的響應(yīng)攔截器實現(xiàn)了從響應(yīng)中獲取服務(wù)器返回的time,就是這么簡單。
設(shè)置通用請求參數(shù)
在實際項目中,各個客戶端往往需要向服務(wù)端傳送一些固定的參數(shù),通常來說有兩種方案:
- 可以將這個公共的請求參數(shù)放到請求Header中
- 也可以將其放在請求參數(shù)中
如何添加到header中我們已經(jīng)介紹過了,現(xiàn)在來看看如何添加公共請求參數(shù)。添加公共請求參數(shù)和添加公共Header實現(xiàn)原理一致,都是借助攔截器來實現(xiàn),這里我們同樣直接來看代碼:
private void commonParamsInterceptor() {
Interceptor commonParams = new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request originRequest = chain.request();
Request request;
// String method = originRequest.method();
// Headers headers = originRequest.headers();
HttpUrl httpUrl = originRequest.url().newBuilder().addQueryParameter("paltform", "android").addQueryParameter("version", "1.0.0").build();
request = originRequest.newBuilder().url(httpUrl).build();
return chain.proceed(request);
}
};
return commonParams;
}
建議:如果需要添加統(tǒng)一的請求參數(shù),最好將其放在請求頭當中。
使用攔截器
上面我們介紹在實際開發(fā)中4中常用的攔截器,可以發(fā)現(xiàn)有了這些攔截器,我們可以很容易處理公共聚焦點。至于攔截器的使用,就是直接將響應(yīng)的攔截器設(shè)置給OkHttpClient客戶端即可,以添加日志攔截器為例:
HttpLoggingInterceptor logging = getHttpLoggingInterceptor();
//設(shè)置日志攔截器
OkHttpClient httpClient = new OkHttpClient.Builder().addInterceptor(logging).build();
Retrofit retofit=new Retrofit.Builder().baseUrl("http://www.demo.com").client(httpClient).build();
客戶端請求策略
任何一個Http請求庫都少不了失敗重試及請求超時的設(shè)置。來看一下retrofit中如何設(shè)置:
失敗重試
retrofit通過okHttpClient來設(shè)置失敗時自動重試,其使用也非常簡單:
public void setRetry(OkHttpClient.Builder builder) {
builder.retryOnConnectionFailure(true);
}
設(shè)置請求超時
當然,retrofit作為一個完善的網(wǎng)絡(luò)請求框架也少不了這方面的設(shè)置。
public void setConnecTimeout(OkHttpClient.Builder builder) {
builder.connectTimeout(10, TimeUnit.SECONDS);
builder.readTimeout(20, TimeUnit.SECONDS);
builder.writeTimeout(20, TimeUnit.SECONDS);
}
添加緩存支持
在上面攔截器的使用中,我i門已經(jīng)介紹了4種攔截器的使用,現(xiàn)在我們來介紹如何使用攔截器來實現(xiàn)HTTP緩存。Http緩存原理在本文中并不做重點解釋。
ok,現(xiàn)在來看看Retrofit中如何配置使用緩存。
設(shè)置緩存的的兩種方式
在retrofit中可以通過兩種方式設(shè)置緩存:
- 通過添加 @Headers(“Cache-Control: max-age=120”) 進行設(shè)置。添加了Cache-Control 的請求,retrofit 會默認緩存該請求的返回數(shù)據(jù)。
- 通過Interceptors實現(xiàn)緩存。
這兩者實現(xiàn)原理一致,但是適用場景不同。通常是使用Interceptors來設(shè)置通用緩存策略,而通過@Header針對某個請求單獨設(shè)置緩存策略。另外,一定要記住,retrofit 2.0底層依賴OkHttp實現(xiàn),這也就意味著retrofit緩存的實現(xiàn)同樣是借助OkHttp來的。另外,無論你是決定使用那種形勢的緩存,首先要為OkHttpClient設(shè)置Cache,否則緩存不會生效(retrofit并為設(shè)置默認緩存目錄),Cache的設(shè)置你將在下文看到。
下面我們來具體看看,如何通過@Headers為某個方法設(shè)置緩存時間
@Headers("Cache-Control:public,max-age=120")
@GET("mobile/active")
Call<ResponseBody> getActive(@Query("id") int activeId);
這樣我們就通過@Headers快速的為該api添加了緩存控制。120s內(nèi),緩存都是生效狀態(tài),即無論有網(wǎng)無網(wǎng)都讀取緩存。
現(xiàn)在我們再來看一下如何利用攔截器來實現(xiàn)緩存:
首先創(chuàng)建緩存攔截器:
public static Interceptor getCacheInterceptor() {
return new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
return response.newBuilder().header("Cache-Control","public,max-age=120").build();
}
};
}
和其它的攔截器使用一樣,將其設(shè)置到OkHttpClient即可,但此時設(shè)置緩存攔截器使用的addNetworkInterceptor()方法。凡是使用該設(shè)置了該緩存攔截器的OkHttpClient都具備了緩存功能,具體代碼如下:
//創(chuàng)建Cache
Cache cache = new Cache(AppContext.context().getCacheDir(), 10 * 1024 * 1024);
//設(shè)置攔截器和Cache
OkHttpClient httpClient = new OkHttpClient.Builder().addNetworkInterceptor(getCacheInterceptor()).cache(cache).build();
//設(shè)置OkHttpClient
Retrofit retofit=new Retrofit.Builder().baseUrl("http://www.stay4it.com").client(httpClient).build();
實際開發(fā)中往往要求,有網(wǎng)的情況下直接從網(wǎng)絡(luò)中獲取數(shù)據(jù),無網(wǎng)絡(luò)的情況下才走緩存,那么此時上面的緩存攔截器就不是適用了,那這該怎么做呢?
在解決這個問題之前首先我們解決大家的一個疑惑:通過addNetworkInterceptor()和通過addInterceptor()添加的攔截器有什么不同呢?
簡單來說,addNetworkInterfacetor()添加的是網(wǎng)絡(luò)攔截器(Network Interfacetor),它會在request和response時分別被調(diào)用一次;addInterceptor()添加的是應(yīng)用攔截器(Application Interceptor),他只會在response被調(diào)用一次。OkHttp中對此做了更加詳細的解釋[OkHttp攔截器詳解]
我們將上面的緩存問題再明確一下:在無網(wǎng)絡(luò)的情況下讀取緩存,有網(wǎng)絡(luò)的情況下根據(jù)緩存的過期時間重新請求,根據(jù)需求,我們創(chuàng)建以下攔截器:
public static Interceptor getCacheInterceptor() {
return new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if (!TDevice.hasInternet()) {
//無網(wǎng)絡(luò)下強制使用緩存,無論緩存是否過期,此時該請求實際上不會被發(fā)送出去。
request=request.newBuilder().cacheControl(CacheControl.FORCE_CACHE)
.build();
}
Response response = chain.proceed(request);
if (TDevice.hasInternet()) {//有網(wǎng)絡(luò)情況下,根據(jù)請求接口的設(shè)置,配置緩存。
//這樣在下次請求時,根據(jù)緩存決定是否真正發(fā)出請求。
String cacheControl = request.cacheControl().toString();
//當然如果你想在有網(wǎng)絡(luò)的情況下都直接走網(wǎng)絡(luò),那么只需要
//將其超時時間這是為0即可:String cacheControl="Cache-Control:public,max-age=0"
return response.newBuilder().header("Cache-Control", cacheControl)
.removeHeader("Pragma")
.build();
}else{//無網(wǎng)絡(luò)
return response.newBuilder().header("Cache-Control", "public,only-if-cached,max-stale=360000")
.removeHeader("Pragma")
.build();
}
}
};
}
接下來,將該請求設(shè)置到OkHttpClient,此時我們的網(wǎng)絡(luò)攔截器和應(yīng)用攔截器都添加的是上面同一個攔截器:
//創(chuàng)建Cache
Cache cache = new Cache(AppContext.context().getCacheDir(), 10 * 1024 * 1024);
//設(shè)置攔截器和Cache
OkHttpClient httpClient = new OkHttpClient.Builder().addNetworkInterceptor(getCacheInterceptor()).cache(cache).addInterceptor(getCacheInterceptor()).build();
//設(shè)置OkHttpClient
Retrofit retofit=new Retrofit.Builder().baseUrl("http://api.stay4it.com/").client(httpClient).build();
實際上,緩存策略應(yīng)該由服務(wù)器指定,但是在有些情況下服務(wù)器并不支持緩存策略,這就要求我們客戶端自行設(shè)置緩存策略。以上的代碼假設(shè)服務(wù)端不支持緩存策略,因此器緩存策略完全由客戶端通過重寫request和response來實現(xiàn)。
不出意外,在進行一些網(wǎng)絡(luò)請求后,我們就可以在緩存目前下看到許多的緩存文件。每一個請求的緩存文件都分為兩部分,非別是以.0結(jié)尾的請求和以.1結(jié)尾的響應(yīng)數(shù)據(jù)。到這里,關(guān)于緩存的部門我們就說完了。我們可能會問,必須要基于retrofit來實現(xiàn)緩存么?如果,以后我更換網(wǎng)絡(luò)框架(盡管可能性非常?。?,這豈不是要出大問題?如果你此顧慮,完全可以自行實現(xiàn)一套緩存框架,其原理本質(zhì)上也非常相似:基于LRU算法。你可能不了解LRU算法,但是LRUCache和LRUDiskCache想必是耳熟能詳?shù)?,對此我不畫蛇添足了?/p>
如果你想了解更多,請參考: Retrofit實現(xiàn)持久化Cookie的三種方案
轉(zhuǎn)換器Converter
Retrofit可以將服務(wù)器的json結(jié)果會自動解析成定義好了的Data Access Object(DAO)。
如果你想接收json 結(jié)果并解析成DAO,你必須把Gson Converter 作為一個獨立的依賴添加進來。
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
然后使用addConverterFactory把它添加進來。
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://api.stay4it.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
這里是Square提供的官方Converter modules列表。選擇一個最滿足你需求的。
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
你也可以通過實現(xiàn)Converter.Factory接口來創(chuàng)建一個自定義的converter 。
我比較贊同這種新的模式。它讓Retrofit對自己要做的事情看起來更清晰。
自定義Gson對象
為了以防你需要調(diào)整json里面的一些格式,比如,Date Format。你可以創(chuàng)建一個Gson 對象并把它傳遞給GsonConverterFactory.create()。
Gson gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://api.nuuneoi.com/base/")
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
與RxJava一起使用
在Retrofit中使用RxJava,你的項目依賴中必須包含兩個modules:
compile 'com.squareup.retrofit:adapter-rxjava:2.0.0-beta1'
compile 'io.reactivex:rxandroid:1.0.1'
Sync Gradle并在Retrofit Builder鏈表中如下調(diào)用addCallAdapterFactory:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://api.stay4it.com/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
你的Service接口現(xiàn)在可以作為Observable返回了!
public interface APIService {
@POST("list")
Call<DessertItemCollectionDao> loadDessertList();
@POST("list")
Observable<DessertItemCollectionDao> loadDessertListRx();
}
你可以完全像RxJava那樣使用它,如果你想讓subscribe部分的代碼在主線程被調(diào)用,需要把observeOn(AndroidSchedulers.mainThread())添加到鏈表中。
Observable<DessertItemCollectionDao> observable = service.loadDessertListRx();
observable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.unsubscribeOn(Schedulers.io())
.subscribe(new Subscriber<DessertItemCollectionDao>() {
@Override
public void onCompleted() {
Toast.makeText(getApplicationContext(),
"Completed",
Toast.LENGTH_SHORT)
.show();
}
@Override
public void onError(Throwable e) {
Toast.makeText(getApplicationContext(),
e.getMessage(),
Toast.LENGTH_SHORT)
.show();
}
@Override
public void onNext(DessertItemCollectionDao dessertItemCollectionDao) {
Toast.makeText(getApplicationContext(),
dessertItemCollectionDao.getData().get(0).getName(),
Toast.LENGTH_SHORT)
.show();
}
});
新的URL定義方式
Retrofit 2.0使用了新的URL定義方式。Base URL與@Url 不是簡單的組合在一起而是和”“的處理方式一致。
對于 Retrofit 2.0中新的URL定義方式,這里是我的建議:
Base URL: 總是以 /結(jié)尾
@Url: 不要以 / 開頭
比如
public interface APIService {
@POST("user/list")
Call<Users> loadUsers();
}
public void doSomething() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://api.stay4it.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
APIService service = retrofit.create(APIService.class);
}
以上代碼中的loadUsers會從 http://api.stay4it.com/user/list獲取數(shù)據(jù)。
而且在Retrofit 2.0中我們還可以在@Url里面定義完整的URL:
public interface APIService {
@POST("http://api.stay4it.com/user/list")
Call<Users> loadSpecialUsers();
}
這種情況下Base URL會被忽略。
混淆
如果你的工程中使用了代碼混淆,那么你的配置中需要添加一下的幾行
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions
實戰(zhàn)演練
配置
在build.gradle中添加
.....
//編譯RxJava
compile 'io.reactivex:rxjava:1.1.6'
//編譯RxAndroid
compile 'io.reactivex:rxandroid:1.2.1'
//編譯Retrofit及其相關(guān)庫,包括Gson
compile 'com.squareup.okhttp3:okhttp:3.3.1'
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
compile 'com.squareup.okhttp3:logging-interceptor:3.3.1'
說明:
Retrofit默認依賴于okhttp,所以需要集成okhttp。
API返回的數(shù)據(jù)為JSON格式,在此我使用的是Gson對返回數(shù)據(jù)解析.請使用最新版的Gson 。
接口
這里我們調(diào)試借助百度名人名言API
該接口的API主機地址為:http://apistore.baidu.com;
需要訪問的接口:avatardata/mingrenmingyan/lookup;
需要一個key等于apikey的Header和一個keyword等于名人名言的查詢關(guān)鍵字,而且該請求為GET請求.
接口返回json格式:
{
"total": 10,
"result": [
{
"famous_name": "佚名",
"famous_saying": "婚姻是一家私人專門銀行,存儲真愛和默契,提取幸福和快樂。夫妻雙方互為賬戶,且存折是活期的,可以隨存隨取,而家庭則是這家銀行里的柜臺,通過它,夫妻雙方可以把自己的喜怒哀樂盡情地存進對方的銀行里,并可隨時提取微笑、鼓勵、安慰、體貼、溫柔等利息。"
},
{
"famous_name": "英國",
"famous_saying": "真愛無坦途"
},
{
"famous_name": "狄太人",
"famous_saying": "一個人真愛的時候,甚至?xí)氩坏阶约菏菒壑鴮Ψ健?
},
{
"famous_name": "佚名",
"famous_saying": "所有的阻礙,全是對真愛的淬煉。"
},
{
"famous_name": "羅蘭",
"famous_saying": "當你真愛一個人的時候,你是會忘記自己的苦樂得失,而只是關(guān)心對方的苦樂得失的。"
},
{
"famous_name": "羅蘭",
"famous_saying": "當兩人之間有真愛情的時候,是不會考慮到年齡的問題,經(jīng)濟的條件,相貌的美丑,個子的高矮,等等外在的無關(guān)緊要的因素的。假如你們之間存在著這種問題,那你要先問問自己,是否真正在愛才好。"
},
{
"famous_name": "佚名",
"famous_saying": "真正的勇氣是來自內(nèi)心的真愛。"
},
{
"famous_name": "佚名",
"famous_saying": "天國般的幸福,存在于對真愛的希望。"
},
{
"famous_name": "狄太人",
"famous_saying": "一個人真愛的時候,甚至?xí)氩坏阶约菏菒壑鴮Ψ?
},
{
"famous_name": "Shakespeare",
"famous_saying": "通向真愛的路從無坦途。"
}
],
"error_code": 0,
"reason": "Succes"
}
定義實體類
我們根據(jù)上面API返回的json數(shù)據(jù)來創(chuàng)建一個Famous數(shù)據(jù)對象,我們可以利用AndroidStudio插件 GsonFormat 快速方便的將json數(shù)據(jù)轉(zhuǎn)為Java 對象。
Famous.java
public class Famous {
//下面變量的定義要與接口中的字段名字保持一致
public int total;
public int error_code;
public String reason;
public List<FamousInfo> result;
public static class FamousInfo {
public String famous_name;
public String famous_saying;
}
}
注意:如果你的字段有跟json不一樣的,要在字段上面加注解@SerializedName,@SerializedName是指定Json格式中的Key名。
如上面的錯誤碼字段,你就像定義為code,而服務(wù)器返回的是error_code,這個時候就應(yīng)該這么寫:
@SerializedName("error_code")
public int code;
使用
首先定義
public abstract class BaseApi {
public static final String API_SERVER = "服務(wù)器地址"
private static final OkHttpClient mOkHttpClient = new OkHttpClient();
private static Retrofit mRetrofit;
protected static Retrofit getRetrofit() {
if (Retrofit == null) {
Context context = Application.getInstance().getApplicationContext();
//設(shè)定30秒超時
mOkHttpClient.setConnectTimeout(30, TimeUnit.SECONDS);
//設(shè)置攔截器,以用于自定義Cookies的設(shè)置
mOkHttpClient.networkInterceptors()
.add(new CookiesInterceptor(context));
//設(shè)置緩存目錄
File cacheDirectory = new File(context.getCacheDir()
.getAbsolutePath(), "HttpCache");
Cache cache = new Cache(cacheDirectory, 20 * 1024 * 1024);
mOkHttpClient.setCache(cache);
//構(gòu)建Retrofit
mRetrofit = new Retrofit.Builder()
//配置服務(wù)器路徑
.baseUrl(API_SERVER + "/")
//設(shè)置日期解析格式,這樣可以直接解析Date類型
.setDateFormat("yyyy-MM-dd HH:mm:ss")
//配置轉(zhuǎn)化庫,默認是Gson
.addConverterFactory(GsonConverterFactory.create())
//配置回調(diào)庫,采用RxJava
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
//設(shè)置OKHttpClient為網(wǎng)絡(luò)客戶端
.client(mOkHttpClient)
.build();
}
return mRetrofit;
}
}
定義FamousApi
public class FamousApi extends BaseApi{
//定義接口
private interface FamousService {
@GET("/avatardata/mingrenmingyan/lookup")
Observable<Famous> getFamousList(@Header("apiKey") String apiKey,
@Query("keyword") String keyword,
@Query("page") int page,
@Query("rows") int rows);
}
protected static final FamousService service = getRetrofit().create(FamousService.class);
public static Observable<UserProfileResp> getFamousList(String apiKey,String keyword, int page, int rows){
return service.getFamousList(apiKey, keyword, page, rows);
}
}
最終使用:
public void getFamousList(){
FamousApi.getFamousList("apiKey","人才",1,20)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<Famous>(){
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(Famous famous) {
List<FamousInfo> list = famous.result;
//填充UI
}
});
}
如下效果: