使用Retrofit+RxJava實現網路請求

JYcoder發表於2018-03-31

安卓基礎開發庫,讓開發簡單點。
DevRing & Demo地址github.com/LJYcoder/De…

學習/參考地址:
Retrofit:
整體教程 http://blog.csdn.net/jdsjlzx/article/details/52015347
檔案上傳 http://blog.csdn.net/jdsjlzx/article/details/52246114
檔案下載 http://www.jianshu.com/p/060d55fc1c82
Https請求 http://blog.csdn.net/dd864140130/article/details/52625666
異常處理 http://blog.csdn.net/mq2553299/article/details/70244529
失敗重試 http://blog.csdn.net/johnny901114/article/details/51539708
生命週期 http://android.jobbole.com/83847 | http://mp.weixin.qq.com/s/eedFDMIQe30rQmryLeif_Q

RxJava:
整體教程(RxJava1) https://gank.io/post/560e15be2dca930e00da1083
整體教程(RxJava2) https://mp.weixin.qq.com/s/UAEgdC2EtqSpEqvog0aoZQ
操作符 https://zhuanlan.zhihu.com/p/21926591
使用場景 http://blog.csdn.net/theone10211024/article/details/50435325
1.x與2.x區別 http://blog.csdn.net/qq_35064774/article/details/53045298

前言

Retrofit是目前主流的網路請求框架,功能強大,操作便捷。
RxJava是實現非同步操作的庫。可線上程間快速切換,同時提供許多操作符,使一些複雜的操作程式碼變得清晰有條理。
兩者結合使用後,使得網路請求更加簡潔,尤其在巢狀請求等特殊場景大有作為。

本文側重於介紹Retrofit網路請求,以及它是如何結合RxJava使用的。還沒了解過RxJava的建議先到上面貼出的參考地址學習,以便更好明白兩者結合的過程。

文章篇幅較長,因為希望儘可能涵蓋常用、實用的模組。

demo以及文章中的RxJava部分,已從1.x更新到2.x。


介紹

下面通過配置,請求,異常處理,生命週期管理,失敗重試,封裝,混淆這幾個部分來介紹。

1. 配置

1.1 新增依賴

//Rxjava
compile 'io.reactivex.rxjava2:rxjava:2.1.6'
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
//Retrofit
compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
compile 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
compile 'com.squareup.okhttp3:logging-interceptor:3.8.0'
複製程式碼

1.2 開啟Log日誌

OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();
//啟用Log日誌
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
okHttpClientBuilder.addInterceptor(loggingInterceptor);
複製程式碼

開啟後,則可以在Log日誌中看到網路請求相關的資訊了,如請求地址,請求狀態碼,返回的結果等。

Log日誌截圖

1.3 開啟Gson轉換

Retrofit.Builder retrofitBuilder = new Retrofit.Builder();
//配置轉化庫,採用Gson
retrofitBuilder.addConverterFactory(GsonConverterFactory.create());
複製程式碼

開啟後,會自動把請求返回的結果(json字串)自動轉化成與其結構相符的實體。

1.4 採用Rxjava

Retrofit.Builder retrofitBuilder = new Retrofit.Builder();
//配置回撥庫,採用RxJava
retrofitBuilder.addCallAdapterFactory(RxJava2CallAdapterFactory.create());
複製程式碼

1.5 設定基礎請求路徑BaseUrl

Retrofit.Builder retrofitBuilder = new Retrofit.Builder();
//伺服器地址,基礎請求路徑,最好以"/"結尾
retrofitBuilder.baseUrl("https://api.douban.com/");
複製程式碼

1.6 設定請求超時

OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();
//設定請求超時時長為15秒
okHttpClientBuilder.connectTimeout(15, TimeUnit.SECONDS);
複製程式碼

1.7 設定快取

Interceptor cacheIntercepter=new Interceptor() {
     @Override
     public Response intercept(Chain chain) throws IOException {
         //對request的設定用來指定有網/無網下所走的方式
         //對response的設定用來指定有網/無網下的快取時長

         Request request = chain.request();
         if (!NetworkUtil.isNetWorkAvailable(mContext)) {
             //無網路下強制使用快取,無論快取是否過期,此時該請求實際上不會被髮送出去。
             //有網路時則根據快取時長來決定是否發出請求
             request = request.newBuilder()
             .cacheControl(CacheControl.FORCE_CACHE).build();
         }

         Response response = chain.proceed(request);
         if (NetworkUtil.isNetWorkAvailable(mContext)) {
             //有網路情況下,超過1分鐘,則重新請求,否則直接使用快取資料
             int maxAge = 60; //快取一分鐘
             String cacheControl = "public,max-age=" + maxAge;
             //當然如果你想在有網路的情況下都直接走網路,那麼只需要
             //將其超時時間maxAge設為0即可
              return response.newBuilder()
              .header("Cache-Control",cacheControl)
              .removeHeader("Pragma").build();
         } else {
             //無網路時直接取快取資料,該快取資料儲存1周
             int maxStale = 60 * 60 * 24 * 7 * 1;  //1周
             return response.newBuilder()
             .header("Cache-Control", "public,only-if-cached,max-stale=" + maxStale)
             .removeHeader("Pragma").build();
         }

     }
 };
 
File cacheFile = new File(mContext.getExternalCacheDir(), "HttpCache");//快取地址
Cache cache = new Cache(cacheFile, 1024 * 1024 * 50); //大小50Mb

//設定快取方式、時長、地址
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();
okHttpClientBuilder.addNetworkInterceptor(cacheIntercepter);
okHttpClientBuilder.addInterceptor(cacheIntercepter);
okHttpClientBuilder.cache(cache);
複製程式碼

