Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 學會Retrofit+OkHttp+RxAndroid的使用

學會Retrofit+OkHttp+RxAndroid的使用

編輯:關於Android編程

概括

這篇博客裡面就來實踐下。在上一篇博客裡面說到了OkHttp類似HttpUrlConnection。按這樣說的話,我們在項目中肯定還是要封裝一層。如果嫌封裝麻煩的話,也可以拿來主義,比如使用鴻洋大神的OkHttpUtils,網絡上對它也好評如潮。又或者曾經很火的Volley框架。為什麼說曾經呢?也不是說它用的少了,只能說有更火的框架出來了。是什麼呢?沒錯,就是這篇文章說到的Retrofit框架。既然是新框架,那為什麼前面又說是OkHttp的實踐呢?這裡我們就要理解Retrofit這個框架了。Retrofit這個框架網絡請求層事用的是OkHttp,它同樣是Square開源組合推出的一個框架。在Retrofit2.0以前,還可以選擇HttpUrlConnection或者HttpClient去請求。Retrofit最近推出的2.0版本以後,直接強制用戶使用OkHttp去做網絡請求了。所以可以說Retrofit和OkHttp已經是一對同胞兄弟了。

其實Retrofit還沒有廣泛使用的時候,使用的最多的還是Volley框架的。Retrofit和Volley一樣對HttpURLConnection或者OkHttp進行封裝。然後有一天,你和你的同事說,咱們把Volley改成Retrofit框架吧,你同事就問你,Volley用的好好的,干嘛要換。那我們要怎麼勸服他去使用呢?你就會要說,Volley的原理我們通過一系列封裝成為一個Request對象,然後我們把它添加到RequestQueue裡面,然後通過NetworkDispatcher進行網絡請求,而Retrofit只需要定義一個API。就可以直接返回我們要請求的數據了。當然,它最好是一個RestfulAPI。

RestfulAPI的理解

網上對RestfulAPI這個概念有很多種理解,說的已經讓我們摸不著頭腦了。怎麼來理解RestfulAPI呢?符合Restful風格的就是RestfulAPI。Restful風格有是什麼鬼?RESTful即Representational State Transfer,可以把它翻譯成(資源)表現層狀態轉換。理解這個名詞就懂了。

資源,服務器給客戶端的文字,圖片,視頻都可以理解為資源。我們一般都是URL這個資源實體去指向資源所在的路徑,當然這個路徑必須是名詞組成的,不能是動詞。比如https://www.google.com.hk/這個網址就可以說是一種資源。 表現層(Representational ),http請求的時候,會有http協議的head部分,post請求的時候還有http body。它描述了請求資源的Content-Type和Content-length等等。這就是一種表示層。又或者我們常使用的json格式也是一種表示層。

狀態轉換(State Transfer)在http請求中,GET用來獲取資源,POST用來新建資源(也可以用於更新資源),PUT用來更新資源,DELETE用來刪除資源。都是狀態轉換,而這些狀態轉換又是建立在表現層之上的,http頭部表現層就會描述請求是通過get或者post方式等來請求的。

為什麼難理解呢,主要是Restful只是一種風格,沒有一套完整的標准,所以網絡上各有各的理解。

准備RESTful API

既然這樣,那麼這裡我們就要先准備下幾個基本的RESTful API。我這裡准備了
一個user表

這裡寫圖片描述

一個新聞列表(news)表<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPjxpbWcgYWx0PQ=="這裡寫圖片描述" src="/uploadfile/Collfiles/20160620/2016062009092949.png" title="\" />

3個API(我的本地ip為192.168.1.103:8080)
注冊接口 http://192.168.1.103:8080/GoachWeb/RegisterDataServlet
參數:username、password(POST/Get)
返回:

{
    "resultCode": 200, 
    "responseTime": "2016-06-14 22:38:49", 
    "data": {
        "errorCode": 1, 
        "userId": 1000000, 
        "userName": "Goach"
    }
}

登錄接口 http://192.168.1.103:8080/GoachWeb/LoginDataServlet

參數:username、password(POST/Get)
返回:

{
    "resultCode": 200, 
    "responseTime": "2016-06-14 22:38:49", 
    "data": {
        "errorCode": 1, 
        "userId": 1000000, 
        "userName": "Goach"
    }
}

新聞列表接口
參數:userId(POST/Get)
返回

