給初學者的RxJava2.0教程(三)

Season發表於2019-02-28

Outline

[TOC]

前言

上一節講解了執行緒排程, 並且舉了兩個實際中的例子, 其中有一個登入的例子, 不知大家有沒有想過這麼一個問題, 如果是一個新使用者, 必須先註冊, 等註冊成功之後再自動登入該怎麼做呢.

很明顯, 這是一個巢狀的網路請求, 首先需要去請求註冊, 待註冊成功回撥了再去請求登入的介面.

我們當然可以想當然的寫成這樣:

    private void login() {
        api.login(new LoginRequest())
                .subscribeOn(Schedulers.io())               //在IO執行緒進行網路請求
                .observeOn(AndroidSchedulers.mainThread())  //回到主執行緒去處理請求結果
                .subscribe(new Consumer<LoginResponse>() {
                    @Override
                    public void accept(LoginResponse loginResponse) throws Exception {
                        Toast.makeText(MainActivity.this, "登入成功", Toast.LENGTH_SHORT).show();
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(Throwable throwable) throws Exception {
                        Toast.makeText(MainActivity.this, "登入失敗", Toast.LENGTH_SHORT).show();
                    }
                });
    }

    private void register() {
        api.register(new RegisterRequest())
                .subscribeOn(Schedulers.io())               //在IO執行緒進行網路請求
                .observeOn(AndroidSchedulers.mainThread())  //回到主執行緒去處理請求結果
                .subscribe(new Consumer<RegisterResponse>() {
                    @Override
                    public void accept(RegisterResponse registerResponse) throws Exception {
                        Toast.makeText(MainActivity.this, "註冊成功", Toast.LENGTH_SHORT).show();
                        login();   //註冊成功, 呼叫登入的方法
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(Throwable throwable) throws Exception {
                        Toast.makeText(MainActivity.this, "註冊失敗", Toast.LENGTH_SHORT).show();
                    }
                });
    }複製程式碼

(其實能寫成這樣的程式碼的人也很不錯了, 至少沒寫到一起…)

這樣的程式碼能夠工作, 但不夠優雅, 通過本節的學習, 可以讓我們用一種更優雅的方式來解決這個問題.

正題

先來看看最簡單的變換操作符map吧

Map

map是RxJava中最簡單的一個變換操作符了, 它的作用就是對上游傳送的每一個事件應用一個函式, 使得每一個事件都按照指定的函式去變化. 用事件圖表示如下:

給初學者的RxJava2.0教程(三)
map.png

圖中map中的函式作用是將圓形事件轉換為矩形事件, 從而導致下游接收到的事件就變為了矩形.用程式碼來表示這個例子就是:

    Observable.create(new ObservableOnSubscribe<Integer>() {
            @Override
            public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
                emitter.onNext(1);
                emitter.onNext(2);
                emitter.onNext(3);
            }
        }).map(new Function<Integer, String>() {
            @Override
            public String apply(Integer integer) throws Exception {
                return "This is result " + integer;
            }
        }).subscribe(new Consumer<String>() {
            @Override
            public void accept(String s) throws Exception {
                Log.d(TAG, s);
            }
        });複製程式碼

在上游我們傳送的是數字型別, 而在下游我們接收的是String型別, 中間起轉換作用的就是map操作符, 執行結果為:

 D/TAG: This is result 1 
 D/TAG: This is result 2 
 D/TAG: This is result 3複製程式碼

通過Map, 可以將上游發來的事件轉換為任意的型別, 可以是一個Object, 也可以是一個集合, 如此強大的操作符你難道不想試試?

接下來我們來看另外一個廣為人知的操作符flatMap.

FlatMap

flatMap是一個非常強大的操作符, 先用一個比較難懂的概念說明一下:

FlatMap將一個傳送事件的上游Observable變換為多個傳送事件的Observables,然後將它們發射的事件合併後放進一個單獨的Observable裡.

這句話比較難以理解, 我們先通俗易懂的圖片來詳細的講解一下, 首先先來看看整體的一個圖片:

給初學者的RxJava2.0教程(三)
flatmap.png

先看看上游, 上游傳送了三個事件, 分別是1,2,3, 注意它們的顏色.

中間flatMap的作用是將圓形的事件轉換為一個傳送矩形事件和三角形事件的新的上游Observable.

還是不能理解? 別急, 再來看看分解動作:

給初學者的RxJava2.0教程(三)
flatmap1.png

這樣就很好理解了吧 !!!

上游每傳送一個事件, flatMap都將建立一個新的水管, 然後傳送轉換之後的新的事件, 下游接收到的就是這些新的水管傳送的資料. 這裡需要注意的是, flatMap並不保證事件的順序, 也就是圖中所看到的, 並不是事件1就在事件2的前面. 如果需要保證順序則需要使用concatMap.

說了原理, 我們還是來看看實際中的程式碼如何寫吧:

        Observable.create(new ObservableOnSubscribe<Integer>() {
            @Override
            public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
                emitter.onNext(1);
                emitter.onNext(2);
                emitter.onNext(3);
            }
        }).flatMap(new Function<Integer, ObservableSource<String>>() {
            @Override
            public ObservableSource<String> apply(Integer integer) throws Exception {
                final List<String> list = new ArrayList<>();
                for (int i = 0; i < 3; i++) {
                    list.add("I am value " + integer);
                }
                return Observable.fromIterable(list).delay(10,TimeUnit.MILLISECONDS);
            }
        }).subscribe(new Consumer<String>() {
            @Override
            public void accept(String s) throws Exception {
                Log.d(TAG, s);
            }
        });複製程式碼

