解剖 RxJava 之過濾操作符

騎摩托馬斯發表於2017-03-27

介紹

此文章結合 Github AnalyseRxJava 專案,給 Android 開發者帶來 RxJava 詳細的解說。參考自 RxJava Essential 及書中的例子

關於 RxJava 的由來及簡介,這裡就不在重複了,感興趣的請閱讀 RxJava Essential

相關文章連結

App 講解

在此 App 中將檢索安裝的應用列表並填充 RecycleView 的 item 來展示它們。通過下拉重新整理的功能和一個進度條來告知使用者當前任務正在執行。

首先建立一個 Observable 來檢索安裝的應用程式列表並把它提供給我們的觀察者。我們一個接一個的發射這些應用程式資料,將它們分組到一個單獨的列表中,以此來展示響應式方法的靈活性。

private Observable<AppInfo> getApps() {
        final Intent mainIntent = new Intent(Intent.ACTION_MAIN);
        mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);

        return Observable.from(this.getPackageManager().queryIntentActivities(mainIntent, 0))
                .map(new Func1<ResolveInfo, AppInfo>() {
                    @Override
                    public AppInfo call(ResolveInfo info) {
                        AppInfoRich appInfoRich = new AppInfoRich(AppListActivity.this, info);
                        Bitmap icon = BitmapUtils.drawableToBitmap(appInfoRich.getIcon());
                        String name = appInfoRich.getName();
                        String iconPath = App.getInstance().getFilesDir() + "/" + name;
                        BitmapUtils.storeBitmap(App.getInstance(), icon, name);
                        return new AppInfo(name, iconPath, appInfoRich.getLastUpdateTime());
                    }
                });
    }複製程式碼

RxJava Essiential 書中,使用 Obervable.create 方式不同,這裡使用的是 Observable.from,在使用 RxJava 中應該儘量的避免編寫著自定義操作符, 具體原因請閱讀實現操作符時的一些陷阱等系列文,通過閱讀該系列,我發現很難寫出正確的操作符。所以儘量避免編寫自定義操作符。

AppInfo物件如下:

@Data
@Accessors(prefix = "m")
public class AppInfo implements Comparable<AppInfo> {

    long mLastUpdateTime;

    String mName;

    String mIcon;

    public AppInfo(String name, String icon, long lastUpdateTime) {
        this.mLastUpdateTime = lastUpdateTime;
        this.mName = name;
        this.mIcon = icon;
    }

    @Override
    public int compareTo(@NonNull AppInfo appInfo) {
        // 實現改方法為之後使用 Observable.toSortedList
        return getName().compareTo(appInfo.getName());
    }
}複製程式碼

我們使用 refreshAppList 下拉重新整理方法,因此列表資料可以來自初始化載入,或由使用者觸發的一個重新整理動作。針對這兩個場景,我們用同樣的行為,因此我們把我們的觀察者放在一個易被複用的函式裡面。下面是我們的觀察者,定義了成功、失敗、完成要做的事情:

private void refreshAppList() {
        getApps().toSortedList()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<List<AppInfo>>() {
                    @Override
                    public void onCompleted() {
                    }

                    @Override
                    public void onError(Throwable e) {
                        ToastUtils.SHORT.show(getBaseContext(), "Something went wrong!");
                        mAppsRefreshLayout.setRefreshing(false);
                    }

                    @Override
                    public void onNext(List<AppInfo> appInfos) {
                        mAppInfos.addAll(appInfos);
                        mAppsRecyclerView.setVisibility(View.VISIBLE);
                        mAppsAdapter.setData(appInfos);
                        mAppsRefreshLayout.setRefreshing(false);
                    }
                });
    }複製程式碼

以上就來檢索安裝的應用程式列表並把它提供給我們的觀察者。我們一個接一個的發射這些應用程式資料,將它們分組到一個單獨的列表中。

過濾操作符