1.8 設定header

可統一設定

Interceptor headerInterceptor = new Interceptor() {
     @Override
     public Response intercept(Chain chain) throws IOException {
         Request originalRequest = chain.request();
         Request.Builder builder = originalRequest.newBuilder();
         //設定具體的header內容
         builder.header("timestamp", System.currentTimeMillis() + "");

         Request.Builder requestBuilder = 
         builder.method(originalRequest.method(), originalRequest.body());
         Request request = requestBuilder.build();
         return chain.proceed(request);
     }
 };
//設定統一的header
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();
okHttpClientBuilder.addInterceptor(getHeaderInterceptor());
複製程式碼

也可在請求方法中單獨設定

@Headers("Cache-Control: max-age=120")
@GET("請求地址")
Observable<HttpResult> getInfo();

或者

@GET("請求地址")
Observable<HttpResult> getInfo(@Header("token") String token);
複製程式碼

1.9 設定https訪問

現在不少伺服器介面採用了https的形式,所以有時就需要設定https訪問。
下面列舉“客戶端內建證照”時的配置方法,其他方式請參考 http://blog.csdn.net/dd864140130/article/details/52625666

//設定https訪問(驗證證照,請把伺服器給的證照檔案放在R.raw資料夾下)
okHttpClientBuilder.sslSocketFactory(getSSLSocketFactory(mContext, new int[]{R.raw.tomcat}));
okHttpClientBuilder.hostnameVerifier(org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
複製程式碼

getSSLSocketFactory()方法如下:

//設定https證照
protected static SSLSocketFactory getSSLSocketFactory(Context context, int[] certificates) {

    if (context == null) {
        throw new NullPointerException("context == null");
    }

    //CertificateFactory用來證照生成
    CertificateFactory certificateFactory;
    try {
        certificateFactory = CertificateFactory.getInstance("X.509");
        //Create a KeyStore containing our trusted CAs
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null, null);

        for (int i = 0; i < certificates.length; i++) {
            //讀取本地證照
            InputStream is = context.getResources().openRawResource(certificates[i]);
            keyStore.setCertificateEntry(String.valueOf(i), certificateFactory
            .generateCertificate(is));

            if (is != null) {
                is.close();
            }
        }
        
        //Create a TrustManager that trusts the CAs in our keyStore
        TrustManagerFactory trustManagerFactory = TrustManagerFactory
        .getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);

        //Create an SSLContext that uses our TrustManager
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
        return sslContext.getSocketFactory();

    } catch (Exception e) {

    }
    return null;
}
複製程式碼

1.10 綜合前面的配置

OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();
//設定請求超時時長
okHttpClientBuilder.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
//啟用Log日誌
okHttpClientBuilder.addInterceptor(getHttpLoggingInterceptor());
//設定快取方式、時長、地址
okHttpClientBuilder.addNetworkInterceptor(getCacheInterceptor());
okHttpClientBuilder.addInterceptor(getCacheInterceptor());
okHttpClientBuilder.cache(getCache());
//設定https訪問(驗證證照)
okHttpClientBuilder.sslSocketFactory(getSSLSocketFactory(mContext, new int[]{R.raw.tomcat}));
okHttpClientBuilder.hostnameVerifier(org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
//設定統一的header
okHttpClientBuilder.addInterceptor(getHeaderInterceptor());

Retrofit retrofit = new Retrofit.Builder()
           //伺服器地址
           .baseUrl(UrlConstants.HOST_SITE_HTTPS)
           //配置轉化庫,採用Gson
           .addConverterFactory(GsonConverterFactory.create())
           //配置回撥庫,採用RxJava
           .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
           //設定OKHttpClient為網路客戶端
           .client(okHttpClientBuilder.build()).build();
複製程式碼

配置後得到的retrofit變數用於後面發起請求。

2. 請求

2.1 建立API介面

定義一個介面,在其中新增具體的網路請求方法。
請求方法的格式大致如下:

@其他宣告
@請求方式("請求地址")
Observable<請求返回的實體> 請求方法名(請求引數);

或者

@其他宣告
@請求方式
Observable<請求返回的實體> 請求方法名(@Url String 請求地址,請求引數);

第一種格式中的請求地址,填寫基礎請求路徑baseUrl後續的部分即可。
第二種格式中的請求地址,需填寫完整的地址。


下面列舉**Get請求**、**Post請求**、**檔案上傳**、**檔案下載**的介面定義。
其中HttpResult是自定義的、與後臺返回的json資料結構相符的實體。
  • Get請求

請求引數逐個傳入

@GET("v2/movie/in_theaters")
Observable<HttpResult> getPlayingMovie(@Query("start") int start, @Query("count") int count);
複製程式碼

請求引數一次性傳入(通過Map來存放key-value)

@GET("v2/movie/in_theaters")
Observable<HttpResult> getPlayingMovie(@QueryMap Map<String, String> map);
複製程式碼

以上兩種方式,請求引數是以“?key=vale%key=value...”方式拼接到地址後面的,假如你需要的是以"/value"的方式拼接到地址後面(restful模式?),那麼可以這麼寫

@GET("v2/movie/in_theaters/{start}/{count}")
Observable<HttpResult> getPlayingMovie(@Path("start") int start, @Path("count") int count);
複製程式碼
  • Post請求

請求引數逐個傳入

@FormUrlEncoded
@POST("請求地址")
Observable<HttpResult> getInfo(@Field("token") String token, @Field("id") int id);
複製程式碼

請求引數一次性傳入(通過Map來存放引數名和引數值)

@FormUrlEncoded
@POST("請求地址")
Observable<HttpResult> getInfo(@FieldMap Map<String, String> map);
複製程式碼
  • 上傳文字+檔案

1)上傳單個文字和單個檔案

