「チェックAがONならば、項目Bは入力必須とする」という Validation を RxJava + RxAndroid でやる

 例えばショッピングサイトとかの発送先指定のフォーム『登録されている住所とは違う住所に送りたい時、「別の住所に送る」をチェックする、すると「住所2」が必須入力となり、入力するまで次へ進めない』的なちょっと込み入ったValidationをReactive ExtensionsのJava版、RxJavaRxAndroidでやってみました。

動作イメージ

 まずいきなり動作結果から。

  • 住所1は入力必須。
  • 住所2は「住所2へ配送する」がチェックされている場合のみ、入力必須。
  • 必須項目が入力されていない場合はボタンを押せない

こんな仕様です。

実装

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_my);

    // 「注文を確定する」ボタン
    final Button buttonSubmit = (Button)findViewById(R.id.buttonSubmit);

    // チェックボックスのON/OFFをObservable化
    final Observable<Boolean> useSecondaryAddress =
            ViewObservable.input((CheckBox) findViewById(R.id.checkUseSecondary), true)
            .map(new Func1<OnCheckedChangeEvent, Boolean>() {
                @Override
                public Boolean call(OnCheckedChangeEvent onCheckedChangeEvent) {
                    return onCheckedChangeEvent.value;
                }
            });

    // 住所1をObservable化
    final Observable<OnTextChangeEvent> primaryAddress =
            ViewObservable.text((EditText) findViewById(R.id.editPrimaryAddress), true);
    // 住所2をObservable化
    final Observable<OnTextChangeEvent> secondaryAddress =
            ViewObservable.text((EditText) findViewById(R.id.editSecondaryAddress), true);

    // チェックボックスと住所2の必須条件をObservable化
    final Observable<Boolean> secondaryIsValid = 
        Observable.combineLatest(useSecondaryAddress, secondaryAddress,
            new Func2<Boolean, OnTextChangeEvent, Boolean>() {
                @Override
                public Boolean call(Boolean useSecondary, OnTextChangeEvent secondaryAddress) {
                    if (!useSecondary) {
                        return true;
                    }

                    return !TextUtils.isEmpty(secondaryAddress.text);
                }
            });


    // 全部まとめると、
    //  住所1は入力必須、
    //  住所2はチェックボックスがONの時だけ入力必須
    //  必須条件を満たしていたらtrueを流す
    final Observable<Boolean> isValidAll = Observable.combineLatest(primaryAddress, secondaryIsValid,
            new Func2<OnTextChangeEvent, Boolean, Boolean>() {
                @Override
                public Boolean call(OnTextChangeEvent primaryAddress, Boolean isValidSecondary) {
                    if (!isValidSecondary) {
                        return false;
                    }

                    return !TextUtils.isEmpty(primaryAddress.text);
                }
            });


    // 購読、監視
    isValidAll.subscribe(new Observer<Boolean>() {
        @Override
        public void onNext(final Boolean isValid) {
            // 必須条件を満たしていたら「注文を確定する」を有効にする
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    buttonSubmit.setEnabled(isValid);
                }
            });
        }

        @Override
        public void onCompleted() {
        }

        @Override
        public void onError(Throwable e) {
        }
    });
}

 ViewObservable.xxx で、UI要素をObservable化します。これはRxAndroidの機能。これでテキストの変更とか、チェックボックスの変更のたびに、OnNextが発生するようになります。

 Validationでは、RxJavaの機能である Observable.combineLatestがキモで、こいつに2つのObservableを渡してやると、その片方が値が変化した時に、T3 call(T1 a, T2 b) が呼ばれます。T1、T2 は渡すObservableの型、T3は後続へ流す型で、ValidationなのでBooleanです。 上記 secondaryIsValid の実装では、「住所2に配送する」のチェックボックスと「住所2」のテキストボックスの2つのObservableを渡していて、

  • 「住所2に配送する」がOFFなら true を返す
  • 「住所2に配送する」がONで、且つ「住所2」が空でなければ true を返す

としています。

 次に、isValidAll の実装では、「住所1」と secondaryIsValid を渡していて、

  • secondaryIsValidfalse なら false を返す
  • secondaryIsValidtrue で、且つ「住所1」が空でなければ true を返す

という実装です。

 んで、こいつ(isValidAll)を購読(subscribe)すると、onNext にValidationの結果が通知されるので、ボタンのEnabledを切り替えます。

 conbimeLatest は、本家Rxなら obsA.CombineLatest(obsB, (tA, tB) => tX).CombineLatest(obsC, (tX, tC) => tY)... とチェインして書けるのですが、RxJava の combineLatest はなぜか static メソッドしかなくてチェインできません、残念。

まとめ

 このレベルだと、すべてのUI要素に変更通知を仕込んで共通な関数を呼ぶ、的な実装で問題ないですが、要素や条件が増えてくると大変です。

 Observable と combineLatest を使うと、制約の一部を(Observableに)部分化できて、それらを組み合わせるのも自由自在(Observableだから)。

Javaなのでかなり長ったらしくて読みづらいコードになってしまいました。

Xamarin.Android + 本家Reactive Extensions + ReactiveProporty なら、相当スッキリするんだけどなあー