{
    "resultCode": 200, 
    "responseTime": "2016-06-18 22:17:30", 
    "data": {
        "newsItem": [
            {
                "id": 1, 
                "title": "高盛:中國房地產可能在6-9個月內迎來“拐點", 
                "content": "6月14日,王逸等高盛分析師在報告中寫道,預計2017年房價將疲弱,因為該行業因槓桿率上升、需求減弱,不久將見到拐點。"
            }, 
            {
                "id": 2, 
                "title": "國產大飛機C919首飛時間曝光 已接517架次訂單", 
                "content": "《經濟參考報》記者日前從多個權威渠道獲悉,我國自主研制的C919大型客機將於今年下半年首飛,最快2017年完成後續各項技術驗證,並開始正式交付。"
            }, 
            {
                "id": 3, 
                "title": "解放軍大批巨炮同時開火 現場升碩大火球", 
                "content": " 6月10日,陸軍第42集團軍某防空旅全員全裝在粵東某陌生地域展開戰場機動、偵察預警、陸空對抗、實彈射擊等課目訓練,錘煉部隊實戰本領。"
            }, 
            {
                "id": 4, 
                "title": "拳王鄒市明,一場比賽460萬獎金,只開90萬的車", 
                "content": "中國拳王鄒市明,一年的收入有多少?和帕奎奧,梅威瑟這種級別的相比,鄒市明的收入只能算是小收入,從最初打職業比賽時的30萬美金的獎金,到最高70萬美金獎金,這其中受了多少傷只有他自己最清楚。如果能7場比賽速成世界拳王,獎金不過也就100萬美金,或許他”永遠“也不能成為梅威瑟這樣的拳王。"
            }, 
            {
                "id": 5, 
                "title": "40萬人看楊毅直播講道理???", 
                "content": " 由總決賽第四場比賽中,楊毅對於詹姆斯和格林的一次沖突而進行的評述,引發的一系列事件,還在持續發酵中。"
            }, 
            {
                "id": 11, 
                "title": "女王杯穆雷雙搶7險勝 瓦林卡爆冷止步首輪", 
                "content": "騰訊體育6月15日訊 ATP500賽倫敦女王杯草地公開賽今日繼續男單首輪比賽的爭奪,賽會頭號種子、英國名將穆雷通過兩盤搶7以7-6(8)和7-6(1)險勝法國選手馬胡特,驚險晉級次輪;而2號種子瑞士名將瓦林卡則連丟兩盤以2-6和6-7(3)不敵西班牙選手沃達斯科,爆冷止步首輪。"
            }
        ]
    }
}

接口比較簡單。主要是自己後台開發比較low。這裡後台使用的是通過servlet和jdbc通過gson轉換為json進行開發的。

使用Retrofit框架

接口准備好了。下面就來集成Retrofit框架。

添加幾個權限

 
 

build.gradle添加依賴,下面會用到的也在這裡了:

compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'com.squareup.retrofit2:retrofit:2.0.2'
compile 'com.squareup.retrofit2:converter-gson:2.0.2'
compile 'com.squareup.okhttp3:okhttp:3.3.0'
compile 'com.squareup.okio:okio:1.7.0'
 compile 'com.squareup.okhttp3:logging-interceptor:3.2.0'
compile 'io.reactivex:rxandroid:1.1.0'
compile 'io.reactivex:rxjava:1.1.0'
compile 'com.android.support:recyclerview-v7:23.4.0'

兩個retrofit依賴包,兩個okhttp依賴包,okhttp3:logging-interceptor依賴包主要是攔截請求日志使用,引入下面要使用的rxandroid的兩個依賴包reactivex:rxandroid和reactivex:rxjava,recyclerview主要是新聞列表頁要使用的。

基本UI頁面

下面就是寫登錄注冊頁面。
登錄頁面效果如下

這裡寫圖片描述

注冊頁面效果如下

這裡寫圖片描述

新聞頁面布局效果

這裡寫圖片描述

布局代碼後面源碼提供下載,而且比較簡單。

創建Retrofit對象

頁面寫好了。下面我們通過單例形式創建一個Retrofit對象。

public class HRetrofitNetHelper{
    public static HRetrofitNetHelper mInstance;
    public Retrofit mRetrofit;
    //本地ip為192.168.1.103
    public static final String BASE_URL = "http://192.168.1.103:8080/GoachWeb/";
    private HRetrofitNetHelper(){
        mRetrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .build();
    }
    public static HRetrofitNetHelper getInstance(){
        if(mInstance==null){
            synchronized (HRetrofitNetHelper.class){
                if(mInstance==null)
                    mInstance = new HRetrofitNetHelper ();
            }
        }
        return mInstance ;
    }
}

簡單的創建好了一個Retrofit。這裡只是配置了一個接口的baseUrl,也就是根路徑。

配置ConverterFactory

如果要Retrofit直接將json轉換為為Dao對象。那麼我們就要通過addConverterFactory來配置,如下:

 mRetrofit = new Retrofit.Builder()
                .addConverterFactory(GsonConverterFactory.create())
                .build();

上面是使用依賴:

compile'com.squareup.retrofit2:converter-gson:2.0.2'

包。然後addConverterFactory來配置。通過源碼方法

addConverterFactory(Converter.Factory factory)

我們可以看到要傳入一個繼承Converter.Factory的對象。Retrofit裡面就有這樣的對象,這裡我們用的是Gson來進行解析,那就有對應的GsonConverterFactory。那好下面就來創建這個對象

創建這個對象有兩種方式

一種是像上面寫的一樣
GsonConverterFactory.create()

這種方式就是簡單的創建默認的Gson對象,然後像我們平常一樣轉換為Dao對象。

還有一種方式就是通過GsonBuilder創建Gson對象,比如這裡統一把後台提供的帶有yyyy-MM-dd HH:mm:ss格式的Date對象,客戶端如果用上面這種方式創建的話,會報下面這個錯
java.text.ParseException: Failed to parse date ["2016-06-11 20:57:28']: Invalid time zone indicator ' ' (at offset 0)

這種情況下,我們就可以這樣:

Gson mGson = new GsonBuilder()
                .setDateFormat("yyyy-MM-dd HH:mm:ss").create();

然後再創建GsonConverterFactory對象的時候傳入Gson

.addConverterFactory(GsonConverterFactory.create(mGson))

就可以很好的解決這個問題了。

這裡只是說了使用Gson進行解析,其實Retrofit還提供了其他的一些解析工具,如下:

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

用法類似這樣
導入包(xx可以指Jackson或者Moshi等等):

compile 'com.squareup.retrofit2:converter-xx:2.0.2'

然後:

.addConverterFactory(xxConverterFactory.create(mGson))

當然,我們還是可以設置多個converter
比如支持 proto 格式和json格式。那麼如下添加:

Retrofit retrofit = new Retrofit.Builder()
      //...
    .addConverterFactory(ProtoConverterFactory.create())
    .addConverterFactory(GsonConverterFactory.create())
    .build();

ProtoConverterFactory和GsonConverterFactory添加 converter 的順序很重要。Retrofit會依次詢問每一個 converter 能否處理一個類型。當Retrofit試圖反序列化一個 proto 格式,它其實會被當做 JSON 來對待。所以Retrofit會先要檢查 proto buffer 格式,然後才是 JSON。所以要先添加ProtoConverterFactory,然後是GsonConverterFactory。

又比如我們需要Retrofit支持RxJava。添加:

.addCallAdapterFactory(RxJavaCallAdapterFactory.create())

就好了。

配置HttpLoggingInterceptor

Retrofit還可以添加OkHttpClient對象。比如我們可以添加一個攔截器來監聽每次請求體:
依賴的包

compile'com.squareup.okhttp3:logging-interceptor:3.2.0'
 HttpLoggingInterceptor  interceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
             @Override
             public void log(String message) {
                 Log.d("zgx", "OkHttp====message " + message);
             }
         });
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);         