@Multipart
@POST("請求地址")
Observable<HttpResult> upLoadTextAndFile(@Query("textKey") String text, 
				@Part("fileKey\"; filename=\"test.png") RequestBody fileBody);

複製程式碼

第一個引數用於傳文字,

--- @Query("textKey")中的"textKey"為文字引數的引數名。

--- String text 為文字引數的引數值,傳入你要上傳的字串即可。

第二個引數用於傳檔案,

--- @Part("fileKey"; filename="test.png")
其中的"fileKey"為檔案引數的引數名(由伺服器後臺提供)
其中的"test.png"一般是指你希望儲存在伺服器的檔名字,傳入File.getName()即可

--- RequestBody fileBody為檔案引數的引數值,生成方法如下:
RequestBody fileBody = RequestBody.create(MediaType.parse("image/png"), file);
(這裡檔案型別以png圖片為例,所以MediaType為"image/png",
不同檔案型別對應不同的type,具體請參考http://tool.oschina.net/commons)

2)上傳多個文字和多個檔案(通過Map來傳入)

@Multipart
@POST("")
Observable<HttpResult> upLoadTextAndFiles(@QueryMap Map<String, String> textMap, @PartMap Map<String, RequestBody> fileBodyMap);
複製程式碼

第一個引數用於傳文字,

通過Map來存放文字引數的key-value

第二個引數用於傳檔案,

Map的key為String,內容請參考上方“上傳文字和單個檔案”中@Part()裡的值。
Map的value值為RequestBody,內容請參考上方“上傳文字和單個檔案”中RequestBody的生成。

如果上傳報錯,可以嘗試把文字引數前面的註解改為@Field(@FieldMap)或者@Part(@PartMap)看看可否。

  • 下載檔案
//下載大檔案時,請加上@Streaming,否則容易出現IO異常
@Streaming
@GET("請求地址")
Observable<ResponseBody> downloadFile();
//ResponseBody是Retrofit提供的返回實體,要下載的檔案資料將包含在其中
複製程式碼

(目前使用@Streaming進行下載的話,需新增Log攔截器(且LEVEL為BODY)才不會報錯,但是網上又說新增Log攔截器後進行下載容易OOM,
所以這一塊還很懵,具體原因也不清楚,有知道的朋友可以告訴下我)

2.2 發起請求

完成前面說的的配置和請求介面的定義後,就可以發起請求了。

//構建Retrofit類
Retrofit retrofit = new Retrofit.Builder()
	        //伺服器地址
	         .baseUrl("https://api.douban.com/")
	         //配置轉化庫,採用Gson
	         .addConverterFactory(GsonConverterFactory.create())
	         //配置回撥庫,採用RxJava
	         .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
	         //設定OKHttpClient為網路客戶端
	         .client(okHttpClientBuilder.build()).build();

//獲取API介面
mApiService = retrofit.create(ApiService.class);
//呼叫之前定義好的請求方法,得到Observable
Observable observable = mApiService.xxx();

複製程式碼

普通請求、上傳請求:

//通過Observable發起請求
observable
.subscribeOn(Schedulers.io())//指定網路請求在io後臺執行緒中進行
.observeOn(AndroidSchedulers.mainThread())//指定observer回撥在UI主執行緒中進行
.subscribe(observer);//發起請求,請求的結果會回撥到訂閱者observer中

複製程式碼

下載請求:

//通過Observable發起請求
observable
.subscribeOn(Schedulers.io()) //指定網路請求在io後臺執行緒中進行
.observeOn(Schedulers.io()) //指定doOnNext的操作在io後臺執行緒進行
.doOnNext(new Consumer<ResponseBody>() {
           //doOnNext裡的方法執行完畢,observer裡的onNext、onError等方法才會執行。
           @Override
           public void accept(ResponseBody body) throws Exception {
                 //下載檔案,儲存到本地
                 //通過body.byteStream()可以得到輸入流,然後就是常規的IO讀寫儲存了。
                 ...
           }
})
.observeOn(AndroidSchedulers.mainThread()) //指定observer回撥在UI主執行緒中進行
.subscribe(observer); //發起請求,請求的結果先回撥到doOnNext進行處理,再回撥到observer中

複製程式碼

3. 異常處理

使用Retrofit+RxJava發起請求後,如果請求失敗,會回撥observer中的onError方法,該方法的引數為Throwable,並沒能反饋更直接清楚的異常資訊給我們,所以有必要對Throwable異常進行處理轉換。

//observer封裝類中的程式碼

@Override
public void onError(Throwable e) {
    if (e instanceof Exception) {
        //將throwable進行解析處理得到相應的異常資訊(裡面包含了異常碼和異常描述資訊)
        ExceptionHandler.ResponeThrowable responeThrowable = ExceptionHandler.handleException(e);
        onError(responeThrowable.code, responeThrowable.message);
    } else {
        //判定為未知錯誤
        ExceptionHandler.ResponeThrowable responeThrowable = new ExceptionHandler.ResponeThrowable(e, ExceptionHandler.ERROR.UNKNOWN);
        onError(responeThrowable.code, responeThrowable.message);
    }
}

//應用中具體實現的是下面這個onError方法
public abstract void onError(int errType, String errMessage);
複製程式碼
public class ExceptionHandler {
    ....

