また来てしまいました...。こんにちわ、 RxProperty エヴァンジェリズムアドボケイトの @amay077 です。
興味深く読ませていただきました。
こちらの記事における ViewProperties の要点は次の箇所かと。
-
ObservableFieldを公開することにより Android DataBinding を活用する - RxJava の
BehaviorSubject.onNextで View -> ViewProperties への値の更新通知 - RxJava のオペレータ(
mapやcombineLatestなど)を使うことで、 「入力項目が valid か?」 を通知する仕組みが簡単に作れる
これらは、 RxProperty を使うともっとスッキリと実装できます!
-
先日も書いた 通り、 RxProperty は
.valueでObservableFieldに変換できます -
RxProperty<T>自体は RxJava のObservable<T>からの派生であり、また最新の値を保持し設定もできます。subscribeした時に保持されている値がすぐに notify されるかも選択できるので、 実質ほぼBehaviorSubject<T>です。 -
RxProperty<T>は、バリデータも内蔵しており、setValidator((T)->String?)に 「値 → エラー文字列への変換関数」 を渡してやるだけで validation ができます。 - 「実行された時の処理」と「それが実行できるか?」がセットになった
RxCommandというクラスがあり、これをボタンにバインドしてやるだけで、Button.enabledとButton.onClickが連動します。
RxProperty で書いてみた
というわけで、元記事の FormProperties を、RxProperty を使って書いてみました。うずうずしてガマンできなかった
。
class FormProperties {
enum class Gender(val id :Int) {
MAN(0), WOMAN(1), OTHER(2), NOT_SET(9)
}
private val disposables = CompositeDisposable()
/** ニックネーム */
val nickname = RxProperty<String>("")
.setValidator {
if (it.length < 2 || it.length > 10)
// エラーの場合はその説明を、エラーなしの場合は null を返却
"ニックネームは2文字以上10文字以下にしてください" else null }
/** 誕生日(Rawデータ) */
val birthday = RxProperty<Calendar>(Calendar.getInstance())
.setValidator {
if (it >= Calendar.getInstance().apply { add(Calendar.YEAR, -18 ) }) "18歳以上が必要です" else null
}
/** 誕生日(表示用文字列) */
val birthdayText = birthday.map {
SimpleDateFormat("yyyy/MM/dd", Locale.JAPAN).format(it.time)
}.toReadOnlyRxProperty()
/** 性別(Rawデータ) */
val gender = RxProperty<Gender>(Gender.NOT_SET)
.setValidator { if (it == Gender.NOT_SET) "性別を何か選択してください" else null }
/** 性別(表示用文字列) */
val genderTextResId = gender.map {
when (it) {
Gender.MAN -> R.string.male
Gender.WOMAN -> R.string.female
Gender.OTHER -> R.string.other
else -> R.string.empty
}
}.toReadOnlyRxProperty()
/** 利用規約同意 */
val isAgreed = RxProperty<Boolean>(false)
/** Toast を通知するためだけの LiveData */
private val _toast = MutableLiveData<String>()
val toast : LiveData<String> = _toast
/** 登録ボタンが実行できるか */
private val canRegistration : Observable<Boolean> = Observable
.combineLatest(listOf(
nickname.onHasErrorsChanged().map { !it },
gender.onHasErrorsChanged().map { !it },
birthday.onHasErrorsChanged().map { !it },
isAgreed),
{ anyList -> anyList.map { it as Boolean }.all { it }})
/** 登録ボタンを押したときのコマンド */
// canRegistration が true の時だけ実行可能なコマンド
val register = canRegistration.toRxCommand<NoParameter>()
.apply { this.subscribe {
// RxCommand の subscribe が呼ばれた時 = ボタンが押された時
// とりあえずトースト投げる
_toast.postValue("RegistrationCompleteActivity へ移動するよ")
}.addTo(disposables) }
fun dispose() {
disposables.clear()
}
}
要点をいくつか
基本的なところ
/** ニックネーム */
val nickname = RxProperty<String>("")
.setValidator {
if (it.length < 2 || it.length > 10)
// エラーの場合はその説明を、エラーなしの場合は null を返却
"ニックネームは2文字以上10文字以下にしてください" else null }
これはニックネームを入力する EditText がバインドするプロパティです。
Android DataBinding の場合は、レイアウトXMLで android:text="@={prop.nickname.value}" なんて書きます。
.setValidator() でバリデータを設定しています。ここでは入力値が 2文字未満または10文字より長い場合はエラーメッセージを返し、そうでない場合はエラーがない事を示す null を返します。1
このエラー値もデータバインドできるようになっていて、 android:text="@{props.nickname.error}" と書いてバインドできます。
表示用に値を変換
/** 誕生日(表示用文字列) */
val birthdayText = birthday.map {
SimpleDateFormat("yyyy/MM/dd", Locale.JAPAN).format(it.time)
}.toReadOnlyRxProperty()
RxProperty<Calendar> 型である birthday プロパティを Binding や View 側で文字列に変換するのもできるのですが、せっかくなので Rx ライクにいきましょう。 .map {} でよしなに変換してやるだけです。最後に .toReadOnlyRxProperty() としているのは、このプロパティは読み取り専用、つまり OneWay Bind しか許可しないことを示しています。
コマンド
/** 登録ボタンが実行できるか */
private val canRegistration : Observable<Boolean> = Observable
.combineLatest(listOf(
nickname.onHasErrorsChanged().map { !it },
gender.onHasErrorsChanged().map { !it },
birthday.onHasErrorsChanged().map { !it },
isAgreed),
{ anyList -> anyList.map { it as Boolean }.all { it }})
/** 登録ボタンを押したときのコマンド */
// canRegistration が true の時だけ実行可能なコマンド
val register = canRegistration.toRxCommand<Nothing>()
.apply { this.subscribe {
// RxCommand の subscribe が呼ばれた時 = ボタンが押された時
// とりあえずトースト投げる
_toast.postValue("RegistrationCompleteActivity へ移動するよ")
}.addTo(disposables) }
登録ボタンは、「ニックネーム」、「性別」、「誕生日」がすべて valid であり、さらに 「利用規約に同意」 が true である場合にだけ押すことができる仕様です。
それを定義しているのが canRegistration : Observable<Boolean> です。 「valid かどうか?」 は、 nickname.onHasErrorsChanged().map { !it } のように、「エラーがあるか?」を「エラーがないか?」に反転するだけで表せます。これらを元記事のように Observable.combineLatest でまとめてあげて「入力項目が全て true なら登録ボタンは押せる」となります。
登録ボタンが押されたときの処理は、 register : RxCommand<T> の .subscribe に書きます。ここでは Kotlin の便利な .apply 関数を使って、プロパティの定義とともに書けますね。
実際のボタンが押された処理は、「xxxへ移動するよ」というトーストを表示させるために LiveData に通知を送っています。Activity 側で LiveData を observe して Toast.show を呼んでいます。2
ボタンを RxCommand にバインドするには、レイアウトXMLに app:rxCommandOnClick="@{props.register}" と書きます。これだけで、登録ボタンは、入力項目が全てvalidになるまでは disabled になります。
まとめ
ViewProperties を RxProperty を使って書き直してみたところ、行数は 88 から 68 に減りました
。
行数の削減というよりも、「値を保持する Subject」、「エラー通知用の Observable<bool>」、「データバインディング用の ObservableField」 をそれぞれ用意しなくてもすべて RxProperty<T> の宣言だけでできてしまう事が最大のメリットです。
今回のできあがり品はこちらです。
ソースも公開してるので是非動かして RxProperty の凄さを体験してみてくださいね。 12/8 にリリースされた RxProperty 4.0.0 にも超速で対応 ![]()
-
実は標準の
.setValidatorは引数が(T)->Stringになっていてnullが返せないので、アプリ内で拡張関数を定義して使っていま、したが RxProperty v4.0.0 で対応してもらえました
↩ -
MVVM だと、 ViewModel の中で View に依存する処理(画面遷移とか、Toast/DialogBoxの表示など)を行うのは抵抗がありますが、MVP ならまあやってもいいかもですね。今回は Toast の表示は Activity 側に任せることにして、
ViewPropertiesからは EventBus ライクに、LiveData<String>で通知をするようにしてみました。 ↩