在這一節中,我們將基於 RxJava 的 just(),repeat(),defer(),range(),interval() 方法展示一些例子。

just()

此方法的示意圖為

解剖 RxJava 之過濾操作符

該方法可以接受 1 ~ 10 個元素作為引數,然後將這些元素依次的進行發射。可以將一個函式作為引數傳給 just() 方法,你將會得到一個已存在程式碼的原始 Observable 版本。在一個新的響應式架構的基礎上遷移已存在的程式碼,這個方法可能是一個有用的開始點。

private void performJust() {
        Observable.just(mAppInfos.get(0), mAppInfos.get(1), mAppInfos.get(2))
                .toSortedList()
                .subscribe(new Action1<List<AppInfo>>() {
                    @Override
                    public void call(List<AppInfo> appInfos) {
                        mAppsAdapter.clear();
                        mAppsAdapter.setData(appInfos);
                    }
                });
    }複製程式碼

檢索列表並提取出三個元素, 依次進行訂閱發射

repeat()

此方法的示意圖為

解剖 RxJava 之過濾操作符

假如你想對一個 Observable 重複發射三次資料。例如,我們用 just() 例子中的 Observable:

Observable.just(mAppInfos.get(0), mAppInfos.get(1), mAppInfos.get(2))
                .repeat(3)
                .toSortedList()
                .subscribe(new Action1<List<AppInfo>>() {
                    @Override
                    public void call(List<AppInfo> appInfos) {
                        mAppsAdapter.clear();
                        mAppsAdapter.setData(appInfos);
                    }
                });複製程式碼

正如你看到的,我們在 just() 建立 Observable 後追加了 repeat(3) ,它將會建立 9 個元素的序列,每一個都單獨發射。

defer()

此函式的示意圖:

解剖 RxJava 之過濾操作符

defer() 用以確保Observable程式碼在被訂閱後才執行(而不是建立後立即執行)。just()from() 等這類能夠建立 Observable 的操作符。在建立之初,就已經儲存了物件的值,而不被訂閱的時候。這種情況,顯然不是預期表現,我們想要的是無論什麼時候請求,都能夠表現為當前值。defer() 就能滿足這種需求,使用 defer() 操作符的唯一缺點就是,每次訂閱都會建立一個新的 Observable 物件。create() 操作符則為每一個訂閱者都使用同一個函式,所以,後者效率更高。

private void performDefer() {
        Observable<AppInfo> observable = Observable.defer(new Func0<Observable<AppInfo>>() {
            @Override
            public Observable<AppInfo> call() {
                return Observable.just(mAppInfos.get(0), mAppInfos.get(1), mAppInfos.get(2));
            }
        });

        // 改變 List 中的順序
        CollectionUtils.reverseList(mAppInfos);
        mAppsAdapter.clear();

        // defer 操作符只有在被訂閱的時候才會執行 List.get 操作
        observable.subscribe(new Action1<AppInfo>() {
            @Override
            public void call(AppInfo appInfo) {
                mAppsAdapter.add(appInfo);
            }
        });
    }複製程式碼

以上程式碼,雖然 List 資料的反轉是在 Observable.just() 之後,但是因為 defer() 函式的作用,還是會得到最新的反轉之後的資料

range()

此函式的示意圖:

解剖 RxJava 之過濾操作符

range() 函式用兩個數字作為引數:第一個是起始點,第二個是我們想發射數字的個數。

    private void performRange() { // 獲取指定範圍的 AppInfo
        mAppsAdapter.clear();
        Observable.range(4, 4)
                .map(new Func1<Integer, AppInfo>() {
                    @Override
                    public AppInfo call(Integer integer) {
                        return mAppInfos.get(integer);
                    }
                })
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppsAdapter.add(appInfo);
                    }
                });
    }複製程式碼

interval()

此函式的示意圖:

解剖 RxJava 之過濾操作符

interval()函式在你需要建立一個輪詢程式時非常好用。 interval() 函式的兩個引數:一個指定兩次發射的時間間隔,另一個是用到的時間單位。