    public static ResponseThrowable handleException(Throwable e) {
        ResponseThrowable responseThrowable;

        if (e instanceof HttpException) {
            HttpException httpException = (HttpException) e;
            responseThrowable = new ResponseThrowable(e, ERROR.HTTP_ERROR);
            switch (httpException.code()) {
                case UNAUTHORIZED:
                case FORBIDDEN:
                case NOT_FOUND:
                case REQUEST_TIMEOUT:
                case GATEWAY_TIMEOUT:
                case INTERNAL_SERVER_ERROR:
                case BAD_GATEWAY:
                case SERVICE_UNAVAILABLE:
                default:
                    responseThrowable.code = httpException.code();
                    responseThrowable.message = "網路錯誤";
                    break;
            }
            return responseThrowable;
        } else if (e instanceof ServerException) {
            ServerException resultException = (ServerException) e;
            responseThrowable = new ResponseThrowable(resultException, resultException.code);
            responseThrowable.message = resultException.message;
            return responseThrowable;
        } else if (e instanceof JsonParseException || e instanceof JSONException || e instanceof ParseException) {
            responseThrowable = new ResponseThrowable(e, ERROR.PARSE_ERROR);
            responseThrowable.message = "解析錯誤";
            return responseThrowable;
        } else if (e instanceof ConnectException) {
            responseThrowable = new ResponseThrowable(e, ERROR.CONNECT_ERROR);
            responseThrowable.message = "連線失敗";
            return responseThrowable;
        } else if (e instanceof javax.net.ssl.SSLHandshakeException) {
            responseThrowable = new ResponseThrowable(e, ERROR.SSL_ERROR);
            responseThrowable.message = "證照驗證失敗";
            return responseThrowable;
        } else {
            responseThrowable = new ResponseThrowable(e, ERROR.UNKNOWN);
            responseThrowable.message = "未知錯誤";
            return responseThrowable;
        }
    }

    public static class ResponseThrowable extends Exception {
        public int code;
        public String message;

        public ResponseThrowable(Throwable throwable, int code) {
            super(throwable);
            this.code = code;
        }
    }

    ....
}
複製程式碼

處理後得到ResponeThrowable,裡面包含了異常碼code異常描述資訊message,這樣就可以方便地知道請求失敗的原因了。

4. 生命週期管理

4.1 意義

如果頁面發起了網路請求並且在請求結果返回前就已經銷燬了,那麼我們應該在它銷燬時把相關的請求終止。一方面是為了停止無意義的請求,另一方面是為了避免可能帶來的記憶體洩漏。

強大的RxJava可以幫助我們實現這一需求。下面通過 takeUntil、PublishSubject、綜合兩者進行控制 三個部分來講解如何實現。

4.2 takeUntil

RxJava中提供了許多操作符,這裡我們需要使用takeUntil操作符。

ObservableA.takeUntil(ObservableB) 的作用是:
監視ObservableB,當它發射內容時,則停止ObservableA的發射並將其終止。

下面通過示意圖和示例程式碼來加深瞭解,參考自https://zhuanlan.zhihu.com/p/21966621

示意圖:

takeUntil

示例程式碼:

//下面的Observable.interval( x, TimeUnit.MILLISECONDS) 表示每隔x毫秒發射一個long型別數字,數字從0開始,每次遞增1
Observable<Long> observableA = Observable.interval(300, TimeUnit.MILLISECONDS);
Observable<Long> observableB = Observable.interval(800, TimeUnit.MILLISECONDS);

observableA.takeUntil(observableB)
        .subscribe(new Observer<Long>() {
            
            //...onComplete...
			//...onError...
			
            @Override
            public void onNext(Long aLong) {
                System.out.println(aLong);
            }
        });
複製程式碼
輸出結果為
0
1
複製程式碼
  • 示例程式碼大意: ObservableA每隔300ms發射一個數字(並列印出發射的數字),ObservableB每隔800ms發射一個數字。
    由於ObservableB在800ms時發射了內容,終止了ObservableA的發射,所以ObservableA最後只能發射0,1兩個數字。

因此,我們可以利用takeUntil這一特性,讓ObservableA負責網路請求,讓ObservableB負責在頁面銷燬時發射事件,從而終止ObservableA(網路請求)。

4.3 PublishSubject

上面提到了需要一個ObservableB來負責在頁面銷燬時發射事件,PublishSubject就能充當這一角色。

閱讀PublishSubject的原始碼可以發現,它既可充當Observable,擁有subscribe()等方法;也可充當Observer(Subscriber),擁有onNext(),onError等方法。

它的特點是進行subscribe()訂閱後,並不立即發射事件,而是允許我們在認為合適的時機通過呼叫onNext(),onError(),onCompleted()來發射事件。

所以,我們需在Activity或Fragment的生命週期onDestroy()中通過PublishSubject來發射事件

//一般以下程式碼寫在Activity或Fragment的基類中。

PublishSubject<LifeCycleEvent> lifecycleSubject = PublishSubject.create();

//用於提供lifecycleSubject到RetrofitUtil中。
public PublishSubject<LifeCycleEvent> getLifeSubject() {
    return lifecycleSubject;
}

//一般是在onDestroy()時發射事件終止請求,當然你也可以根據需求在生命週期的其他狀態中發射。
@Override
protected void onDestroy() {
	//publishSubject發射事件
    lifecycleSubject.onNext(LifeCycleEvent.DESTROY);
    super.onDestroy();
 }
複製程式碼

4.4 進行控制

瞭解 takeUntil 和 PublishSubject 後,就可以綜合兩者來實現生命週期的控制了。

//省略Retrofit和ApiService的構造過程
...
...
//得到負責網路請求的Observable
Observable observableNet= mApiService.getCommingMovie(count);

