Android RxJava+Retrofit完美封裝(快取,請求,生命週期管理)

weixin_34292287發表於2016-11-10
*文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出
拖拖踏踏的第三篇文章,我又來造輪子了,一直糾結要不要寫這個主題的文章,總感覺的自己駕馭不了RxJava這麼高深的東西。本篇可能比較多的是個人的理解。
------------- 2018-05-21更新--------------

升級為Retrofit2.0+RxJava2 的版本,專案結構做了一些修改。專案地址https://github.com/Hemumu/Template

前言

RetrofitRxJava已經出來很久了,很多前輩寫了很多不錯的文章,在此不得不感謝這些前輩無私奉獻的開源精神,能讓我們站在巨人的肩膀上望得更遠。對於 RxJava 不是很瞭解的同學推薦你們看扔物線大神的這篇文章給 Android 開發者的 RxJava 詳解一遍看不懂就看第二遍。Retrofit的使用可以參考Android Retrofit 2.0使用

本文內容是基於Retrofit + RxJava做的一些巧妙的封裝。參考了很多文章加入了一些自己的理解,請多指教。原始碼地址https://github.com/Hemumu/RxSample

先放出build.gradle

    compile 'io.reactivex:rxjava:1.1.0'
    compile 'io.reactivex:rxandroid:1.1.0'
    compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'
    compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4'
    compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0-beta4'

本文是基於RxJava1.1.0Retrofit 2.0.0-beta4來進行的。

初始化 Retrofit

新建類Api,此類就是初始化Retrofit,提供一個靜態方法初始化Retrofit非常簡單.

    private static ApiService SERVICE;
    /**
     * 請求超時時間
     */
    private static final int DEFAULT_TIMEOUT = 10000;

    public static ApiService getDefault() {
        if (SERVICE == null) {
            //手動建立一個OkHttpClient並設定超時時間
            OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder();
            httpClientBuilder.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
            /**
             * 對所有請求新增請求頭
             */
            httpClientBuilder.addInterceptor(new Interceptor() {
                @Override
                public okhttp3.Response intercept(Chain chain) throws IOException {
                    Request request = chain.request();
                    okhttp3.Response originalResponse = chain.proceed(request);
                    return originalResponse.newBuilder().header("key1", "value1").addHeader("key2", "value2").build();
                }
            });
            SERVICE = new Retrofit.Builder()
                    .client(httpClientBuilder.build())
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                    .baseUrl(Url.BASE_URL)
                    .build().create(ApiService.class);
        }
        return SERVICE;
    }

提供一個靜態方法初始化Retrofit,手動建立了OkHttpClient設定了請求的超時時間。並在OkHttp的攔截器中增加了請求頭。注意這裡是為所有的請求新增了請求頭,你可以單獨的給請求增加請求頭,例如

    @Headers("apikey:b86c2269fe6588bbe3b41924bb2f2da2")
    @GET("/student/login")
    Observable<HttpResult> login(@Query("phone") String phone,  @Query("password") String psw);

Retrofit初始化不同的地方就在我們新增了這兩句話

.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())

service的定義也從這樣

@GET("/student/login")
Call<HttpResult> getTopMovie(@Query("start") int start, @Query("count") int count);

變成了

@GET("/student/login")
Observable<HttpResult> login(@Query("phone") String phone,  @Query("password") String psw);

返回值變成了Observable,這個Observable不就是RxJava的可觀察者(即被觀察者)麼。

封裝伺服器請求以及返回資料

使用者在使用任何一個網路框架都只關係請求的返回和錯誤資訊,所以對請求的返回和請求要做一個細緻的封裝。

我們一般請求的返回都是像下面這樣

{
   "code":"200",
   "message":"Return Successd!",
   "data":{
         "name":"張三"
          "age":3
   }
}

如果你們的伺服器返回不是這樣的格式那你就只有坐下來請他喝茶,跟他好好說(把他頭摁進顯示器)了。大不了就獻出你的菊花吧!

1796924-9fb250a575deef87.png

對於這樣的資料我們肯定要對code做出一些判斷,不同的code對應不同的錯誤資訊。所以我們新建一個HttpResult類,對應上面的資料結構。

public class HttpResult<T> {

    private int code;
    private String message;
    private T data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
    
}

這算是所有實體的一個基類,data可以為任何資料型別。