創建好後,然後通過retrofit對象添加client,如下:

mRetrofit = new Retrofit.Builder()
                //...
                .client(mOkHttpClient)
                .build();

這樣我們就通過HttpLoggingInterceptor 攔截器可以獲取道http請求體,可以獲取我們請求方式,請求的參數,然後的json數據。這裡以登錄接口為例,如下:

06-11 22:16:11.064 31186-8789/com.goach.client D/zgx: OkHttp====message --> POST http://192.168.1.102:8080/GoachWeb/LoginDataServlet http/1.1
06-11 22:16:11.064 31186-8789/com.goach.client D/zgx: OkHttp====message Content-Type: application/x-www-form-urlencoded
06-11 22:16:11.068 31186-8789/com.goach.client D/zgx: OkHttp====message Content-Length: 30
06-11 22:16:11.068 31186-8789/com.goach.client D/zgx: OkHttp====message 
06-11 22:16:11.068 31186-8789/com.goach.client D/zgx: OkHttp====message username=Goach&password=123456
06-11 22:16:11.068 31186-8789/com.goach.client D/zgx: OkHttp====message --> END POST (30-byte body)
06-11 22:16:12.376 31186-8789/com.goach.client D/zgx: OkHttp====message <-- 200 OK http://192.168.1.102:8080/GoachWeb/LoginDataServlet (1308ms)
06-11 22:16:12.376 31186-8789/com.goach.client D/zgx: OkHttp====message Server: Apache-Coyote/1.1
06-11 22:16:12.376 31186-8789/com.goach.client D/zgx: OkHttp====message Content-Type: text/plain;charset=UTF-8
06-11 22:16:12.376 31186-8789/com.goach.client D/zgx: OkHttp====message Transfer-Encoding: chunked
06-11 22:16:12.376 31186-8789/com.goach.client D/zgx: OkHttp====message Date: Sat, 11 Jun 2016 14:15:19 GMT
06-11 22:16:12.384 31186-8789/com.goach.client D/zgx: OkHttp====message 
06-11 22:16:12.384 31186-8789/com.goach.client D/zgx: OkHttp====message {"errorCode":1,"userId":1000000,"responseTime":"2016-06-11 22:15:19","resultCode":200}
06-11 22:16:12.384 31186-8789/com.goach.client D/zgx: OkHttp====message <-- END HTTP (86-byte body)

既然能用OkHttp的攔截機制,那麼我們就可以在RequestBody 裡面添加基本參數

配置基本提交參數

我們可以再新建一個攔截器,這裡我舉例加些簡單的系統參數,如下:

        class HttpBaseParamsLoggingInterceptor implements Interceptor{

        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            Request.Builder requestBuilder = request.newBuilder();
            RequestBody formBody = new FormBody.Builder()
            .add("userId", "10000")
            .add("sessionToken", "E34343RDFDRGRT43RFERGFRE")
            .add("q_version", "1.1")
            .add("device_id", "android-344365")
            .add("device_os", "android")
            .add("device_osversion","6.0")
            .add("req_timestamp", System.currentTimeMillis() + "")
            .add("app_name","forums")
            .add("sign", "md5")
            .build();
            String postBodyString = Utils.bodyToString(request.body());
            postBodyString += ((postBodyString.length() > 0) ? "&" : "") +  Utils.bodyToString(formBody);
            request = requestBuilder
                    .post(RequestBody.create(MediaType.parse("application/x-www-form-urlencoded;charset=UTF-8"),
                            postBodyString))
                    .build();
            return chain.proceed(request);
        }
    }

上面Utils類是使用的okio.Buffer裡面的工具類。通過RequestBody構建要上傳的一些基本公共的參數,然後通過”&”符號在http 的body裡面其他要提交參數拼接。然後再通過requestBuilder重新創建request對象,然後再通過chain.proceed(request)返回Response 。

接下來在創建OkHttpClient對象的時候修改為如下代碼:

    mOkHttpClient = new OkHttpClient.Builder()
     .addInterceptor(interceptor)
     .addInterceptor(new HttpBaseParamsLoggingInterceptor())
     .build();

這樣就添加好了一些基本的公共參數。

當然。我們也可以直接借助github 上的BasicParamsInterceptor。代碼如下:

public class BasicParamsInterceptor implements Interceptor {

    Map queryParamsMap = new HashMap<>();
    Map paramsMap = new HashMap<>();
    Map headerParamsMap = new HashMap<>();
    List headerLinesList = new ArrayList<>();