//得到負責在頁面銷燬時發射事件的Observable
Observable<LifeCycleEvent> observableLife = 
     lifecycleSubject.filter(new Predicate<LifeCycleEvent>() {
             @Override
             public boolean test(LifeCycleEvent lifeCycleEvent) throws Exception {
             //當生命週期為DESTROY狀態時,發射事件
             return lifeCycleEvent.equals(LifeCycleEvent.DESTROY);
           }
     }).take(1);

//通過takeUntil將兩個Observable聯絡在一起,實現生命週期的控制
observableNet.takeUntil(observableLife)
			 .subscribeOn(Schedulers.io())//設定網路請求在io後臺執行緒中進行
			 .observeOn(AndroidSchedulers.mainThread())//設定請求後的回撥在UI主執行緒中進行
			 .subscribe(observer);//發起請求,請求的回撥結果會傳到訂閱者observer中
複製程式碼

還有其他方式可以實現生命週期的控制,具體實現可到以下地址檢視:
http://www.jianshu.com/p/d62962243c33
http://mp.weixin.qq.com/s/eedFDMIQe30rQmryLeif_Q

5.失敗重試機制

有時候使用者的網路比較不穩定,出現了請求失敗的情況。這時我們不一定就要直接反饋使用者請求失敗,而可以在失敗後嘗試重新請求,說不定這時網路恢復穩定請求成功了呢?! 這樣或許可以提高使用者體驗。
下面介紹如何設定某個請求在失敗後自動進行重試,以及設定重試的次數延遲重試的時間

先上程式碼:

Observable observableNet= mApiService.getCommingMovie(count);

observableNet.retryWhen(new RetryFunction(3,3))//加入失敗重試機制(失敗後延遲3秒開始重試,重試3次)

.takeUntil(observableLife)//生命週期控制
.subscribeOn(Schedulers.io())//設定網路請求在io後臺執行緒中進行
.observeOn(AndroidSchedulers.mainThread())//設定請求後的回撥在UI主執行緒中進行
.subscribe(observer);//發起請求
複製程式碼
//請求失敗重試機制
public static class RetryFunction implements Function<Observable<Throwable>, ObservableSource<?>> {

        private int retryDelaySeconds;//延遲重試的時間
        private int retryCount;//記錄當前重試次數
        private int retryCountMax;//最大重試次數

        public RetryFunction(int retryDelaySeconds, int retryCountMax) {
            this.retryDelaySeconds = retryDelaySeconds;
            this.retryCountMax = retryCountMax;
        }

        @Override
        public ObservableSource<?> apply(Observable<Throwable> throwableObservable) throws Exception {

            //方案一:使用全域性變數來控制重試次數,重試3次後不再重試,通過程式碼顯式回撥onError結束請求
            return throwableObservable.flatMap(new Function<Throwable, ObservableSource<?>>() {
                @Override
                public ObservableSource<?> apply(Throwable throwable) throws Exception {
                    //如果失敗的原因是UnknownHostException(DNS解析失敗,當前無網路),則沒必要重試,直接回撥error結束請求即可
                    if (throwable instanceof UnknownHostException) {
                        return Observable.error(throwable);
                    }

                    //沒超過最大重試次數的話則進行重試
                    if (++retryCount <= retryCountMax) {
                        //延遲retryDelaySeconds後開始重試
                        return Observable.timer(retryDelaySeconds, TimeUnit.SECONDS);
                    }

                    return Observable.error(throwable);
                }
            });


            //方案二:使用zip控制重試次數,重試3次後不再重試(會隱式回撥onComplete結束請求,但我需要的是回撥onError,所以沒采用方案一)
//            return Observable.zip(throwableObservable,Observable.range(1, retryCountMax),new BiFunction<Throwable, Integer, Throwable>() {
//                @Override
//                public Throwable apply(Throwable throwable, Integer integer) throws Exception {
//                    LogUtil.e("ljy",""+integer);
//                    return throwable;
//                }
//            }).flatMap(new Function<Throwable, ObservableSource<?>>() {
//                @Override
//                public ObservableSource<?> apply(Throwable throwable) throws Exception {
//                    if (throwable instanceof UnknownHostException) {
//                        return Observable.error(throwable);
//                    }
//                    return Observable.timer(retryDelaySeconds, TimeUnit.SECONDS);
//                }
//            });

        }
}

複製程式碼

分析:

  1. 通過observableNet.retryWhen(new RetryFunction(3,3))加入失敗重試機制,其引數RetryFunction中的apply方法會返回一個Observable,後面就稱它為ObservableRetry吧。 加入後,當網路請求失敗時,並不會直接回撥observer中的onError,而是會先將失敗異常throwable作為ObservableRetry的事件源。如果ObservableRetry通過onNext發射了事件,則觸發重新請求,而如果ObservableRetry發射了onError/onComplete通知,則該請求正式結束。因此可以我們對apply方法中的throwableObservable進行改造,然後返回一個合適的ObservableRetry來實現自己想要的重試效果。
  2. 程式碼中對throwableObservable進行了flatMap操作,目的是對其事件throwable的型別進行判斷。如果為UnknownHostException型別,則表示無網路DNS解析失敗,這時就沒必要進行重試(都沒網路還重試啥呀),直接通過Observable.error(throwable)結束該次請求。
  3. 然後通過全域性變數 retryCount 和 retryCountMax 來控制重試的次數。重試retryCountMax次之後如果還是失敗,那就通過Observable.error(throwable)放棄重試並結束請求。
  4. 程式碼中還有個方案二,與方案一的區別在於使用zip操作符來控制重試的次數。 瞭解過zip的應該知道其產生的ObservableZip發射的事件總量,與組合成員中事件量少的一致。所以我們通過Observable.range(start, count)發射有限的事件,如range(1, 3)只發射"1","2","3"三個事件,從而限制了ObservableZip最終發射的事件總量不大於3,即重試的次數不超過3次。當超過3次的時候,它會隱式地呼叫onComplete來結束該次請求(方案一是通過顯式地呼叫onError來結束請求,而我需要在observer的onError中反饋給使用者請求失敗,所以選擇了方案一)

