天天看點

用RxJava處理複雜表單驗證問題

用RxJava處理複雜表單驗證問題

無論是簡單的登入頁面,還是複雜的訂單送出頁面,表單的前端驗證(比如登入名和密碼都符合基本要求才能點亮登入按鈕)都是必不可少的步驟。本文展示了如何用RxJava來友善的處理表單送出前的驗證問題,例子采用了Android上的一個簡單的登入頁面

内容提要

  • 傳統的驗證方式
  • combineLatest操作符
  • 用combineLatest處理表單驗證
  • combineLatest和zip的差別

本文中所示範的例子sample代碼位于RxAndroidDemo,參見loginActivity這個檔案

這裡我們用最簡單的例子來說明,如上圖,一個email輸入和一個password輸入,下方是一個登入的按鈕。隻有當email輸入框内容含有@字元,password輸入框内容大于4個,才點亮下方的按鈕。

首先你用EditText還是繼承自EditText的控件,一般來說監聽它的内容,都是用addTextChangedListener。但是如何顯然登入按鈕的enable與否是同時要判斷email和password的,兩個都成立才可點亮。是以我們在email的TextWatcher中除了要判斷email是否符合條件以外,還要同時判斷password是否符合條件,這樣以來就容易造成多重判斷。

試想如果你在送出一個訂單的表單,上面是十幾個輸入框,每個輸入的内容都同時符合條件才可以點亮“送出”按鈕,這是多麼痛苦的事情————每一個輸入框的改變都要同時再判斷其他十幾個輸入框内容是否符合(實際上此時其他十幾個輸入框沒變化)

combineLatest是RxJava本身提供的一個常用的操作符,它接受兩個或以上的Observable和一個FuncX閉包。當傳入的Observable中任意的一個發射資料時,combineLatest将每個Observable的最近值(Lastest)聯合起來(combine)傳給FuncX閉包進行處理。要點在于

  1. combineLatest是會存儲每個Observable的最近的值的
  2. 任意一個Observable發射新值時都會觸發操作->“combine all the Observable's lastest value together and send to Function”

首先我們寫上email和password的驗證方法,一個需要含有@字元,一個要求字元數超過4個:

private boolean isEmailValid(String email) {
        //TODO: Replace this with your own logic
        return email.contains("@");
    }

private boolean isPasswordValid(String password) {
    //TODO: Replace this with your own logic
    return password.length() > 4;
}
           

随後,我們針對email和password分别建立Observable,發射的值即為各自edittext的變化的内容,而call回調方法的傳回值是textWatcher中afterTextChanged方法的傳入參數:

Observable<String> ObservableEmail = Observable.create(new Observable.OnSubscribe<String>() {

            @Override
            public void call(final Subscriber<? super String> subscriber) {
                mEmailView.addTextChangedListener(new TextWatcher() {
                    @Override
                    public void beforeTextChanged(CharSequence s, int start, int count, int after) {

                    }

                    @Override
                    public void onTextChanged(CharSequence s, int start, int before, int count) {

                    }

                    @Override
                    public void afterTextChanged(Editable s) {
                        subscriber.onNext(s.toString());
                    }
                });
            }
        });

Observable<String> ObservablePassword = Observable.create(new Observable.OnSubscribe<String>() {

    @Override
    public void call(final Subscriber<? super String> subscriber) {
        mPasswordView.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {

            }

            @Override
            public void afterTextChanged(Editable s) {
                subscriber.onNext(s.toString());
            }
        });
    }
});
           

最後,用combineLastest将ObservableEmail和ObservablePassword聯合起來進行驗證:

Observable.combineLatest(ObservableEmail, ObservablePassword, new Func2<String, String, Boolean>() {
            @Override
            public Boolean call(String email, String password) {
                return isEmailValid(email) && isPasswordValid(password);
            }
        }).subscribe(new Subscriber<Boolean>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(Boolean verify) {
                if (verify) {
                    mEmailSignInButton.setEnabled(true);
                } else {
                    mEmailSignInButton.setEnabled(false);
                }
            }
        });
           

onNext中的verify就是經過combineLastest對兩者驗證後組合的結果。

參見LoginActivity的bindView()方法

這裡,即使表單非常複雜,實際上你需要擴充的話就很容易了:

  1. 對每個EditText封裝一個Observable
  2. 改寫這句話,加入新的邏輯:
return isEmailValid(email) && isPasswordValid(password);
           

覺得為每一個EditText封裝一個Observable要寫很多重複代碼?放心,Jake Wharton大神早已經想到,RxBinding中的RxTextView就可以解決這個問題:

Observable<CharSequence> ObservableEmail = RxTextView.textChanges(mEmailView);
Observable<CharSequence> ObservablePassword = RxTextView.textChanges(mPasswordView);

Observable.combineLatest(ObservableEmail, ObservablePassword, new Func2<CharSequence, CharSequence, Boolean>() {
    @Override
    public Boolean call(CharSequence email, CharSequence password) {
        return isEmailValid(email.toString()) && isPasswordValid(password.toString());
    }
}).subscribe(new Subscriber<Boolean>() {
    @Override
    public void onCompleted() {

    }

    @Override
    public void onError(Throwable e) {

    }

    @Override
    public void onNext(Boolean verify) {
        if (verify) {
            mEmailSignInButton.setEnabled(true);
        } else {
            mEmailSignInButton.setEnabled(false);
        }
    }
});
           
參見LoginActivity的bindViewByRxBinding()方法

zip是和combineLatest有點像的一個操作符,接受的參數也是兩個或多個Observable和一個閉包。但是差別在于:

  1. zip是嚴格按照順序來組合每個Observable,比如ObservableA的第一個資料和ObservableB的第一個資料組合在一起發射給FuncX來處理,兩者的第N個資料組合在一起發射給FuncX來處理,以此類推
  2. zip并不是任意一個Observable發射資料了就觸發閉包處理,而是等待每個Observable的第N個資料都發射齊全了才觸發

zip一般用于整合多方按照順序排列的資料。