例えばショッピングサイトとかの発送先指定のフォーム『登録されている住所とは違う住所に送りたい時、「別の住所に送る」をチェックする、すると「住所2」が必須入力となり、入力するまで次へ進めない』的なちょっと込み入ったValidationをReactive ExtensionsのJava版、RxJavaとRxAndroidでやってみました。
動作イメージ
まずいきなり動作結果から。
- 住所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
を渡していて、
-
secondaryIsValid
がfalse
ならfalse
を返す -
secondaryIsValid
がtrue
で、且つ「住所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 なら、相当スッキリするんだけどなあー