6.監聽進度

這裡只講下實現步驟思路,程式碼太多就不放上來了,大家可以直接看DevRing/Demo裡的程式碼,基本參考自JessYan的ProgressManager庫

6.1 上傳進度

  1. 自定義請求實體,繼承RequestBody重寫其幾個必要的方法。 其中監聽上傳進度主要是重寫其writeTo(BufferedSink sink)方法,從該方法中獲取資料總量以及已寫入請求實體的資料量,在這裡通過回撥傳遞相關進度。
  2. 自定義攔截器,實現Interceptor的intercept(Chain chain)方法。 通過該方法將第1步定義的請求實體應用到請求中。
  3. 新增攔截器到OkHttpClient中。 builder.addNetworkInterceptor(progressInterceptor); ###6.2 下載進度 思路和上傳進度差不多
  4. 自定義響應實體,繼承ResponseBody重寫其幾個必要的方法。 其中監聽下載進度主要是重寫其source(Source source)方法,從該方法中獲取資料總量以及已寫入響應實體的資料量,在這裡通過回撥傳遞相關進度。
  5. 自定義攔截器,實現Interceptor的intercept(Chain chain)方法。 通過該方法將第1步定義的響應實體應用到請求中。
  6. 新增攔截器到OkHttpClient中。 builder.addNetworkInterceptor(progressInterceptor);

7. 封裝

(2018.3.27:Demo已對封裝這一塊做了新的調整,詳情請看demo,但封裝的思路還是和下文差不多的)

封裝分為 初始化配置、統一轉換、請求結果封裝、請求回撥(Observer)封裝 四個部分進行。

7.1 初始化配置