我們要對所以返回結果進行預處理,新建一個RxHelper,預處理無非就是對code進行判斷和解析,不同的錯誤返回不同的錯誤資訊,這還不簡單。Rxjavamap操作符不是輕鬆解決

Api.getDefault().login("name","psw")
     .map(new HttpResultFunc<UserEntity>());
     .subscribeOn(Schedulers.io())
     .unsubscribeOn(Schedulers.io())
     .subscribeOn(AndroidSchedulers.mainThread())
     .observeOn(AndroidSchedulers.mainThread())
     .subscribe(subscriber);


    private class HttpResultFunc<T> implements Func1<HttpResult<T>, T> {
        @Override
        public T call(HttpResult<T> httpResult) {
            Log.e("error", httpResult.getData().toString() + "");
            if (httpResult.getCode() != 0) {
                throw new ApiException(httpResult.getCode());
            }
            return httpResult.getData();
        }
    }

喲,這不是輕鬆愉快 so seay麼!對code進行了判斷,code為0就做對應更新UI或者其他後續操作,不等於0就丟擲異常,在ApiException中隊code做處理,根據message欄位進行提示使用者

    private static String getApiExceptionMessage(int code){
        switch (code) {
            case USER_NOT_EXIST:
                message = "該使用者不存在";
                break;
            case WRONG_PASSWORD:
                message = "密碼錯誤";
                break;
            default:
                message = "未知錯誤";
        }
        return message;
    }

撒花!!!

1796924-f6b58d5d3cd07185.jpg

然而。。。RxJava永遠比你想象的強大。RxJava中那麼多操作符看到我身體不適,有個操作符compose。因為我們在每一個請求中都會處理code以及一些重用一些操作符,比如用observeOnsubscribeOn來切換執行緒。RxJava提供了一種解決方案:Transformer(轉換器),一般情況下就是通過使用操作符Observable.compose()來實現。具體可以參考避免打斷鏈式結構:使用.compose( )操作符

新建一個RxHelper對結果進行預處理,程式碼

public class RxHelper {
    /**
     * 對結果進行預處理
     *
     * @param <T>
     * @return
     */
    public static <T> Observable.Transformer<HttpResult<T>, T> handleResult() {
        return new Observable.Transformer<HttpResult<T>, T>() {
            @Override
            public Observable<T> call(Observable<HttpResult<T>> tObservable) {
                return tObservable.flatMap(new Func1<HttpResult<T>, Observable<T>>() {
                    @Override
                    public Observable<T> call(HttpResult<T> result) {
                        LogUtils.e(result.getCode()+"");
                        if (result.getCode() == 0) {
                            return createData(result.getData());
                        } else {
                            return Observable.error(new ApiException(result.getCode()));
                        }
                    }
                }).subscribeOn(Schedulers.io()).unsubscribeOn(Schedulers.io()).subscribeOn(AndroidSchedulers.mainThread()).observeOn(AndroidSchedulers.mainThread());
            }
        };
    }

    /**
     * 建立成功的資料
     *
     * @param data
     * @param <T>
     * @return
     */
    private static <T> Observable<T> createData(final T data) {
        return Observable.create(new Observable.OnSubscribe<T>() {
            @Override
            public void call(Subscriber<? super T> subscriber) {
                try {
                    subscriber.onNext(data);
                    subscriber.onCompleted();
                } catch (Exception e) {
                    subscriber.onError(e);
                }
            }
        });
    }
}

Transformer實際上就是一個Func1<Observable<T>, Observable<R>>,換言之就是:可以通過它將一種型別的Observable轉換成另一種型別的Observable,和呼叫一系列的內聯操作符是一模一樣的。這裡我們首先使用flatMap操作符把Obserable<HttpResult<T>>,轉換成為Observable<T>在內部對code進行了預處理。如果成功則把結果Observable<T>發射給訂閱者。反之則把code交給ApiException並返回一個異常,ApiException中我們對code進行相應的處理並返回對應的錯誤資訊


public class ApiException extends RuntimeException{

    public static final int USER_NOT_EXIST = 100;
    public static final int WRONG_PASSWORD = 101;
    private static String message;

    public ApiException(int resultCode) {
        this(getApiExceptionMessage(resultCode));
    }

    public ApiException(String detailMessage) {
        super(detailMessage);
    }

    @Override
    public String getMessage() {
        return message;
    }