private void performInterval() { // 每 2 秒顯示一個 AppInfo
        mAppsAdapter.clear();
        mInterval = Observable.interval(2, TimeUnit.SECONDS) // interval 預設是在 work 執行緒
                .map(new Func1<Long, AppInfo>() {
                    @Override
                    public AppInfo call(Long index) {
                        if (index.intValue() < 5) {
                            if (mInterval.isUnsubscribed()) {
                                mInterval.unsubscribe();
                            }
                        }
                        return mAppInfos.get(index.intValue());
                    }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppsAdapter.add(appInfo);
                    }
                });
    }複製程式碼

timer()

timer() 已作廢,請參考 interval()

filter()

此函式示意圖為:

解剖 RxJava 之過濾操作符

filter() 函式接受一個 Func1 物件,即只有一個引數的函式。Func1 有一個 Object 物件來作為它的引數型別並且返回 Boolean 物件。只要條件符合 filter() 函式就會返回 true。此時,值會發射出去並且所有的觀察者都會接收到。

    private void performFilter() {
        mAppsAdapter.clear();
        Observable.from(mAppInfos)
                .filter(new Func1<AppInfo, Boolean>() {
                    @Override
                    public Boolean call(AppInfo appInfo) { // 過濾出以 C 開頭的 AppInfo
                        return appInfo.getName().startsWith("C");
                    }
                })
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppsAdapter.add(appInfo);
                    }
                });
    }複製程式碼

以上函式,遍歷已安裝的應用列表,只展示以字母 C 開頭的已安裝的應用

take()

此函式的示意圖為:

解剖 RxJava 之過濾操作符

take() 函式用整數 N 來作為一個引數,從原始的序列中發射前 N 個元素

    private void performTake() {
        mAppsAdapter.clear();
        Observable.from(mAppInfos)
                .take(5) // 顯示序列頭 5 個 AppInfo
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppsAdapter.add(appInfo);
                    }
                });
    }複製程式碼

takeLast()

此函式的示意圖為:

解剖 RxJava 之過濾操作符

takeLast() 函式用整數 N 來作為一個引數,從原始的序列中發射後 N 個元素

    private void performTakeLast() {
        mAppsAdapter.clear();
        Observable.from(mAppInfos)
                .takeLast(5) // 顯示序列後 5 個 AppInfo
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppsAdapter.add(appInfo);
                    }
                });
    }複製程式碼

distinct()

此函式的示意圖為:

解剖 RxJava 之過濾操作符

如果我們想對一個指定的值僅處理一次該怎麼辦?我們可以對我們的序列使用 distinct() 函式去掉重複的。就像 takeLast() 一樣,distinct() 作用於一個完整的序列,然後得到重複的過濾項,它需要記錄每一個發射的值。如果你在處理一大堆序列或者大的資料記得關注記憶體使用情況。

    private void performDistinct() { // 獲取序列的頭三條資料,然後重複三次,最後將重複去掉
        mAppsAdapter.clear();
        Observable.from(mAppInfos)
                .take(3)
                .repeat(3)
                .distinct()
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppsAdapter.add(appInfo);
                    }
                });
    }複製程式碼

distinctUntilChanged()

此函式示意圖為:

解剖 RxJava 之過濾操作符

ditinctUntilChanged() 過濾函式能做到這一點。它能輕易的忽略掉所有的重複並且只發射出新的值。

    private void performDistinctUntilChanged() {
        mAppsAdapter.clear();
        mDistinctInterval = Observable.interval(1, TimeUnit.SECONDS)
                .map(new Func1<Long, AppInfo>() {
                    @Override
                    public AppInfo call(Long aLong) {

                        if (aLong.intValue() == mAppInfos.size() - 1) {
                            if (!mDistinctInterval.isUnsubscribed()) {
                                mDistinctInterval.unsubscribe();
                            }
                        }

                        if (aLong.intValue() % 3 == 0) {
                            return mAppInfos.get(aLong.intValue());
                        }

                        return mAppInfos.get(3);
                    }
                })
                .distinctUntilChanged()
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppsAdapter.add(appInfo);
                    }
                });
    }複製程式碼

