Retrofit 詳細分解

什么是 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          
                        }
        });
}

如下效果:

圖片名稱
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容