    /**
     * 由於伺服器傳遞過來的錯誤資訊直接給使用者看的話,使用者未必能夠理解
     * 需要根據錯誤碼對錯誤資訊進行一個轉換,在顯示給使用者
     * @param code
     * @return
     */
    private static String getApiExceptionMessage(int code){
        switch (code) {
            case USER_NOT_EXIST:
                message = "該使用者不存在";
                break;
            case WRONG_PASSWORD:
                message = "密碼錯誤";
                break;
            default:
                message = "未知錯誤";
        }
        return message;
    }
}

最後呼叫了頻繁使用的subscribeOn()observeOn()以及unsubscribeOn()

處理ProgressDialog

Rxjava中我們什麼時候來顯示Dialog呢。一開始覺得是放在Subscriber<T>onStart中。onStart可以用作流程開始前的初始化。然而 onStart()由於在 subscribe()發生時就被呼叫了,因此不能指定執行緒,而是隻能執行在 subscribe()被呼叫時的執行緒。所以onStart並不能保證永遠在主執行緒執行。

怎麼辦呢?

1796924-af8e7703ac37a12b.jpg

千萬不要小看了RxJava,與 onStart()相對應的有一個方法 doOnSubscribe(),它和 onStart()同樣是在subscribe()呼叫後而且在事件傳送前執行,但區別在於它可以指定執行緒。預設情況下, doOnSubscribe()執行在 subscribe()發生的執行緒;而如果在 doOnSubscribe()之後有 subscribeOn()的話,它將執行在離它最近的subscribeOn()所指定的執行緒。可以看到在RxHelper中看到我們呼叫了兩次subscribeOn,最後一個呼叫也就是離doOnSubscribe()最近的一次subscribeOn是指定的AndroidSchedulers.mainThread()也就是主執行緒。這樣我們就就能保證它永遠都在主線執行了。這裡不得不感概RxJava的強大。

這裡我們自定義一個類ProgressSubscriber繼承Subscriber<T>

public  abstract class ProgressSubscriber<T> extends Subscriber<T> implements ProgressCancelListener{


    private SimpleLoadDialog dialogHandler;

    public ProgressSubscriber(Context context) {
        dialogHandler = new SimpleLoadDialog(context,this,true);
    }

    @Override
    public void onCompleted() {
        dismissProgressDialog();
    }


    /**
     * 顯示Dialog
     */
    public void showProgressDialog(){
        if (dialogHandler != null) {
            dialogHandler.obtainMessage(SimpleLoadDialog.SHOW_PROGRESS_DIALOG).sendToTarget();
        }
    }

    @Override
    public void onNext(T t) {
        _onNext(t);
    }

    /**
     * 隱藏Dialog
     */
    private void dismissProgressDialog(){
        if (dialogHandler != null) {
            dialogHandler.obtainMessage(SimpleLoadDialog.DISMISS_PROGRESS_DIALOG).sendToTarget();
            dialogHandler=null;
        }
    }
    @Override
    public void onError(Throwable e) {
        e.printStackTrace();
        if (false) { //這裡自行替換判斷網路的程式碼
            _onError("網路不可用");
        } else if (e instanceof ApiException) {
            _onError(e.getMessage());
        } else {
            _onError("請求失敗,請稍後再試...");
        }
        dismissProgressDialog();
    }


    @Override
    public void onCancelProgress() {
        if (!this.isUnsubscribed()) {
            this.unsubscribe();
        }
    }
    protected abstract void _onNext(T t);
    protected abstract void _onError(String message);
}

初始化ProgressSubscriber新建了一個我們自己定義的ProgressDialog並且傳入一個自定義介面ProgressCancelListener。此介面是在SimpleLoadDialog消失onCancel的時候回撥的。用於終止網路請求。

  load.setOnCancelListener(new DialogInterface.OnCancelListener() {
                @Override
                public void onCancel(DialogInterface dialog) {
                    mProgressCancelListener.onCancelProgress();
                }
    });

ProgressSubscriber其他就很簡單了,在onCompleted()onError()的時候取消Dialog。需要的時候呼叫showProgressDialog即可。

處理資料快取

伺服器返回的資料我們肯定要做快取,所以我們需要一個RetrofitCache類來做快取處理。