first()

此函式示意圖為:

解剖 RxJava 之過濾操作符

first() 方法, 它們從 Observable 中只發射第一個元素。傳 Func1 作為引數,:一個可以確定我們感興趣的第一個符合約束條件的元素。


    private void performFirst() { // 過濾出序列中第一個以 C 開頭的 AppInfo
        mAppsAdapter.clear();
        Observable.from(mAppInfos)
                .first(new Func1<AppInfo, Boolean>() {
                    @Override
                    public Boolean call(AppInfo appInfo) {
                        return appInfo.getName().startsWith("C");
                    }
                })
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppsAdapter.add(appInfo);
                    }
                });
    }複製程式碼

last()

此函式示意圖為:

解剖 RxJava 之過濾操作符

last() 方法, 它們從 Observable 中只發射最後一個元素。傳Func1作為引數,:一個可以確定我們感興趣的最後一個的符合約束條件的元素。

    private void performLast() { // 過濾出序列中最後一個以 C 開頭的 AppInfo
        mAppsAdapter.clear();
        Observable.from(mAppInfos)
                .last(new Func1<AppInfo, Boolean>() {
                    @Override
                    public Boolean call(AppInfo appInfo) {
                        return appInfo.getName().startsWith("C");
                    }
                })
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppsAdapter.add(appInfo);
                    }
                });
    }複製程式碼

skip() & skipLast()

skip() 此函式的示意圖為:

解剖 RxJava 之過濾操作符

skipLast() 此函式的示意圖為:

解剖 RxJava 之過濾操作符

skip()skipLast() 函式與 take()takeLast() 相對應。它們用整數 N 作引數,從本質上來說,它們不讓 Observable 發射前 N 個或者後 N 個值。如果我們知道一個序列以沒有太多用的“可控”元素開頭或結尾時我們可以使用它。

// skip()

    private void performSkip() { // 跳過頭兩條 AppInfo
        mAppsAdapter.clear();
        Observable.from(mAppInfos)
                .skip(2)
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppsAdapter.add(appInfo);
                    }
                });
    }複製程式碼
// skipLast()

    private void performSkipLast() { // 跳過最後兩條 AppInfo
        mAppsAdapter.clear();
        Observable.from(mAppInfos)
                .skipLast(2)
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppsAdapter.add(appInfo);
                    }
                });
    }複製程式碼

elementAt()

此函式的示意圖:

解剖 RxJava 之過濾操作符

如果我們只想要可觀測序列發射的第五個元素該怎麼辦?elementAt() 函式僅從一個序列中發射第 n 個元素然後就完成了。
如果我們想查詢第五個元素但是可觀測序列只有三個元素可供發射時該怎麼辦?我們可以使用 elementAtOrDefault() 。下圖展示瞭如何通過使用 elementAt(2) 從一個序列中選擇第三個元素以及如何建立一個只發射指定元素的新的 Observable。

    private void performElementAt() { // 發射序列中的第 4 個元素,如果沒有預設發射序列中第一個元素
        mAppsAdapter.clear();
        Observable.from(mAppInfos)
                .elementAtOrDefault(3, mAppInfos.get(0))
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppsAdapter.add(appInfo);
                    }
                });
    }複製程式碼

sample()

此函式的示意圖:

解剖 RxJava 之過濾操作符