    private BasicParamsInterceptor() {

    }

    @Override
    public Response intercept(Chain chain) throws IOException {

        Request request = chain.request();
        Request.Builder requestBuilder = request.newBuilder();

        // process header params inject
        Headers.Builder headerBuilder = request.headers().newBuilder();
        if (headerParamsMap.size() > 0) {
            Iterator iterator = headerParamsMap.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry entry = (Map.Entry) iterator.next();
                headerBuilder.add((String) entry.getKey(), (String) entry.getValue());
            }
        }

        if (headerLinesList.size() > 0) {
            for (String line: headerLinesList) {
                headerBuilder.add(line);
            }
        }

        requestBuilder.headers(headerBuilder.build());
        // process header params end




        // process queryParams inject whatever it's GET or POST
        if (queryParamsMap.size() > 0) {
            injectParamsIntoUrl(request, requestBuilder, queryParamsMap);
        }
        // process header params end




        // process post body inject
        if (request.method().equals("POST") && request.body().contentType().subtype().equals("x-www-form-urlencoded")) {
            FormBody.Builder formBodyBuilder = new FormBody.Builder();
            if (paramsMap.size() > 0) {
                Iterator iterator = paramsMap.entrySet().iterator();
                while (iterator.hasNext()) {
                    Map.Entry entry = (Map.Entry) iterator.next();
                    formBodyBuilder.add((String) entry.getKey(), (String) entry.getValue());
                }
            }
            RequestBody formBody = formBodyBuilder.build();
            String postBodyString = bodyToString(request.body());
            postBodyString += ((postBodyString.length() > 0) ? "&" : "") +  bodyToString(formBody);
            requestBuilder.post(RequestBody.create(MediaType.parse("application/x-www-form-urlencoded;charset=UTF-8"), postBodyString));
        } else {    // can't inject into body, then inject into url
            injectParamsIntoUrl(request, requestBuilder, paramsMap);
        }

        request = requestBuilder.build();
        return chain.proceed(request);
    }

    // func to inject params into url
    private void injectParamsIntoUrl(Request request, Request.Builder requestBuilder, Map paramsMap) {
        HttpUrl.Builder httpUrlBuilder = request.url().newBuilder();
        if (paramsMap.size() > 0) {
            Iterator iterator = paramsMap.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry entry = (Map.Entry) iterator.next();
                httpUrlBuilder.addQueryParameter((String) entry.getKey(), (String) entry.getValue());
            }
        }

        requestBuilder.url(httpUrlBuilder.build());
    }

    private static String bodyToString(final RequestBody request){
        try {
            final RequestBody copy = request;
            final Buffer buffer = new Buffer();
            if(copy != null)
                copy.writeTo(buffer);
            else
                return "";
            return buffer.readUtf8();
        }
        catch (final IOException e) {
            return "did not work";
        }
    }

    public static class Builder {

        BasicParamsInterceptor interceptor;

        public Builder() {
            interceptor = new BasicParamsInterceptor();
        }

        public Builder addParam(String key, String value) {
            interceptor.paramsMap.put(key, value);
            return this;
        }

        public Builder addParamsMap(Map paramsMap) {
            interceptor.paramsMap.putAll(paramsMap);
            return this;
        }

        public Builder addHeaderParam(String key, String value) {
            interceptor.headerParamsMap.put(key, value);
            return this;
        }

        public Builder addHeaderParamsMap(Map headerParamsMap) {
            interceptor.headerParamsMap.putAll(headerParamsMap);
            return this;
        }

        public Builder addHeaderLine(String headerLine) {
            int index = headerLine.indexOf(":");
            if (index == -1) {
                throw new IllegalArgumentException("Unexpected header: " + headerLine);
            }
            interceptor.headerLinesList.add(headerLine);
            return this;
        }

        public Builder addHeaderLinesList(List headerLinesList) {
            for (String headerLine: headerLinesList) {
                int index = headerLine.indexOf(":");
                if (index == -1) {
                    throw new IllegalArgumentException("Unexpected header: " + headerLine);
                }
                interceptor.headerLinesList.add(headerLine);
            }
            return this;
        }

        public Builder addQueryParam(String key, String value) {
            interceptor.queryParamsMap.put(key, value);
            return this;
        }

        public Builder addQueryParamsMap(Map queryParamsMap) {
            interceptor.queryParamsMap.putAll(queryParamsMap);
            return this;
        }

        public BasicParamsInterceptor build() {
            return interceptor;
        }

    }
}

我們只要向上面一樣配置就行了。

其實攔截器還能做很多事。比如在開發中,我們會遇到,我們去請求某些接口的時候,服務端會直接返回一個信息給客戶端,讓客戶端去Toast提示。下面,我就以只要是請求登錄接口就給個提示框為例

Rxandroid的使用和特殊Url請求攔截處理

還是會用到攔截器,要知道,攔截器接口實現的intercept這個方法可不是在ui線程裡面執行的,所以這裡彈Toast,我們用RxAndroid實現再好不過了。
既然要用到RxAndroid,那就需要再依賴兩個包:

compile 'io.reactivex:rxandroid:1.1.0'
compile 'io.reactivex:rxjava:1.1.0'

依賴好了,下面就可以在OkHttpClient創建的時候再添加一個攔截器mUrlInterceptor,代碼如下,

 @Override
    public okhttp3.Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        okhttp3.Response response = chain.proceed(request);
        String requestUrl = response.request().url().uri().getPath();
        if(!TextUtils.isEmpty(requestUrl)){
            if(requestUrl.contains("LoginDataServlet")) {
                if (Looper.myLooper() == null) {
                    Looper.prepare();
                }
                createObservable("現在請求的是登錄接口");
            }
        }
        return response;
    }

