專案實戰之Rxjava、RxBinding在實際專案中的使用

小劉哥可愛多發表於2018-05-24

前言

網上很多講rxjava入門的文章,講了什麼是rxjava以及一些高大上的操作符,但是真正在專案中使用的場景很少講,那本篇文章主要講一下rxjava在實際專案中的應用場景,rxjava結合rxbinding在實際專案中的使用姿勢瞭解一下。因為rxbind2 本身依賴rxjava2,所以專案中引入rxbinding就可以了,rxjava2就不用引入了。

implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
複製程式碼

引入完了就看一下常用的使用場景吧:

1、優化搜尋

基本上app裡都有搜尋這個功能需求吧,監聽et的文字變化然後請求伺服器拉取資料,如果不優化處理器的話,每次et的值發生變化都會請求伺服器,在弱網環境下很可能出現資料錯亂的問題。如果不用rxjava來處理,各種timer會把人寫暈掉吧,那麼看看rxjava怎麼來優雅的處理:

//優化搜尋功能
    RxTextView.textChanges(mBinding.etSearch)
            // 跳過一開始et內容為空時的搜尋
            .skip(1)
            //debounce 在一定的時間內沒有操作就會傳送事件
            .debounce(1000, TimeUnit.MILLISECONDS)
            //下面這兩個都是資料轉換
            //flatMap:當同時多個網路請求訪問的時候,前面的網路資料會覆蓋後面的網路資料
            //switchMap:當同時多個網路請求訪問的時候,會以最後一個傳送請求為準,前面網路資料會被最後一個覆蓋
            .switchMap(new Function<CharSequence, ObservableSource<List<String>>>() {
                @Override
                public ObservableSource<List<String>> apply(CharSequence charSequence) throws Exception {
                    String searchKey = charSequence.toString();
                    System.out.println("binding=======搜尋內容:" + searchKey);
                    //這裡執行網路操作,獲取資料
                    List<String> list = new ArrayList<String>();
                    list.add("小劉哥");
                    list.add("可愛多");

                    return Observable.just(list);
                }
            })
            // .onErrorResumeNext()
            //網路操作,獲取我們需要的資料
            .subscribeOn(Schedulers.io())
            //介面更新在主執行緒
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Consumer<List<String>>() {
                @Override
                public void accept(List<String> strings) throws Exception {
                    System.out.println("binding=======搜尋到" + strings.size() + "條資料");
                }
            });
複製程式碼

註釋寫的很清楚了,不用解釋了吧,需要注意的一點就是 .skip(1) 這個操作符不能少,不然頁面一開啟就會執行一次搜尋的。

2、結合rxbinding防手抖

    /**
     * 防止多次點選--2秒內執行一次點選
     */
    RxView.clicks(mBinding.btClick)
            .throttleFirst(2, TimeUnit.SECONDS)
            .subscribe(c -> System.out.println("binding=======點選了按鈕"));
複製程式碼

假如一個頁面有一個按鈕,點選一次要請求一下伺服器或者其他操作都可以,這裡做了2秒內響應一次點選事件,很常用的場景。

3、長按事件

    /**
     * 長按事件
     */
    RxView.longClicks(mBinding.btClick)
            .subscribe(c->System.out.println("binding=======長按了按鈕"));
複製程式碼

長按事件,這個不用說了吧。

4、監聽view的選中狀態

    /**
     * checkbox 選中就修改textview
     */
    RxCompoundButton.checkedChanges(mBinding.checkbox)
            .subscribe(new Consumer<Boolean>() {
                @Override
                public void accept(Boolean aBoolean) throws Exception {
                    mBinding.tvCb.setText(aBoolean ? "按鈕選中了" : "按鈕未選中");
                }
            });
複製程式碼

假如頁面有一個cb,比如選中表示同意閱讀了使用者協議什麼的,用來監聽選中狀態來做一些邏輯操作,幾行程式碼就搞定。

5、註冊、登入等獲取驗證碼時的倒數計時操作

/**
 * 倒數計時操作
 */