如果我們想讓 App 列表美妙發射一個元素,但是隻是每 3 秒才顯示當前發射的元素,更恰當的例子是溫度感測器。它每秒都會發射當前室內的溫度。說實話,我們並不認為溫度會變化這麼快,我們可以使用一個小的發射間隔。
這時候就可以用到 sample()

    private void performSample() { // 每秒發出一個 AppInfo,但是每隔三秒的時候才顯示出來
        mAppsAdapter.clear();
        mSampleInterval = Observable.interval(1, TimeUnit.SECONDS)
                .map(new Func1<Long, AppInfo>() {
                    @Override
                    public AppInfo call(Long aLong) {
                        if (aLong.intValue() == mAppInfos.size() - 1) {
                            if (!mSampleInterval.isUnsubscribed()) {
                                mSampleInterval.unsubscribe();
                            }
                        }
                        return null;
                    }
                })
                .sample(3, TimeUnit.SECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppsAdapter.add(appInfo);
                    }
                });
    }複製程式碼

timeout()

此函式的示意圖為:

解剖 RxJava 之過濾操作符

假設我們工作的是一個時效性的環境,我們溫度感測器每秒都在發射一個溫度值。我們想讓它每隔兩秒至少發射一個,我們可以使用 timeout() 函式來監聽源可觀測序列,就是在我們設定的時間間隔內如果沒有得到一個值則發射一個錯誤。我們可以認為 timeout() 為一個 Observable 的限時的副本。如果在指定的時間間隔內 Observable 不發射值的話,它監聽的原始的 Observable 時就會觸發 onError() 函式。類似的還可以用於網路請求超時的異常處理

    private void performTimeout() {
        mAppsAdapter.clear();
        mTimeoutInterval = Observable.interval(1, 3, TimeUnit.SECONDS)  // 第 1 秒時發射一個 AppInfo, 之後每隔 3 秒發射一個 AppInfo
                .map(new Func1<Long, AppInfo>() {
                    @Override
                    public AppInfo call(Long aLong) {
                        if (aLong.intValue() == mAppInfos.size() - 3) {
                            if (!mTimeoutInterval.isUnsubscribed()) {
                                mTimeoutInterval.unsubscribe();
                            }
                        }
                        return mAppInfos.get(aLong.intValue());
                    }
                })
                .timeout(2, TimeUnit.SECONDS) // 超時時間設定為 2 秒
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<AppInfo>() {
                    @Override
                    public void onCompleted() {
                    }

                    @Override
                    public void onError(Throwable e) {
                        ToastUtils.SHORT.show(getBaseContext(), "Timeout!!!!");
                    }

                    @Override
                    public void onNext(AppInfo appInfo) {
                        mAppsAdapter.add(appInfo);
                    }
                });
    }複製程式碼

debounce()

此函式的示意圖:

解剖 RxJava 之過濾操作符

debounce() 函式過濾掉由 Observable 發射的速率過快的資料;如果在一個指定的時間間隔過去了仍舊沒有發射一個,那麼它將發射最後的那個
就像 sample()timeout() 函式一樣,debounce() 使用 TimeUnit 物件指定時間間隔。
下圖展示了多久從 Observable 發射一次新的資料,debounce() 函式開啟一個內部定時器,如果在這個時間間隔內沒有新的資料發射,則新的 Observable 發射出最後一個資料:

debounce 是一個非常有用的函式,在 RxBinding 中,就使用 debounce 來解決多次點選按鈕等問題

    private void performDebounce() {
        mAppsAdapter.clear();
        mDebounceInterval = Observable.interval(2, TimeUnit.SECONDS)
                .map(new Func1<Long, AppInfo>() {
                    @Override
                    public AppInfo call(Long aLong) {
                        if (aLong.intValue() == mAppInfos.size() - 1) {
                            if (!mDebounceInterval.isUnsubscribed()) {
                                mDebounceInterval.unsubscribe();
                            }
                        }
                        return mAppInfos.get(aLong.intValue());
                    }
                })
                .debounce(1, TimeUnit.SECONDS) // 通過修改數值來顯示不同的結果
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppsAdapter.add(appInfo);
                    }
                });
    }複製程式碼

Github

相關文章