如程式碼所示, 我們在flatMap中將上游發來的每個事件轉換為一個新的傳送三個String事件的水管, 為了看到flatMap結果是無序的,所以加了10毫秒的延時, 來看看執行結果吧:

D/TAG: I am value 1
D/TAG: I am value 1
D/TAG: I am value 1
D/TAG: I am value 3
D/TAG: I am value 3
D/TAG: I am value 3
D/TAG: I am value 2
D/TAG: I am value 2
D/TAG: I am value 2複製程式碼

結果也確實驗證了我們之前所說.

這裡也簡單說一下concatMap吧, 它和flatMap的作用幾乎一模一樣, 只是它的結果是嚴格按照上游傳送的順序來傳送的, 來看個程式碼吧:

        Observable.create(new ObservableOnSubscribe<Integer>() {
            @Override
            public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
                emitter.onNext(1);
                emitter.onNext(2);
                emitter.onNext(3);
            }
        }).concatMap(new Function<Integer, ObservableSource<String>>() {
            @Override
            public ObservableSource<String> apply(Integer integer) throws Exception {
                final List<String> list = new ArrayList<>();
                for (int i = 0; i < 3; i++) {
                    list.add("I am value " + integer);
                }
                return Observable.fromIterable(list).delay(10,TimeUnit.MILLISECONDS);
            }
        }).subscribe(new Consumer<String>() {
            @Override
            public void accept(String s) throws Exception {
                Log.d(TAG, s);
            }
        });複製程式碼

只是將之前的flatMap改為了concatMap, 其餘原封不動, 執行結果如下:

D/TAG: I am value 1   
D/TAG: I am value 1   
D/TAG: I am value 1   
D/TAG: I am value 2   
D/TAG: I am value 2   
D/TAG: I am value 2   
D/TAG: I am value 3   
D/TAG: I am value 3   
D/TAG: I am value 3複製程式碼

可以看到, 結果仍然是有序的.

好了關於RxJava的操作符最基本的使用就講解到這裡了, RxJava中內建了許許多多的操作符, 這裡通過講解mapflatMap只是起到一個拋磚引玉的作用, 關於其他的操作符只要大家按照本文的思路去理解, 再仔細閱讀文件, 應該是沒有問題的了, 如果大家有需要也可以將需要講解的操作符列舉出來, 我可以根據大家的需求講解一下.

實踐

學習了FlatMap操作符, 我們就可以回答文章開頭提出的那個問題了.

如何優雅的解決巢狀請求, 只需要用flatMap轉換一下就行了.

先回顧一下上一節的請求介面:

public interface Api {
    @GET
    Observable<LoginResponse> login(@Body LoginRequest request);

    @GET
    Observable<RegisterResponse> register(@Body RegisterRequest request);
}複製程式碼

可以看到登入和註冊返回的都是一個上游Observable, 而我們的flatMap操作符的作用就是把一個Observable轉換為另一個Observable, 因此結果就很顯而易見了:

            api.register(new RegisterRequest())            //發起註冊請求
                .subscribeOn(Schedulers.io())               //在IO執行緒進行網路請求
                .observeOn(AndroidSchedulers.mainThread())  //回到主執行緒去處理請求註冊結果
                .doOnNext(new Consumer<RegisterResponse>() {
                    @Override
                    public void accept(RegisterResponse registerResponse) throws Exception {
                        //先根據註冊的響應結果去做一些操作
                    }
                })
                .observeOn(Schedulers.io())                 //回到IO執行緒去發起登入請求
                .flatMap(new Function<RegisterResponse, ObservableSource<LoginResponse>>() {
                    @Override
                    public ObservableSource<LoginResponse> apply(RegisterResponse registerResponse) throws Exception {
                        return api.login(new LoginRequest());
                    }
                })
                .observeOn(AndroidSchedulers.mainThread())  //回到主執行緒去處理請求登入的結果
                .subscribe(new Consumer<LoginResponse>() {
                    @Override
                    public void accept(LoginResponse loginResponse) throws Exception {
                        Toast.makeText(MainActivity.this, "登入成功", Toast.LENGTH_SHORT).show();
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(Throwable throwable) throws Exception {
                        Toast.makeText(MainActivity.this, "登入失敗", Toast.LENGTH_SHORT).show();
                    }
                });複製程式碼

從這個例子也可以看到我們切換執行緒是多麼簡單.

好了本次的教程就到這裡了. 下一節我們將會學到Flowable 以及理解Backpressure背壓的概念. 敬請期待.

相關文章