public void clickTimer(View view) {

    // 2 秒後傳送資料
    Observable.timer(2, TimeUnit.SECONDS)
            .subscribe(new Observer<Long>() {
                @Override
                public void onSubscribe(Disposable d) {

                }

                @Override
                public void onNext(Long value) {
                    System.out.println("binding=======value:" + value);//0
                }

                @Override
                public void onError(Throwable e) {

                }

                @Override
                public void onComplete() {

                }
            });

    //倒數計時操作
    final int count = 10;
    Observable.interval(0, 1, TimeUnit.SECONDS)//設定0延遲,每隔一秒傳送一條資料
            .take(count + 1)//設定迴圈次數
            .map(new Function<Long, Long>() {
                @Override
                public Long apply(Long aLong) throws Exception {

                    return count - aLong;
                }
            })
            .doOnSubscribe(new Consumer<Disposable>() {
                @Override
                public void accept(Disposable disposable) throws Exception {
                    //在傳送資料的時候設定為不能點選
                    mBinding.btCutdown.setEnabled(false);

                    //背景色
                    mBinding.btCutdown.setBackgroundColor(Color.parseColor("#39c6c1"));
                }
            })
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Observer<Long>() {
                @Override
                public void onSubscribe(Disposable d) {

                }

                @Override
                public void onNext(Long value) {
                    mBinding.btCutdown.setText("" + value);
                }

                @Override
                public void onError(Throwable e) {

                }

                @Override
                public void onComplete() {
                    mBinding.btCutdown.setText("重新獲取");
                    mBinding.btCutdown.setEnabled(true);
                    mBinding.btCutdown.setBackgroundColor(Color.parseColor("#d1d1d1"));
                }
            });

}
複製程式碼

很簡潔吧

6、註冊登入等情況下,所有輸入都合法再點亮登入按鈕

/**
     * 註冊登入等情況下,所有輸入都合法再點亮登入按鈕
     */
    Observable<CharSequence> name = RxTextView.textChanges(mBinding.etName).skip(1);
    Observable<CharSequence> age = RxTextView.textChanges(mBinding.etAge).skip(1);

    Observable.combineLatest(name, age, new BiFunction<CharSequence, CharSequence, Boolean>() {
        @Override
        public Boolean apply(CharSequence charSequence, CharSequence charSequence2) throws Exception {

            boolean isNameEmpty = TextUtils.isEmpty(mBinding.etName.getText());
            boolean isAgeEmpty = TextUtils.isEmpty(mBinding.etAge.getText());

            return !isNameEmpty && !isAgeEmpty;
        }
    })
            .subscribe(new Consumer<Boolean>() {
                @Override
                public void accept(Boolean aBoolean) throws Exception {
                    System.out.println("bt======" + aBoolean);
                    mBinding.btSubmit.setEnabled(aBoolean);
                }
            });
複製程式碼

7、使用interval做週期性操作

 /**
 * 每隔2秒 輸出一次日誌
 */
Disposable mDisposable;
public void clickIntervar(View view) {

    Observable.interval(2, TimeUnit.SECONDS)
            .subscribe(new Observer<Long>() {
                @Override
                public void onSubscribe(Disposable d) {
                    mDisposable =d;

                }

                @Override
                public void onNext(Long value) {
                    System.out.println("binding=======輸出日誌:" + value);
                    if (value == 5L) {
                        System.out.println("binding=======dispose");
                        mDisposable.dispose();
                    }
                }

                @Override
                public void onError(Throwable e) {

                }

                @Override
                public void onComplete() {

                }
            });
}
複製程式碼

8、使用schedulePeriodically做輪詢請求

/**
 * 使用schedulePeriodically做輪詢請求 3秒輪詢一次
 */
Observable.create(new ObservableOnSubscribe<String>() {
        @Override
        public void subscribe(final ObservableEmitter<String> e) throws Exception {

            Schedulers.newThread().createWorker()
                    .schedulePeriodically(new Runnable() {
                        @Override
                        public void run() {
                            e.onNext("net work-----");
                        }
                    }, 0, 3, TimeUnit.SECONDS);

        }
    }).subscribe(new Consumer<String>() {
        @Override
        public void accept(String s) throws Exception {
            System.out.println("binding=======net work");
        }
    });