public Retrofit initRetrofit() {
   
        OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();
        //設定請求超時時長
        okHttpClientBuilder.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
        //啟用Log日誌
        okHttpClientBuilder.addInterceptor(getHttpLoggingInterceptor());
        //設定快取方式、時長、地址
        okHttpClientBuilder.addNetworkInterceptor(getCacheInterceptor());
        okHttpClientBuilder.addInterceptor(getCacheInterceptor());
        okHttpClientBuilder.cache(getCache());
        //設定https訪問(驗證證照)
        okHttpClientBuilder.hostnameVerifier
		(org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
        //設定統一的header
        okHttpClientBuilder.addInterceptor(getHeaderInterceptor());

        Retrofit retrofit = new Retrofit.Builder()
                //伺服器地址
                .baseUrl(UrlConstants.HOST_SITE_HTTPS)
                //配置轉化庫,採用Gson
                .addConverterFactory(GsonConverterFactory.create())
                //配置回撥庫,採用RxJava
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                //設定OKHttpClient為網路客戶端
                .client(okHttpClientBuilder.build()).build();    
   
        return retrofit ;
}
複製程式碼

7.2 統一轉換

由於每次請求都要進行執行緒切換以及生命週期的控制,頻繁地呼叫以下程式碼

observable.takeUntil(lifecycleObservable)
		  .subscribeOn(Schedulers.io())
		  .observeOn(AndroidSchedulers.mainThread());
複製程式碼

因此可以使用compose方法對Observable進行統一的轉換

//RetrofitUtil中的方法

/**
* 對observable進行統一轉換,併發起請求
*
* @param observable         被訂閱者
* @param observer           訂閱者
* @param event              生命週期中的某一個狀態,比如傳入DESTROY,則表示在進入destroy狀態時  
*                           lifecycleSubject會發射一個事件從而終止請求
* @param lifecycleSubject   生命週期事件發射者
*/
public static void composeToSubscribe(Observable observable, Observer observer, LifeCycleEvent event, PublishSubject<LifeCycleEvent> lifecycleSubject) {

    observable.compose(getTransformer(event, lifecycleSubject)).subscribe(observer);
}


/**
 * 獲取統一轉換用的Transformer
 *
 * @param event               生命週期中的某一個狀態,比如傳入DESTROY,則表示在進入destroy狀態時
 *                            lifecycleSubject會發射一個事件從而終止請求
 * @param lifecycleSubject    生命週期事件發射者
 */
public static <T> ObservableTransformer<T, T> getTransformer(final LifeCycleEvent event, final PublishSubject<LifeCycleEvent> lifecycleSubject) {
    return new ObservableTransformer() {
        @Override
        public ObservableSource apply(Observable upstream) {

            //當lifecycleObservable發射事件時,終止操作。
            //統一在請求時切入io執行緒,回撥後進入ui執行緒
            //加入失敗重試機制(延遲3秒開始重試,重試3次)
            return upstream
                .takeUntil(getLifeCycleObservable(event, lifecycleSubject))
                .retryWhen(new RetryFunction(3,3))
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
        }
    };
}


複製程式碼

7.3 封裝請求結果

伺服器返回的請求結果,一般分為三個部分:請求結果的狀態值,請求結果的描述,返回的資料內容。

{
   "status" : 1,
   "message" : "success",
   "data":{
         "name":"小明",
          "sex": 0,
          "age": 10
   }
}
複製程式碼

其中status和message的型別是固定的,而data的型別不確定,所以data可以採用泛型表示

豆瓣介面返回的結構比較特殊,並不是上面所說的那三部分。實際結構根據伺服器後臺給的來定

//與請求結果結構相符的實體類
public class HttpResult<T> {

    private int count;//請求的數量
    private int start;//請求的起始頁碼
    private int total;//得到的資料總數
    private String title;//請求結果的描述
    private T subjects;//返回的資料內容,型別不確定,使用泛型T表示

	//getter&setter
	...
}
複製程式碼

7.4 封裝請求回撥(Observer)

(DevRing中提供了三種封裝好的Observer,分別用於普通請求,上傳請求(可監聽進度),下載請求(可監聽進度))

可對Observer封裝一層,作用:

  • 在onError中進行統一的異常處理,得到更直接詳細的異常資訊
  • 在onNext中進行統一操作,如請求回來後,先判斷token是否失效,如果失效則直接跳轉登入頁面
  • 在onNext中對返回的結果進行處理,得到更直接的資料資訊
  • 在onSubscribe中進行請求前的操作,注意,onSubscribe是執行在 subscribe() 被呼叫時的執行緒,所以如果在onSubscribe裡進行UI操作,就要保證subscribe()也是呼叫在UI執行緒裡。
public abstract class HttpObserver<T> implements Observer<HttpResult<T>> {

    @Override
    public void onSubscribe(Disposable d) {

    }

    @Override
    public void onComplete() {

    }

    @Override
    public void onError(Throwable e) {
        if (e instanceof Exception) {
            //訪問獲得對應的Exception
            ExceptionHandler.ResponeThrowable responeThrowable = ExceptionHandler.handleException(e);
            onError(responeThrowable.code, responeThrowable.message);
        } else {
            //將Throwable 和 未知錯誤的status code返回
            ExceptionHandler.ResponeThrowable responeThrowable = new ExceptionHandler.ResponeThrowable(e, ExceptionHandler.ERROR.UNKNOWN);
            onError(responeThrowable.code, responeThrowable.message);
        }
    }

    @Override
    public void onNext(HttpResult<T> httpResult) {
        //做一些回撥後需統一處理的事情
        //如請求回來後,先判斷token是否失效
        //如果失效則直接跳轉登入頁面
        //...

        //如果沒失效,則正常回撥
        onNext(httpResult.getTitle(), httpResult.getSubjects());
    }

    //具體實現下面兩個方法,便可從中得到更直接詳細的資訊
    public abstract void onNext(String title, T t);
    public abstract void onError(int errType, String errMessage);
}
複製程式碼

到此,封裝算是結束了,這樣使用起來就會便捷很多,整個的使用流程會在下面的“使用”中介紹,也可以檢視demo。

8. 混淆

在proguard-rules.pro檔案中新增以下內容進行混淆配置

#Retrofit開始
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions
-dontwarn okio.**
#Retrofit結束


#Rxjava&RxAndroid開始
-dontwarn sun.misc.**
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
   long producerIndex;
   long consumerIndex;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
    rx.internal.util.atomic.LinkedQueueNode producerNode;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {
    rx.internal.util.atomic.LinkedQueueNode consumerNode;
}
#Rxjava&RxAndroid結束
複製程式碼

使用

經過前面的配置和封裝後,下面演示一下在實際場景的使用。

1. 一般場景

請求正在上映的電影,然後在View層展示

@GET("v2/movie/in_theaters")
Observable<HttpResult<List<MovieRes>>> getPlayingMovie(@Query("count") int count);
複製程式碼
//被訂閱者(用於發起網路請求)
Observable observable = RetrofitUtil.getApiService().getPlayingMovie(count);

//訂閱者(網路請求回撥)
HttpObserver<List<MovieRes>> observer = new HttpObserver<List<MovieRes>>() {
            //請求成功回撥
            @Override
            public void onNext(String title, List<MovieRes> list) {
                LogUtil.d(TAG,"獲取"+title+"成功");
                //通過IView介面將資料回撥給View層展示
                if (mIView != null) {
                    mIView.getMovieSuccess(list);
                }
            }

			//請求失敗回撥
            @Override
            public void onError(int errType, String errMessage) {
		        //通過IView介面將資料回撥給View層展示
                if (mIView != null) {
                    mIView.getMovieFail(errType, errMessage);
                }
            }
        };

//通過IView介面獲取View層的PublishSubject來進行生命週期的控制 
PublishSubject<LifeCycleEvent> lifecycleSubject = mIView.getLifeSubject();

//發起請求
RetrofitUtil.composeToSubscribe(observable, observer, lifecycleSubject);
複製程式碼

2. 特殊場景

由於沒找到相符的介面,所以demo中沒有提供以下程式碼。就當作提供個思路,請諒解。

2.1 巢狀請求(使用flatMap實現)

場景:先請求token,再根據得到的token請求使用者資訊,最後在View層展示

@GET("...")
Observable<HttpResult<String>> getToken();

@GET("...")
Observable<HttpResult<UserInfo>> getUserInfo(@Query("token") String token);
複製程式碼
//被訂閱者(用於發起網路請求)
Observable observable = RetrofitUtil.getApiService().getToken()
	.flatMap(new Function<HttpResult<String>, ObservableSource<HttpResult<UserInfo>>{
	
		@Override
        public ObservableSource<HttpResult<UserInfo>> apply(HttpResult<String> httpResult) throws Exception {
        
	      //從httpResult中得到請求來的token,然後再發起使用者資訊的請求
          return RetrofitUtil.getApiService().getUserInfo(httpResult.getData());
        }
	});

//訂閱者(網路請求回撥)
HttpObserver<UserInfo> observer= new HttpObserver<UserInfo>() {
            //請求成功回撥
            @Override
            public void onNext(UserInfo userInfo) {
                //通過IView介面將資料回撥給View層展示
                if (mIView != null) {
                    mIView.getUserInfoSuccess(userInfo);
                }
            }

			//請求失敗回撥
            @Override
            public void onError(int errType, String errMessage) {
		        //通過IView介面將資料回撥給View層展示
                if (mIView != null) {
                    mIView.getUserInfoFail(errType, errMessage);
                }
            }
        };

//通過IView介面獲取View層的PublishSubject來進行生命週期的控制 
PublishSubject<LifeCycleEvent> lifecycleSubject = mIView.getLifeSubject();

//發起請求
RetrofitUtil.composeToSubscribe(observable, observer, lifecycleSubject);
複製程式碼

2.2 組合請求返回的結果(使用zip實現)

場景:請求今日最佳男歌手,請求今日最佳女歌手,將男歌手和女歌手進行組合,得到“最佳歌手組合”,最後在View層展示

@GET("...")
Observable<HttpResult<Singer>> getBestSingerMale();

@GET("...")
Observable<HttpResult<Singer>> getBestSingerFemale();
複製程式碼
//被訂閱者(用於發起網路請求)
Observable observableMale = RetrofitUtil.getApiService().getBestSingerMale();
Observable observableFemale = RetrofitUtil.getApiService().getBestSingerFemale();
Observable observableGroup =
	Observable.zip(observableMale , observableFemale , 
		 new BiFunction<HttpResult<Singer>, HttpResult<Singer>, HttpResult<SingerGroup>() {
	         @Override
	         public HttpResult<SingerGroup> apply(HttpResult<Singer> resultMale, 
										         HttpResult<Singer> resultFemale) {
			
			 //組合男女歌手
	         Singer singerMale = resultMale.getData();
	         Singer singerFemale = resultFemale.getData();
	         SingerGroup singerGroup = new SingerGroup(singerMale, singerFemale);
	         
	         HttpResult<SingerGroup> resultGroup = new HttpResult<SingerGroup>();
	         resultGroup.setData(singerGroup); 
	         
	         return resultGroup;
	        }
	     });

//訂閱者(網路請求回撥)
HttpObserver<SingerGroup> observer= new HttpObserver<SingerGroup>() {
            //請求成功回撥
            @Override
            public void onNext(SingerGroup singerGroup) {
                //通過IView介面將資料回撥給View層展示
                if (mIView != null) {
                    mIView.getSingerGroupSuccess(singerGroup);
                }
            }

			//請求失敗回撥
            @Override
            public void onError(int errType, String errMessage) {
		        //通過IView介面將資料回撥給View層展示
                if (mIView != null) {
                    mIView.getSingerGroupFail(errType, errMessage);
                }
            }
        };

//通過IView介面獲取View層的PublishSubject來進行生命週期的控制    
PublishSubject<LifeCycleEvent> lifecycleSubject = mIView.getLifeSubject();

//發起請求
RetrofitUtil.composeToSubscribe(observableGroup , observer, lifecycleSubject);
複製程式碼

實際開發中肯定還有其他的特殊場景,關鍵是運用好RxJava的操作符。操作符的學習地址已貼在文章頂部。


更新:

已將demo和文章中關於Rxjava的部分從1.+改為2.+ 這裡貼一下RxJava2與RxJava1的區別總結(隨筆記錄,僅供參考):

  • RxJava2 按是否可以背壓處理,分成Observable和Flowable,Observable的訂閱者為Observer,Flowable的訂閱者為Subscriber。 不瞭解背壓的可以看這個系列的5-9篇。

  • 背壓處理
    1)上游(Flowable)通過emitter.requested()檢視事件容器的剩餘空間。下游(Subscriber)通過subscription.request(n)從事件容器中請求並消耗事件(消耗一個事件並不代表事件容器立刻多出一個位置)
    2)四種策略 BUFFER,ERROR,DROP,LATEST
    Buffer:事件容器的空間不限制,非Buffer策略時事件容器大小為128
    ERROR:當事件容器溢位時會報MissingBackpressureException。該策略下,當下遊累計消耗完96個事件後,才會給事件容器騰出96個位置。
    DROP: 事件容器裝入128個事件後,剩下的將不會裝入,當下遊累計消耗完128個事件後,才會給事件容器騰出128個位置,這時再取當前時刻傳送的事件裝入。
    LATEST: 與DROP類似,但它會保證取到最後發射的事件

  • Observable多了幾個小夥伴:Single、Completable、Maybe。他們都繼承了ObservableSource。
    Single/SingleObserver:只傳送/接收onNext和onError,且只傳送一次
    Completable/Completable:只傳送/接收onComplete和onError
    Maybe:Single與Completable的結合

  • Func1改為Function,Func2..n改為BiFunction。其中的方法call改成了apply。另外對於filter()過濾,其引數為不為Function而是Predicate

  • Action1改為Consumer,Action2改為BiConsumer。其中的方法call改成了accept。

  • Observer/Subscriber的抽象方法中多了一個onSubscribe(Disposable/Subscription),類似1.+的onStart方法,它在subscribe()時呼叫。其中的引數Disposable/Subscription可以用來取消訂閱/查詢訂閱狀態,Subscription還可用於背壓中請求消耗事件。

  • 不再能傳送null事件,Observable 不再發射任何值,而是正常結束或者丟擲空指標。


相關文章