142
100

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Android その2Advent Calendar 2017

Day 4

LiveData について勘違いしていたことをいくつか

Last updated at Posted at 2017-12-04

Android Architecture Components(以下 AAC) に含まれる 「LiveData」 のサンプルを Kotlin で書いてみました。

LiveData とは

LiveData とは、「ライフサイクルに応じて自動的に購読解除してくれる通知プロパティ」です。

モダン(と呼ぶにはもはや古い?)なUIパターンでは、UI側はデータの変更を検知して自身を書き換えます。
すなわちUI側では、「データを購読する」というコードを書くわけですが、「購読をやめる」というコードも合わせて考えなければなりません。しかし Android の Activity や Fragment はライフサイクルが複雑で、購読を管理するのも一苦労であり、不具合の温床になりがちです。

LiveData は AAC に含まれる Lifecycle と深く結びつき、この「購読解除」をほとんど自動的に行なってくれます。

作ってみたアプリ

いったい何人が、何回つくったのだろうと思われる GitHub のレポジトリ一覧を表示するやつです。

Screenshot_1509709626.png

主な要素は次の通りです。

  • 依存関係は MainActivity -> MainViewModel -> GithubRepository -> GithubService(Retrofit)
  • MainViewModel は「ユーザー名」と「リポジトリリスト」を LiveData として公開。あと load メソッドも。
  • MainActivity は自身の EditText や ListView と、MainViewModel の LiveData をデータバインド(自作)
  • MainViewModel は load が呼ばれたら、GithubRepository を使用してリポジトリ一覧を 非同期で 要求し、取得できたら自身の LiveData を更新する。
  • GithubRepository では、 Retrofit を使ってるだけ。 コールバックをUIスレッドで受けないように callbackExecutor を設定してます。
  • coroutine(async/await) や RxJava、 DataBinding は使ってないです。なるべく LiveData のみのシンプルな方針で。

LiveData はライフサイクルと結びついて購読管理をしてくれるモノ、ということは ViewModel で使うのが自然かなと。Google のサンプルもそうなってたし。

LiveData で勘違いしてたこと

さて、 LiveData を実際に使ってみたところ「マジか・・・」と思った点がいくつかあったので挙げてみます。

「変更通知」じゃなくて、ただの「垂れ流し」だった

私が期待していたのは「DataBinding の ObservableFieldのように使える、且つ、購読管理が楽」というモノだったんですけど、ObservableField と決定的に違うのがここでした。

ObservableField は、値が 変更された時 に通知を行います。
LiveData は、値が 変更されていなくても設定されれば 通知を行います。

val observableFld = ObservableField<Int>()
observableFld.set(5)
observableFld.set(5)
observableFld.set(5) // 最初の1回しか通知されない(=onChangedは呼ばれない)

val liveData = MutableLiveData<Int>()
liveData.postValue(5)
liveData.postValue(5)
liveData.postValue(5)// 3回とも通知される(=onChangedが呼ばれる)

ViewModel が持つプロパティは、値が変わった時に通知し、View側はそれを検知して更新する。
が常識なので、LiveData もてっきりそうなってるのかと思ってましたが、違いました(だって on Changed だったし…)。

この事を知っておかないと、以下のようなリスクがあります。

  • ムダな画面の更新が発生する
  • TwoWay バインディングを何も考えずに作ると無限ループで死ぬ

後者は、このサンプル作成で体験しました。
画面の EditText と ViewModel の LiveData の TwoWay バインディングを次のように「何も考えず」実装しました。

// viewModel.user の TwoWay バインド
// EditText -> LiveData
editUserName.addTextChangedListener(object : TextWatcher {
    override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
        val userName = editUserName.text;
        viewModel.user.postValue(userName.toString())
    }

    override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { }
    override fun afterTextChanged(p0: Editable?) { }
})

// LiveData -> EditText
viewModel.user.observe(this, Observer { userName ->
    editUserName.setTextKeepState(userName ?: "")
})
editUserName.setTextKeepState(viewModel.user?.value ?: "")