然後再上面OkHttp創建的時候修改下:

mOkHttpClient = new OkHttpClient.Builder()
                //..前面兩個攔截器省略
                .addInterceptor(mUrlInterceptor)
                .build();

說下上面intercept裡面的,注意在createObservable方法調用前,要先Looper.prepare()下,否則會報錯提示你要先調用Looper.prepare()方法下。其他的代碼應該理解沒什麼問題了。我們知道RxAndroid兩個核心就是Observable事件被觀察者,然後就是subscribe事件訂閱者,可以理解為觀察者模式,但是它和觀察者模式又有不同的地方,就是當事件被觀察者沒有關注者的時候,事件不會發送出去。詳細就不講解了。我這裡只是彈個Toast,不用那麼復雜。代碼如下:

private void createObservable(String msg){
        Observable.just(msg).map(new Func1() {
            @Override
            public String call(String s) {
                return s;
            }
        })
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(onNextAction);
    }

通過Func1,直接發送一條消息給訂閱者,發送完後這個事件就結束了。

.observeOn(AndroidSchedulers.mainThread())

的作用就是把訂閱者處理事件發送給ui線程去處理。

接下來訂閱者,就簡單的用onNextAction實現了。

 private void createSubscriberByAction() {
        onNextAction = new Action1() {
            @Override
            public void call(String s) {
                Log.d("zgx","s=========="+s);
                Toast.makeText(mContext,s, Toast.LENGTH_SHORT).show();
            }
        };
    }

createSubscriberByAction方法在HRetrofitNetHelper對象構造器裡面調用就好了。

  private HRetrofitNetHelper(Context context){
       //...
        createSubscriberByAction();
        //...
 }

這樣就實現了,上面提的需求。

緩存

配置了這麼多,接下來肯定又會想到緩存問題還沒有處理呢。那麼,接下來就來說下緩存處理了。

寫之前,先看下源碼裡面注釋的一段話

 if (!requestMethod.equals("GET")) {
      // Don't cache non-GET responses. We're technically allowed to cache
      // HEAD requests and some POST requests, but the complexity of doing
      // so is high and the benefit is low.
      return null;
    }

看懂了吧,OkHttp建議在不是Get請求的響應體不要緩存,因為如果緩存的話會提高它的復雜性而且好處不大。
沒看到這段話之前。郁悶了很久為什麼Post請求緩存生成不了,而且會報一個錯

504 Unsatisfiable Request (only-if-cached)

這個錯的意思就是只去讀緩存,但是緩存不存在,所以就會報錯了。但是我覺得有時候Post請求緩存的需求還是會有的,比如有時候在應用中經常想在沒網的情況下緩存這個頁面,而這個頁面的請求接口也是post請求。所以還是要有緩存更好,比如volley框架就可以緩存整個頁面,但是也是要改下volley的代碼。目前還不知道怎麼去緩存post請求。目前github上有RxCache,或者是通過Sqlite自己實現緩存都有,沒有仔細研究,後面有時間在看。
下面就來看下實現代碼

創建局部變量Cache,以及兩個Get方法,一個獲取Cache對象,一個清除Cache緩存。
private final Cache cache;
 public Cache getCache(){
        return cache;
    }
    public void clearCache() throws IOException {
        cache.delete();
    }
配置OkHttp緩存
File cacheFile = new File(context.getCacheDir(), "HttpCache");
cache = new Cache(cacheFile, 1024 * 1024 * 100); //100Mb
mOkHttpClient = new OkHttpClient.Builder()
                //...
                .cache(cache)
                .build();

官方建議緩存路徑寫在context.getCacheDir()裡面,也就是在/data/data/com.goach.client/cache/HttpCache裡面。這樣配置好了,如果雲端通過http的header裡面Cache-Control做了緩存。那麼這樣就緩存完了。但是如果雲端沒有做了,那麼我們客戶端也可以自己通過Interceptor實現。這裡我就把緩存邏輯寫在上面的mUrlInterceptor攔截器裡面了。修改如下

    @Override
    public okhttp3.Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        //緩存
        if(NetUtil.checkNetwork(mContext)==NetUtil.NO_NETWORK){
            request = request.newBuilder()
                    .cacheControl(CacheControl.FORCE_CACHE)
                    .build();
            Log.d("zgx","no network");
        }

        okhttp3.Response response = chain.proceed(request);
        String requestUrl = response.request().url().uri().getPath();
        if(!TextUtils.isEmpty(requestUrl)){
            if(requestUrl.contains("LoginDataServlet")) {
                if (Looper.myLooper() == null) {
                    Looper.prepare();
                }
                createObservable("現在請求的是登錄接口");
            }
        }
        //緩存響應
        if(NetUtil.checkNetwork(mContext)!=NetUtil.NO_NETWORK){
            //有網的時候讀接口上的@Headers裡的配置,你可以在這裡進行統一的設置
            String cacheControl = request.cacheControl().toString();
            Log.d("zgx","cacheControl====="+cacheControl);
            return response.newBuilder()
                    .header("Cache-Control", cacheControl)
                    //http1.0的舊東西,優先級比Cache-Control低
                    .removeHeader("Pragma")
                    .build();
        }else{
            return response.newBuilder()
                    .header("Cache-Control", "public, only-if-cached, max-stale=30*24*60*60")
                    .removeHeader("Pragma")
                    .build();
        }
    }