複製程式碼

9、網路出錯重試

/**
 * 網路錯誤重試
 * 這裡just操作符 改為retrofit 網路請求返回的即可。
 */
int mRetryCount;

public void clickRetry(View view) {
    Observable.just("retry")
            .retryWhen(new Function<Observable<Throwable>, ObservableSource<?>>() {
                @Override
                public ObservableSource<?> apply(Observable<Throwable> throwableObservable) throws Exception {

                    // 引數Observable<Throwable>中的泛型 = 上游操作符丟擲的異常,可通過該條件來判斷異常的型別
                    return throwableObservable.flatMap(new Function<Throwable, ObservableSource<?>>() {
                        @Override
                        public ObservableSource<?> apply(Throwable throwable) throws Exception {

                            // 判斷異常資訊  根據異常資訊判斷是否需要重試
                            if (throwable instanceof IOException) {
                                System.out.println("retry======y==");
                                // 重試
                                // 判斷重試次數 這裡設定最多重試5次
                                if (mRetryCount < 5) {
                                    mRetryCount++;
                                    /**
                                     * 1、通過返回的Observable傳送的事件 = Next事件,從而使得retryWhen()重訂閱,最終實現重試功能
                                     * 2、延遲1段時間再重試  採用delay操作符 = 延遲一段時間傳送,以實現重試間隔設定
                                     * 3、在delay操作符的等待時間內設定 = 每重試1次,增多延遲重試時間1s
                                     */
                                    int time = 1000 + mRetryCount * 1000;
                                    return Observable.just(1).delay(time, TimeUnit.MILLISECONDS);
                                } else {
                                    System.out.println("retry======5==");
                                    return Observable.error(new Throwable("已重試5次 放棄治療"));
                                }

                            } else {
                                // 不重試
                                System.out.println("retry======n==");
                                return Observable.error(new Throwable("發生了非網路異常(非I/O異常)"));
                            }
                        }
                    });
                }
            })
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Observer<String>() {
                @Override
                public void onSubscribe(Disposable d) {

                }

                @Override
                public void onNext(String value) {
                    System.out.println("retry======suc==" + value);
                }

                @Override
                public void onError(Throwable e) {
                    System.out.println("retry======err==" + e.toString());
                }

                @Override
                public void onComplete() {

                }
            });

}
複製程式碼

10、解決網路巢狀請求

/**
 * 優化網路巢狀請求問題
 * 以下為了方便演示 寫的虛擬碼
 */
public void clickRequest(View view) {
    Observable<String> requestLogin = Observable.just("requestLogin");
    final Observable<String> request2 = Observable.just("request2");

    requestLogin.subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .doOnNext(new Consumer<String>() {
                @Override
                public void accept(String s) throws Exception {
                    System.out.println("flat=======loginsuccess");
                }
            })
            .observeOn(Schedulers.io())
            .flatMap(new Function<String, ObservableSource<String>>() {
                @Override
                public ObservableSource<String> apply(String s) throws Exception {
                    // 將網路請求1轉換成網路請求2,即傳送網路請求2
                    return request2;
                }
            })
            // (新被觀察者,同時也是新觀察者)切換到IO執行緒去發起登入請求
            //  特別注意:因為flatMap是對初始被觀察者作變換,所以對於舊被觀察者,它是新觀察者,所以通過observeOn切換執行緒
            // 但對於初始觀察者,它則是新的被觀察者
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Consumer<String>() {
                @Override
                public void accept(String s) throws Exception {
                    System.out.println("flat=======第二次請求成功");
                }
            }, new Consumer<Throwable>() {
                @Override
                public void accept(Throwable throwable) throws Exception {
                    System.out.println("flat=======loginerr");
                }
            });
}
複製程式碼

11、背壓--這個就是記錄一下

