RxProperty でイケてる入力フォームをもっとスッキリ実装する

また来てしまいました...。こんにちわ、 RxProperty エヴァンジェリズムアドボケイトの @amay077 です。

* [RxJava,Kotlin,Databindingでイケてる入力フォームをスッキリ実装する | eureka tech blog](https://developers.eure.jp/tech/android_cool_form)

興味深く読ませていただきました。

こちらの記事における ``ViewProperties`` の要点は次の箇所かと。

1. ``ObservableField`` を公開することにより Android DataBinding を活用する
2. RxJava の ``BehaviorSubject.onNext`` で View -> ViewProperties への値の更新通知
3. RxJava のオペレータ(``map`` や ``combineLatest`` など)を使うことで、 「入力項目が valid か？」 を通知する仕組みが簡単に作れる

これらは、 [RxProperty](https://github.com/k-kagurazaka/rx-property-android) を使うともっとスッキリと実装できます！

1. [先日も書いた](https://qiita.com/amay077/items/58e589780ccea6fd5470) 通り、 RxProperty は ``.value`` で **``ObservableField`` に変換できます**
2. ``RxProperty<T>`` 自体は RxJava の ``Observable<T>`` からの派生であり、また最新の値を保持し設定もできます。 ``subscribe`` した時に保持されている値がすぐに notify されるかも選択できるので、 **実質ほぼ ``BehaviorSubject<T>`` です**。
3. ``RxProperty<T>`` は、バリデータも内蔵しており、 ``setValidator((T)->String?)`` に **「値 → エラー文字列への変換関数」 を渡してやるだけで validation ができます**。
4. 「実行された時の処理」と「それが実行できるか？」がセットになった ``RxCommand`` というクラスがあり、これをボタンにバインドしてやるだけで、``Button.enabled`` と ``Button.onClick`` が連動します。

## RxProperty で書いてみた

というわけで、元記事の ``FormProperties`` を、RxProperty を使って書いてみました。うずうずしてガマンできなかった :pray:  。

```kotlin
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()
    }
}
```

## 要点をいくつか

### 基本的なところ

```kotlin
/** ニックネーム */
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]

[^1]: 実は標準の ``.setValidator`` は引数が ``(T)->String`` になっていて ``null`` が返せないので、アプリ内で拡張関数を定義して使っていま、したが RxProperty v4.0.0 で対応してもらえました :tada: 

このエラー値もデータバインドできるようになっていて、 ``android:text="@{props.nickname.error}"`` と書いてバインドできます。

### 表示用に値を変換

```kotlin
/** 誕生日(表示用文字列) */
val birthdayText = birthday.map {
        SimpleDateFormat("yyyy/MM/dd", Locale.JAPAN).format(it.time)
}.toReadOnlyRxProperty()
```

``RxProperty<Calendar>`` 型である ``birthday`` プロパティを Binding や View 側で文字列に変換するのもできるのですが、せっかくなので Rx ライクにいきましょう。 ``.map {}`` でよしなに変換してやるだけです。最後に ``.toReadOnlyRxProperty()`` としているのは、このプロパティは読み取り専用、つまり OneWay Bind しか許可しないことを示しています。

### コマンド

```kotlin
/** 登録ボタンが実行できるか */
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 に減りました :thumbsup: 。
行数の削減というよりも、「値を保持する Subject」、「エラー通知用の ``Observable<bool>``」、「データバインディング用の ObservableField」 をそれぞれ用意しなくてもすべて ``RxProperty<T>`` の宣言だけでできてしまう事が最大のメリットです。

今回のできあがり品はこちらです。

![Untitled.gif](https://qiita-image-store.s3.amazonaws.com/0/8227/de1f7c2b-6ed4-f273-aaf6-a8e5cf67d737.gif)

ソースも公開してるので是非動かして RxProperty の凄さを体験してみてくださいね。 12/8 にリリースされた [RxProperty 4.0.0](https://github.com/k-kagurazaka/rx-property-android/releases/tag/4.0.0) にも超速で対応 :exclamation: 

* [amay077/RxPropertyFormSample](https://github.com/amay077/RxPropertyFormSample)

[^2]: MVVM だと、 ViewModel の中で View に依存する処理（画面遷移とか、Toast/DialogBoxの表示など）を行うのは抵抗がありますが、MVP ならまあやってもいいかもですね。今回は Toast の表示は Activity 側に任せることにして、 ``ViewProperties`` からは EventBus ライクに、 ``LiveData<String>`` で通知をするようにしてみました。