Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
181 lines (136 sloc) 10.4 KB
layout title date comments categories
post
RxProperty でイケてる入力フォームをもっとスッキリ実装する
2017/12/08 23:59:00 +0900
true
Android
Kotlin
RxJava

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

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

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

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

これらは、 RxProperty を使うともっとスッキリと実装できます!

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

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

このエラー値もデータバインドできるようになっていて、 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> の宣言だけでできてしまう事が最大のメリットです。

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

Untitled.gif

ソースも公開してるので是非動かして RxProperty の凄さを体験してみてくださいね。 12/8 にリリースされた RxProperty 4.0.0 にも超速で対応 ❗️

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