沒網的情況下Request 直接從緩存裡面讀取,響應體增加header的Cache-Control,緩存30天,有網的情況下,Request 就會去請求服務器,然後響應體就會去都Retrofit框架裡面的@Header配置,如果沒有配置,就沒不緩存,如果配置了就可以進行緩存。到這裡,當我們去Get請求的時候,就會生成緩存

這裡寫圖片描述

這裡寫圖片描述

我這裡是通過模擬器看到,真機裡面是看不到的。打開可以看到我們請求信息。

超時

okhttp如果沒有配置默認是10s,錯誤信息如下

onFailure======java.net.SocketTimeoutException: failed to connect to /192.168.1.101 (port 8080) after 10000ms

配置

 mOkHttpClient = new OkHttpClient.Builder()
                .connectTimeout(12, TimeUnit.SECONDS)
               //...
                .build();

後,錯誤信息如下

 onFailure======java.net.SocketTimeoutException: failed to connect to /192.168.1.101 (port 8080) after 12000ms

還可以配置

 .writeTimeout(20, TimeUnit.SECONDS)
                .readTimeout(20, TimeUnit.SECONDS)
                .retryOnConnectionFailure(true)

沒毛病,應該看的懂。
這樣Retrofit創建基本的配置就完成了,最後結合上面總結後整個配置類的代碼:

public class HRetrofitNetHelper implements HttpLoggingInterceptor.Logger,Interceptor {
    //HRetrofitNetHelper 實現單例
    public static HRetrofitNetHelper mInstance;
    //緩存對象
    private final Cache cache;
    public Retrofit mRetrofit;
    public OkHttpClient mOkHttpClient;
    //請求日志攔截器
    public HttpLoggingInterceptor mHttpLogInterceptor;
     //基本參數攔截器
    private BasicParamsInterceptor mBaseParamsInterceptor;
    //緩存和特殊Url攔截處理攔截器
    private Interceptor  mUrlInterceptor;
    private Context mContext;
    //Date對象傳遞
    public Gson mGson;
    //接口baseurl
    public static final String BASE_URL = "http://192.168.1.101:8080/GoachWeb/";
    private Action1 onNextAction;
    private HRetrofitNetHelper(Context context){
        this.mContext = context ;
        //提供Action,供特殊Url攔截然後Toast
        createSubscriberByAction();
        //yyyy-MM-dd HH:mm:ss的時間格式,可以轉換為Date對象
        mGson = new GsonBuilder()
                .setDateFormat("yyyy-MM-dd HH:mm:ss")
                .create();
        mHttpLogInterceptor = new HttpLoggingInterceptor(this);
         //打印http的body體
        mHttpLogInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        //基本參數
        Map tempParams = getBaseParams(context);
        mBaseParamsInterceptor = new BasicParamsInterceptor.Builder()
                .addParamsMap(tempParams)
                .build();
        mUrlInterceptor = this;
        //創建緩存路徑
        File cacheFile = new File(context.getCacheDir(), "HttpCache");
        Log.d("zgx","cacheFile====="+cacheFile.getAbsolutePath());
        cache = new Cache(cacheFile, 1024 * 1024 * 100); //100Mb
        mOkHttpClient = new OkHttpClient.Builder()
                .connectTimeout(12, TimeUnit.SECONDS)
                .writeTimeout(20, TimeUnit.SECONDS)
                .readTimeout(20, TimeUnit.SECONDS)
                .retryOnConnectionFailure(true)
                .addInterceptor(mHttpLogInterceptor)
                .addInterceptor(mBaseParamsInterceptor)
                .addInterceptor(mUrlInterceptor)
                .cache(cache)
                .build();
        mRetrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create(mGson))
                .client(mOkHttpClient)
                .build();
    }
    public static HRetrofitNetHelper getInstance(Context context){
        if(mInstance==null){
            synchronized (HRetrofitNetHelper.class){
                if(mInstance==null){
                    mInstance =  new HRetrofitNetHelper(context);
                }
            }
        }
        return mInstance;
    }
     //獲取相應的APIService對象
    public  T getAPIService(Class service) {
        return mRetrofit.create(service);
    }
    //異步callback,對一些特殊response邏輯處理
    public  void enqueueCall(Call> call,final RetrofitCallBack retrofitCallBack){
        call.enqueue(new Callback>() {
            @Override
            public void onResponse(Call> call, Response> response) {
                BaseResp resp = response.body() ;
                if (resp == null) {
                    Toast.makeText(mContext, "暫時沒有最新數據!", Toast.LENGTH_SHORT).show();
                    return;
                }
                if (resp.getResultCode() == 2000 || resp.getResultCode() == 2001 || resp.getResultCode() == 2002) {
                   Toast.makeText(mContext,"code====="+resp.getResultCode(),Toast.LENGTH_SHORT).show();
                }
                if (resp.getResultCode() == 200) {
                    if(retrofitCallBack!=null)
                    retrofitCallBack.onSuccess(resp);
                } else {
                   // ToastMaker.makeToast(mContext, resp.errMsg, Toast.LENGTH_SHORT);
                    if(retrofitCallBack!=null)
                        retrofitCallBack.onFailure(resp.getErrMsg());
                }
            }

            @Override
            public void onFailure(Call> call, Throwable t) {
             //   ToastMaker.makeToast(mContext, "網絡錯誤,請重試!", Toast.LENGTH_SHORT);
                if(retrofitCallBack!=null){
                    retrofitCallBack.onFailure(t.toString());
                }
            }
        });
    }
    @Override
    public void log(String message) {
        Log.d("zgx","OkHttp: " + message);
    }
    //提供一些常用的基本參數
    public Map getBaseParams(Context context){
        Map params = new HashMap<>();
        params.put("userId", "324353");
        params.put("sessionToken", "434334");
        params.put("q_version", "1.1");
        params.put("device_id", "android7.0");
        params.put("device_os", "android");
        params.put("device_type", "android");
        params.put("device_osversion", "android");
        params.put("req_timestamp", System.currentTimeMillis() + "");
        params.put("app_name","forums");
        String sign = makeSign(params);
        params.put("sign", sign);
        return params ;
    }
    public String makeSign(Map params) {
        final String signSalt = "fe#%d8ec93a1159a2a3";
        TreeMap sorted = new TreeMap();
        for (Map.Entry kv : params.entrySet()) {
            sorted.put(kv.getKey(), kv.getValue());
        }
        StringBuilder sb = new StringBuilder(signSalt);
        for (String key : sorted.keySet()) {
            if (!"sign".equals(key) && !key.startsWith("file_")) {
                sb.append(key).append(sorted.get(key));
            }
        }
        sb.append(signSalt);
        return MD5.md5(sb.toString()).toUpperCase();
    }
    @Override
    public okhttp3.Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        //緩存
        if(NetUtil.checkNetwork(mContext)==NetUtil.NO_NETWORK){
            request = request.newBuilder()
                    .cacheControl(CacheControl.FORCE_CACHE)
                    .build();
            Log.d("zgx","no network");
        }

        okhttp3.Response response = chain.proceed(request);
        String requestUrl = response.request().url().uri().getPath();
        if(!TextUtils.isEmpty(requestUrl)){
            if(requestUrl.contains("LoginDataServlet")) {
                if (Looper.myLooper() == null) {
                    Looper.prepare();
                }
                createObservable("現在請求的是登錄接口");
            }
        }
        //緩存響應
        if(NetUtil.checkNetwork(mContext)!=NetUtil.NO_NETWORK){
            //有網的時候讀接口上的@Headers裡的配置,你可以在這裡進行統一的設置
            String cacheControl = request.cacheControl().toString();
            Log.d("zgx","cacheControl====="+cacheControl);
            return response.newBuilder()
                    .header("Cache-Control", cacheControl)
                    .removeHeader("Pragma")
                    .build();
        }else{
            return response.newBuilder()
                    .header("Cache-Control", "public, only-if-cached, max-stale=120")
                    .removeHeader("Pragma")
                    .build();
        }
    }
     //異步特殊處理後回調
    public interface RetrofitCallBack{
        void onSuccess(BaseResp baseResp);
        void onFailure(String error);
    }
    private void createSubscriberByAction() {
        onNextAction = new Action1() {
            @Override
            public void call(String s) {
                Log.d("zgx","s=========="+s);
                Toast.makeText(mContext,s, Toast.LENGTH_SHORT).show();
            }
        };
    }
     //創建事件源
    private void createObservable(String msg){
        Observable.just(msg).map(new Func1() {
            @Override
            public String call(String s) {
                return s;
            }
        })
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(onNextAction);
    }
    public Cache getCache(){
        return cache;
    }
    public void clearCache() throws IOException {
        cache.delete();
    }
}

