Experiments Never Fail

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

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

動作イメージ #

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

こんな仕様です。

実装 #

@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を渡していて、

としています。

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

という実装です。

んで、こいつ(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 なら、相当スッキリするんだけどなあー

published at tags: RxJava RxAndroid Android