/**
 * 背壓 Flowable g觀察者使用
 * 解決傳送和訂閱事件 流速不一致的問題
 * <p>
 * 注意:同步訂閱中,被觀察者 & 觀察者工作於同1執行緒,同步訂閱關係中沒有快取區。
 * 被觀察者在傳送1個事件後,必須等待觀察者接收後,才能繼續發下1個事件.若Subscription.request沒有設定,
 * 觀察者接收不到事件,會丟擲MissingBackpressureException異常。
 */
Subscription mSubscription;

public void clickFlow(View view) {

    Flowable.create(new FlowableOnSubscribe<Integer>() {
        @Override
        public void subscribe(FlowableEmitter<Integer> e) throws Exception {

            /**
             * 同步訂閱:
             * 同步訂閱的情況下,呼叫e.requested()方法,獲取當前觀察者需要接收的事件數量.
             * 根據當前觀察者需要接收的事件數量來傳送事件
             *
             * 非同步訂閱:
             * 由於二者處於不同執行緒,所以被觀察者 無法通過 FlowableEmitter.requested()知道觀察者自身接收事件能力。
             * 非同步的反向控制:
             */
            long count = e.requested();
            System.out.println("flowable======需要接收的事件數量=" + count);

            e.onNext(1);
            e.onNext(2);
            e.onNext(3);
            e.onNext(4);
            e.onNext(5);
            e.onComplete();
        }
    }, BackpressureStrategy.ERROR)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Subscriber<Integer>() {
                @Override
                public void onSubscribe(Subscription s) {

                    // 作用:決定觀察者能夠接收多少個事件,多出的事件放入快取區.若不設定,則不接收事件.
                    // 不過被觀察者仍然在傳送事件(存放在快取區,大小為128),等觀察者需要時 再取出被觀察者事件(比如點選事件裡).
                    // 但是 當快取區滿時  就會溢位報錯
                    // 官方預設推薦使用Long.MAX_VALUE,即s.request(Long.MAX_VALUE);
                    mSubscription = s;
                    s.request(2);
                    // s.request(1); // 同步訂閱 觀察者連續要求接收事件的話,被觀察者e.requested() 返回3
                }

                @Override
                public void onNext(Integer integer) {

                    System.out.println("flowable=======" + integer);
                }

                @Override
                public void onError(Throwable t) {

                }

                @Override
                public void onComplete() {

                }
            });

}
複製程式碼

12、補充一個動態許可權

新增依賴 compile 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.5@aar'

// 記得危險許可權 清單檔案裡也需要配置。
// 因為各個業務元件都可能使用到危險許可權,我把許可權統一寫在了commonLibrary裡
 RxPermissions permissions = new RxPermissions(this);
    RxView.clicks(mBinding.btPermission)
            .throttleFirst(1, TimeUnit.SECONDS)
            .subscribeOn(AndroidSchedulers.mainThread())
            .compose(permissions.ensure(Manifest.permission.CAMERA))
            .subscribe(new Consumer<Boolean>() {
                @Override
                public void accept(Boolean aBoolean) throws Exception {
                    if (aBoolean) {
                        System.out.println("binding=======允許");
                    } else {
                        System.out.println("binding=======拒絕");
                    }
                }
            });
複製程式碼

13、結合retrofit網路請求封裝、統一錯誤預處理等

以上的使用場景是在現有專案中,而專案架構搭建的初期涉及到的網路封裝、統一錯誤預處理等由於篇幅問題,要拿出來單獨寫了。網路返回的資料一般情況下是後臺封裝好的固定格式(比如錯誤碼、錯誤資訊由後臺介面設定),這樣處理起來還簡單一點。但是有時候api返回的資料格式是原生的http響應格式,這樣封裝處理的話外面又要套一層response泛型類,處理起來稍微比第一種情況複雜一點。下篇部落格再寫吧。

其他專案中使用到的場景,遇到了會更新在本部落格……

最後,國際慣例 貼出專案地址:點我點我檢視demo,如果對你有幫助,麻煩動動小手start鼓勵一下,謝謝。

相關文章