public class RetrofitCache {
    /**
     * @param cacheKey 快取的Key
     * @param fromNetwork
     * @param isSave       是否快取
     * @param forceRefresh 是否強制重新整理
     * @param <T>
     * @return
     */
    public static <T> Observable<T> load(final String cacheKey,
                                         Observable<T> fromNetwork,
                                         boolean isSave, boolean forceRefresh) {
        Observable<T> fromCache = Observable.create(new Observable.OnSubscribe<T>() {
            @Override
            public void call(Subscriber<? super T> subscriber) {
                T cache = (T) Hawk.get(cacheKey);
                if (cache != null) {
                    subscriber.onNext(cache);
                } else {
                    subscriber.onCompleted();
                }
            }
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
        //是否快取
        if (isSave) {
            /**
             * 這裡的fromNetwork 不需要指定Schedule,在handleRequest中已經變換了
             */
            fromNetwork = fromNetwork.map(new Func1<T, T>() {
                @Override
                public T call(T result) {
                    Hawk.put(cacheKey, result);
                    return result;
                }
            });
        }
        //強制重新整理
        if (forceRefresh) {
            return fromNetwork;
        } else {
            return Observable.concat(fromCache, fromNetwork).first();
        }
    }
}

幾個引數註釋上面已經寫得很清楚了,不需要過多的解釋。這裡我們先取了一個Observable<T>物件fromCache,裡面的操作很簡單,去快取裡面找個key對應的快取,如果有就發射資料。在fromNetwork裡面做的操作僅僅是快取資料這一操作。最後判斷如果強制重新整理就直接返回fromNetwork反之用Observable.concat()做一個合併。concat操作符將多個Observable結合成一個Observable併發射資料。這裡又用了first()fromCachefromNetwork任何一步一旦發射資料後面的操作都不執行。

最後我們新建一個HttpUtil用來返回使用者關心的資料,快取,顯示Dialog在這裡面進行。

public class HttpUtil{
    /**
     * 構造方法私有
     */
    private HttpUtil() {
    }

    /**
     * 在訪問HttpUtil時建立單例
     */
    private static class SingletonHolder {
        private static final HttpUtil INSTANCE = new HttpUtil();
    }

    /**
     * 獲取單例
     */
    public static HttpUtil getInstance() {
        return SingletonHolder.INSTANCE;
    }

    //新增執行緒管理並訂閱
    public void toSubscribe(Observable ob, final ProgressSubscriber subscriber,String cacheKey,boolean isSave, boolean forceRefresh) {
        //資料預處理
        Observable.Transformer<HttpResult<Object>, Object> result = RxHelper.handleResult();
          //重用操作符
        Observable observable = ob.compose(result)
                .doOnSubscribe(new Action0() {
                    @Override
                    public void call() {
                        //顯示Dialog和一些其他操作
                        subscriber.showProgressDialog();
                    }
                });
        //快取
        RetrofitCache.load(cacheKey,observable,isSave,forceRefresh).subscribe(subscriber);



    }

Activity生命週期管理

基本的網路請求都是向伺服器請求資料,客戶端拿到資料後更新UI。但也不排除意外情況,比如請求回資料途中Activity已經不在了,這個時候就應該取消網路請求。
要實現上面的功能其實很簡單,兩部分

  • 隨時監聽Activity(Fragment)的生命週期並對外發射出去; 在我們的網路請求中,接收生命週期
  • 並進行判斷,如果該生命週期是自己繫結的,如Destory,那麼就斷開資料向下傳遞的過程

實現以上功能需要用到RxjavaSubject的子類PublishSubject
在你的BaseActivity中新增如下程式碼

public class BaseActivity extends AppCompatActivity {

    public final PublishSubject<ActivityLifeCycleEvent> lifecycleSubject = PublishSubject.create();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        lifecycleSubject.onNext(ActivityLifeCycleEvent.CREATE);
        super.onCreate(savedInstanceState);
    }

    @Override
    protected void onPause() {
        lifecycleSubject.onNext(ActivityLifeCycleEvent.PAUSE);
        super.onPause();
    }

    @Override
    protected void onStop() {
        lifecycleSubject.onNext(ActivityLifeCycleEvent.STOP);
        super.onStop();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        lifecycleSubject.onNext(ActivityLifeCycleEvent.DESTROY);
    }

這樣的話,我們把所有生命週期事件都傳給了PublishSubject了,或者說PublishSubject已經接收到了並能夠對外發射各種生命週期事件的能力了。

現在我們要讓網路請求的時候去監聽這個PublishSubject,在收到相應的生命週期後取消網路請求,這又用到了我們神奇的compose(),我們需要修改handleResult程式碼如下

public static <T> Observable.Transformer<HttpResult<T>, T> handleResult(final ActivityLifeCycleEvent event,final PublishSubject<ActivityLifeCycleEvent> lifecycleSubject) {
        return new Observable.Transformer<HttpResult<T>, T>() {
            @Override
            public Observable<T> call(Observable<HttpResult<T>> tObservable) {
                Observable<ActivityLifeCycleEvent> compareLifecycleObservable =
                        lifecycleSubject.takeFirst(new Func1<ActivityLifeCycleEvent, Boolean>() {
                            @Override
                            public Boolean call(ActivityLifeCycleEvent activityLifeCycleEvent) {
                                return activityLifeCycleEvent.equals(event);
                            }
                        });
                return tObservable.flatMap(new Func1<HttpResult<T>, Observable<T>>() {
                    @Override
                    public Observable<T> call(HttpResult<T> result) {
                        if (result.getCount() != 0) {
                            return createData(result.getSubjects());
                        } else {
                            return Observable.error(new ApiException(result.getCount()));
                        }
                    }
                }) .takeUntil(compareLifecycleObservable).subscribeOn(Schedulers.io()).unsubscribeOn(Schedulers.io()).subscribeOn(AndroidSchedulers.mainThread()).observeOn(AndroidSchedulers.mainThread());
            }
        };
    }

呼叫的時候增加了兩個引數一個是ActivityLifeCycleEvent 其實就是一些列舉表示Activity的生命週期


public enum  ActivityLifeCycleEvent {
    CREATE,
    START,
    RESUME,
    PAUSE,
    STOP,
    DESTROY
}

另外一個引數就是我們在BaseActivity新增的PublishSubject,這裡用到了takeUntil()它的作用是監聽我們建立的compareLifecycleObservablecompareLifecycleObservable中就是判斷了如果當前生命週期和Activity一樣就發射資料,一旦compareLifecycleObservable 對外發射了資料,就自動把當前的Observable(也就是網路請求的Observable)停掉。
當然有個庫是專門針對這種情況的,叫RxLifecycle,不過要繼承他自己的RxActivity,當然這個庫不只是針對網路請求,其他所有的Rxjava都可以。有需要的可以去看看。

最後新建一個ApiService存放我們的請求

public interface ApiService {
    @GET("/student/mobileRegister")
    Observable<HttpResult<UserEntity>> login(@Query("phone") String phone, @Query("password") String psw);

}

使用

使用起來就超級簡單了


/**
 *
 *
 //  ┏┓   ┏┓
 //┏┛┻━━━┛┻┓
 //┃       ┃
 //┃   ━   ┃
 //┃ ┳┛ ┗┳ ┃
 //┃       ┃
 //┃   ┻   ┃
 //┃       ┃
 //┗━┓   ┏━┛
  //   ┃   ┃   神獸保佑
  //   ┃   ┃   阿彌陀佛
  //   ┃   ┗━━━┓
  //   ┃       ┣┓
  //   ┃       ┏┛
  //   ┗┓┓┏━┳┓┏┛
  //     ┃┫┫ ┃┫┫
  //     ┗┻┛ ┗┻┛
  //
  */

      //獲取豆瓣電影TOP 100
        Observable ob = Api.getDefault().getTopMovie(0, 100);
        HttpUtil.getInstance().toSubscribe(ob, new ProgressSubscriber<List<Subject>>(this) {
            @Override
            protected void _onError(String message) {
                Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show();
            }

            @Override
            protected void _onNext(List<Subject> list) {
                
            }

        }, "cacheKey", ActivityLifeCycleEvent.PAUSE, lifecycleSubject, false, false);

具體很多東西都可以在使用的時候具體修改,比如快取我用的HawkDialog是我自己定義的一個SimpleLoadDialog。原始碼已經給出請多指教!

-------------更新--------------
評論區有人提出對於Activity生命週期的管理,個人疏忽大意,特地來加上。

END!

Thanks
Rx處理伺服器請求、快取的完美封裝
給 Android 開發者的 RxJava 詳解
RxJava 與 Retrofit 結合的最佳實踐
可能是東半球最全的RxJava使用場景小結
帶你學開源專案:RxLifecycle - 當Activity被destory時自動暫停網路請求

相關文章