もやもやエンジニア

IT系のネタで思ったことや技術系のネタを備忘録的に綴っていきます。フロント率高め。

KotlinとDataBindingとMVVMとか

※ こちらもどうぞ 2018/7/30 追記 AACのViewModel使ってFluxする - もやもやエンジニア

今までDateBindingをButterKnifeの代わりのような使い方しかしてなかったので、ちゃんとMVVMっぽい作りもやってみようということで前に作ったアプリをごそっと書き換えてみました。アプリはKotlin製です。

rei19.hatenablog.com

そもそものMVVM

  • 以下の記事・スライドを参考にしてます。こういうアーキテクチャ系の話の原点って何を探ればいいのかちょっとわからなかったので印象に残った記事を読みこんでます。

ugaya40.hateblo.jp

qiita.com

techblog.reraku.co.jp

techblog.reraku.co.jp

speakerdeck.com

  • 自分の中で噛み砕いたポイントとしては以下の点
    • MVVMのコンテキストでのModelはプロパティの公開と戻り値のないメソッドだけを実装している。ここでいうModelは例えばDDDのコンテキストでのDomainModelのようなものを指しているわけではないので切り離して考えること。
    • ViewModelはModelを監視している。メソッドの呼び出しによりModelの状態が変更された場合はその変化を翻訳してViewに伝える。
    • Modelは内部の状態が更新されたらViewModelに更新通知を送る
  • ※ 勘違いしてたらコメントください。。。

とりあえず作ってみよう

  • このアプリでははてぶの情報を取得するために、対象のユーザーのはてなのIDをSharedPreferencesに持っていていつでも更新することができます。なのでできることはユーザーIDの公開、ユーザーIDの更新です。

Model

  • ModelはシングルトンでViewModelにDIしてます。このアプリではユーザーIDを設定するダイアログとその呼び元のActivityのViewModelそれぞれでUserModelの更新イベントを監視しています。なのでユーザーIDが更新されたら呼び元のActivityも同時に更新されます。

UserModel.kt

    var user: UserEntity = getUserFromPreferences()
        private set(value) {
            field = value
            storeUserToPreferences(value)
            userUpdatedEventSubject.onNext(value)
        }

    // イベントを流す用のPublishSubject
    private val userUpdatedEventSubject = PublishSubject.create<UserEntity>()
    private val unauthorisedEventSubject = PublishSubject.create<Unit>()
    private val errorSubject = PublishSubject.create<Unit>()

    // PublishSubjectをObservableに変換して公開。PublishSubjectのままだとonNextが呼び元で使えてしまう
    val userUpdatedEvent: Observable<UserEntity> = userUpdatedEventSubject
    val unauthorisedEvent: Observable<Unit> = unauthorisedEventSubject
    val error: Observable<Unit> = errorSubject

    fun setUpUserId(userId: String) {

        // ここは http://b.hatena.ne.jp/[UserId]/ を叩いて200が返って来るかを確認して存在するユーザーかチェックしている
        hatenaBookmarkService.userCheck(userId).map {
            // 特定の記号が入っている場合にTopページを取得しているケースがあるのでトップページが返ってきたら 存在しないユーザー = 404として扱う
            return@map !it.contains("<title>はてなブックマーク</title>")
        }.onErrorResumeNext {
            if (it is HttpException) {
                when (it.code()) {
                    HttpURLConnection.HTTP_NOT_FOUND -> {
                        Single.just(false)
                    }
                    else -> {
                        Single.error(it)
                    }
                }
            } else {
                Single.error(it)
            }
        }.subscribeAsync({ isValidId ->
            // Model内でスレッドを指定して配信。subscribeAsyncは拡張関数
            if (isValidId) {
                // 有効なIDだったら渡されたIDを内部に保存
                user = UserEntity(userId)
            } else {
                // 無効なIDだったら認証できなかったイベントを発行
                unauthorisedEventSubject.onNext(Unit)
            }
        }, {
            // 通信エラーなどの場合は一律エラーのイベントを発行
            errorSubject.onNext(Unit)
        })
    }
  • ここでModel内のUserが更新されたらSharedPreferecesに保存して、userUpdatedEventを発行してます。
    var user: UserEntity = getUserFromPreferences()
        private set(value) {
            field = value
            storeUserToPreferences(value)
            userUpdatedEventSubject.onNext(value)
        }

ViewModel

  • ViewModelはonStartでModelのイベントの監視を開始、onStopで解除という感じにしてます。
  • DataBinding周りでProgressDialogとかSnackBarとか表示したい時はEventBus経由でActivityに通知しています。。。が、もっといいやり方がある気がします。
    • TwitterでアドバイスをもらってProgressDialogはViewModelにDIしたり、Activity/Fragmentにどうしても何かさせたい場合はViewModelがEventを公開してActivity/Fragment側でそれを監視するようにしました。EventBusは見通しが悪くなるのでこちらの方が追いやすいですね。

EditUserIdDialogFragmentViewModel.kt

    override fun onStart() {
        super.onStart()
        // Modelから通知されるイベントを翻訳してViewに伝える
        registerDisposable(userModel.userUpdatedEvent.subscribe {
            progressDialog.dismiss()
            idErrorMessage.set("")
            userId.set(it.id)
            dismissDialogEventSubject.onNext(Unit)
        }, userModel.unauthorisedEvent.subscribe {
            progressDialog.dismiss()
            idErrorMessage.set(userIdErrorMessage)
        }, userModel.error.subscribe {
            progressDialog.dismiss()
            snackbarFactory?.create(R.string.message_error_network)?.show()
        })
    }

    override fun onResume() {
        super.onResume()
        userId.set(userModel.user.id)
    }

    fun onClickSetUp(view: View) {
        progressDialog.show()
        userModel.setUpUserId(userId.get())
    }

おわり

  • Modelのメソッド呼び出しの結果をイベントベースで受け取るのはメソッドでObservableを返すよりわかりやすいかなと思いました。その場合はエラーハンドリングでどんなエラーが流れてくるか内部の実装を知っていなければならないので発生しうるイベントとして公開されてる方がModelを呼ぶ側は使いやすいです。

  • コードはこちら。主要な画面だけDataBindingとViewModelで書き直してます(わりと前に書いたものがベースなのでちょっと変なところがあるかも)。

続編のようなもの

rei19.hatenablog.com