Service之請求API

定義上面的3個請求接口API,為了驗證緩存,都是Get請求。

public interface ILoginService {
    @GET("LoginDataServlet")
    @Headers("Cache-Control: public, max-age=30")
    Call> userLogin(@Query("username") String username, @Query("password") String password);
}
public interface INewsService {
    @GET("NewsDataServlet")
    @Headers("Cache-Control: public, max-age=30")
    Call> userNews(@Query("userId") String userId);
}
public interface IRegisterService {
    @FormUrlEncoded
    @POST("RegisterDataServlet")
    Call createUser(@FieldMap Map params);
}

其中Get請求,使用@GET和@Query或者@QueryMap的結合,Post請求@FormUrlEncoded、@POST和@Field或者@FieldMap的結合。又或者url中通過@Path動態添加參數。比如

public interface INewsService
{  
    @GET("NewsDataServlet/currentPage={currentPage}")  
    Call> getUser(@Path("currentPage") String currentPage);  
}  

還有通過@Multipart 實現文件上傳等等,詳細可以看鴻洋大神的 Retrofit2 完全解析 探索與okhttp之間的關系

HRetrofitNetHelper的使用以及Activity相關代碼

BaseActivity

public abstract class BaseActivity extends AppCompatActivity{
    public HRetrofitNetHelper retrofitNetHelper;
    public LayoutInflater mInflater;
    public ProgressDialog mDialog;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        super.setContentView(layoutResID);
        mInflater = LayoutInflater.from(this);
        setContentView(mInflater.inflate(layoutResID,null));
    }

    @Override
    public void setContentView(View view) {
        super.setContentView(view);
        retrofitNetHelper = HRetrofitNetHelper.getInstance(BaseActivity.this);
        mDialog = new ProgressDialog(BaseActivity.this);
    }
}

LonigActicity