このコードは、初回の editUserName.setTextKeepState をトリガーに、TextWatcher.onTextChanged と LiveData.observe が無限に繰り返されます。

今回は、TextWatcher.onTextChanged で同値チェックを行って回避しましたが、予期せぬトラブルの元になりそうです。

setValue はUIスレッドで呼び出さないといけない(postValue を使おう)

LiveData の実際に値を設定できるクラスである MutableLiveData<T> には、 setValue()postValue() の2つの値更新メソッドがあります。

はじめは setValue() しか知らなくてそれを使っていたのですが、非UIスレッドで(Retrofitのコールバックスレッドで) setValue() を使用したら IllegalなんとかException が出ました。
LiveData のコードを追ってみると、次図のようにUIスレッドかどうかをチェックして例外を出していました。

しかしその後、 @kkagurazaka さんに postValue() もあるよ! と教えてもらいました。

postVata は、値の更新と通知をUIスレッド上で行ってくれるメソッドです。

モデル側の処理は大抵は非同期すなわち非UIスレッドで行われることを前提にすると思うので、setValue は事実上使えないでしょう。

そして、LiveData は UIスレッドに強く依存しているので、ViewModel から「向こう側」では使うべきではないでしょう。

逆に LiveDataobserve は、UIスレッドで行われることが保証されているので、わざわざ runOnUiThread などをする必要はなさそうです。

ObservableField と併用不可?

さて DataBinding には BaseObservale の基底クラスまたは ObservavleField が必要です。 AAC を使う= ViewModel を基底クラスにすることが多いと想定されるので前者は実質死亡。となれば DataBinding したければ ObservableField を使うしかありません。しかし ObservableField と LiveData は現在はなんの関係もないクラスなので、

  • DataBinding したいなら ObservableField
  • Lifecycle aware なコードを書きたいなら LiveData

という使い分けをしなければなりません。目的が違うとは言え、なんだか微妙です。

2017/12/21 追記

なんと LiveData が DataBinding に対応するようです。つまり ObservableField<T> は要らない子になる可能性?

You can now use a LiveData object as an observable field in data binding expressions.(続く)

変更通知でなく「値を垂れ流すだけ」である LiveData をデータバインディングできるって、どうなっちゃうのか、すごく興味ありますね。続報を追っかけましょう。

Solutions?

文句言ってるだけでは何の解決にもならないので、現状打てる手を模索してみます。
Kotlin ならば拡張メソッドが使えるので、便利な拡張メソッドを作って使えばいいんじゃないかと。

「値が変わった時だけ」通知を行う LiveData の拡張メソッドを作る

まず、「LiveData は値の変更に関係なく通知されてしまう」については、値が変わったかをチェックして、変わっていた時だけ通知を行うような拡張メソッドを作ってみました。

fun <T> LiveData<T>.observeOnChanged(owner: LifecycleOwner, observer: Observer<T>) : Unit {
    var prev : T? = null
    this.observe(owner, Observer<T> {
        if (!(prev?.equals(it) ?: false)) {
            observer.onChanged(it)
        }
        prev = it
    })
}

// 使う方
val liveData = MutableLiveData<Int>()

liveData.observeOnChanged(owner, Observer {
    Log.d(TAG, "$it")
});

liveData.postValue(5)
liveData.postValue(5)
liveData.postValue(5)// 最初の1回しか onChanged は呼ばれない

やっぱり Observable がイイ!

LiveData の購読管理が楽になるところは良いんですけど、 mapswitchMap(flatMap かな) など最低限の合成メソッドしかない点や、DataBinding との併用が面倒そうな点は微妙です。

ViewModel の向こう側(Usecase層や Repository層)からの I/F は Observable<T> あるいは、それと相互変換可能なモノにしたいと考えると、ViewModel でも Observable<T> を使いたいものです。

ということで、ObservableField と LiveData と RxJava をイイ感じで一緒に使う方法を考えてみたので、明日の 「RxJava Advent Calendar 2017 day 5」 に書きます!

142
100
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
142
100

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?