public class LoginActivity extends BaseActivity implements View.OnClickListener,HRetrofitNetHelper.RetrofitCallBack {
    private AutoCompleteTextView mEmailView;
    private EditText mPasswordView;
    private View mLoginFormView;
    private Button mSignInButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        mEmailView = (AutoCompleteTextView) findViewById(R.id.email);
        mPasswordView = (EditText) findViewById(R.id.password);
        mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) {
                if (id == R.id.login || id == EditorInfo.IME_NULL) {
                    return true;
                }
                return false;
            }
        });

        mSignInButton = (Button) findViewById(R.id.sign_in_button);
        mSignInButton.setOnClickListener(this);
        mLoginFormView = findViewById(R.id.login_form);
    }
    public void startRegister(View view){
        Intent intent = new Intent(LoginActivity.this,RegisterActivity.class);
        startActivity(intent);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.sign_in_button:
                mDialog.setMessage("正在登錄中,請稍後...");
                mDialog.show();
                ILoginService loginService = retrofitNetHelper.getAPIService(ILoginService.class);
                String username = mEmailView.getText().toString();
                String password = mPasswordView.getText().toString();
                if(!TextUtils.isEmpty(username)&&!TextUtils.isEmpty(password)){
                    final Call> repos = loginService.userLogin(username,password);
                    retrofitNetHelper.enqueueCall(repos,this);
                }
                break;
        }
    }
    @Override
    public void onSuccess(BaseResp baseResp) {
        Log.d("zgx","onResponse======"+baseResp.getData().getErrorCode());
        Date date = baseResp.getResponseTime();
        Log.d("zgx","RegisterBean======"+date);
        if(baseResp.getData().getErrorCode()==1){
            Intent intent = new Intent(LoginActivity.this, NewsActivity.class);
            intent.putExtra("intent_user_id",String.valueOf(baseResp.getData().getUserId()));
            startActivity(intent);
            Toast.makeText(getBaseContext(),"登錄成功",Toast.LENGTH_SHORT).show();
        }else {
            Toast.makeText(getBaseContext(),"用戶不存在",Toast.LENGTH_SHORT).show();
        }
        mDialog.dismiss();
    }

    @Override
    public void onFailure(String error) {
        Log.d("zgx","onFailure======"+error);
        mDialog.dismiss();
    }
}

RegisterActivity

public class RegisterActivity extends BaseActivity implements Callback {
    private AutoCompleteTextView mUserName;
    private EditText mPasswordEditText;
    private EditText mConfirmationEditText;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_register);
        mUserName = (AutoCompleteTextView) findViewById(R.id.id_username);
        mPasswordEditText = (EditText)findViewById(R.id.password);
        mConfirmationEditText = (EditText)findViewById(R.id.confirmation_password);
    }
    public void startRegister(View view){
        String userName = mUserName.getText().toString();
        String password = mPasswordEditText.getText().toString();
        String mConfirmation = mConfirmationEditText.getText().toString();
        if(!TextUtils.isEmpty(userName)&&!TextUtils.isEmpty(password)
                &&!TextUtils.isEmpty(mConfirmation)){
            if(password.equals(mConfirmation)){
                IRegisterService loginService = retrofitNetHelper.getAPIService(IRegisterService.class);
                Map mParamsMap = new HashMap<>();
                mParamsMap.put("username",userName);
                mParamsMap.put("password",password);
               Call call =  loginService.createUser(mParamsMap);
                call.enqueue(this);
            }else {
                Toast.makeText(getBaseContext(),"密碼不一致",Toast.LENGTH_SHORT).show();
            }
        }else {
            Toast.makeText(getBaseContext(),"請填寫完整",Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    public void onResponse(Call call, Response response) {
        if(response.body().getErrorCode()==1){
            Intent intent = new Intent(RegisterActivity.this, LoginActivity.class);
            startActivity(intent);
        }else{
            Toast.makeText(getBaseContext(),"注冊失敗",Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    public void onFailure(Call call, Throwable t) {

    }
}

NewsActivity

public class NewsActivity extends BaseActivity implements HRetrofitNetHelper.RetrofitCallBack{
    private String mUserId;
    private RecyclerView mRecyclerView;
    private NewsAdapter mNewsAdapter;
    private List mDataList;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_news);
        mUserId = getIntent().getStringExtra("intent_user_id");
        mDataList = new ArrayList<>();
        mRecyclerView = (RecyclerView)findViewById(R.id.id_news_recycler_view);
        LinearLayoutManager manager = new LinearLayoutManager(NewsActivity.this);
        mRecyclerView.setLayoutManager(manager);
        mNewsAdapter = new NewsAdapter(NewsActivity.this,mDataList);
        mRecyclerView.setAdapter(mNewsAdapter);
        loadData();
    }
    private void loadData(){
        mDialog.setMessage("正在加載中,請稍後...");
        mDialog.show();
        INewsService newService = retrofitNetHelper.getAPIService(INewsService.class);
        Log.d("zgx","mUserId====="+mUserId);
            final Call> repos = newService.userNews(mUserId);
            retrofitNetHelper.enqueueCall(repos,this);
    }

    @Override
    public void onSuccess(BaseResp baseResp) {
        mDialog.dismiss();
        mDataList.clear();
        mDataList.addAll(baseResp.getData().getNewsItem());
        mNewsAdapter.notifyDataSetChanged();
    }

    @Override
    public void onFailure(String error) {
        mDialog.dismiss();
        Toast.makeText(NewsActivity.this,"請求出現異常"+error,Toast.LENGTH_SHORT).show();
    }
}

其他一些幫助類,後面提供源碼下載。

最後來看下實現的效果
這裡寫圖片描述

緩存效果沒有錄制,博客上傳文件有限。


說到這裡,其實還有很多地方還要去學習,比如RxCache框架,通過Retrofit和Rxandroid真正的結合實現緩存處理,比如文件的上傳下載等等。這些後面有時間再學習了。

只是提供服務端和客戶端的源碼,數據庫表和環境搭建配置就不提供了。
使用的環境為:
Android studio 2.1.2
MyEclipse 2014GA
Tomcat8.0
JDK8.0
MySQL Server 5.7
Navicat for MySQL

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved