これはまだプロダクションで動かしてないので気をつけてください
AAC ViewModel に生えた LiveData を外に出すとき、フィールドの重複宣言は面倒だし、だからといって直接 expose はしたくないし、それに MutableLiveData や MediatorLiveData を返してしまうと最悪キャストすれば更新できてしまう。
弊社で Android を触ってるのは今の所自分ひとりなので、とりあえず Mutable で expose して速度を優先しているのだけど、将来的にメンバーが増えたときとかはそうも言ってられない。
もちろんキャストとか ViewModel 外での更新はコードレビューで弾けるようなものなのだけど、「してはいけない」タイプの原則を人間が指摘しなければならないのは無駄だなーと個人的には思っている。また今回の問題は Lint で簡単に検出できる(caller の先祖要素に ViewModel がいないことを見ればいい)ので静的解析でチェックしてもいいんだけど、そうじゃない方法を考えてみたい。
と思ってたときに以下の記事が出てきた。
MutableなLiveDataを特定のクラス外から更新できなくする · stsnブログ
わかる と思ったのだけど、複数のパッケージに跨る LiveData と ViewModel とかも存在する可能性があって、その場合にはこの方法が使えないのでそこらへんを解決してる別の方法を考えてみた。(正直AACを絡めた設計はまだ考えているという段階で、将来的にそういうケースが出てくるかも?程度の可能性でしかない)
- APT
- Compiler plugin
とかも考えたけど、これは結局メンテできるひとがいなくなると黒魔術を越えた失われし秘術になってしまうので却下。Kotlin versionに合わせて動かなくなったら面倒くさいし。
ということでViewModel とその派生クラスからのみ更新するにはやはり可視性を protected にするという点を利用する。また Observable#hide
のように、別の実体に変換して対応する。
import android.app.Application import androidx.annotation.CallSuper import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer open class ViewModel(application: Application) : AndroidViewModel(application) { protected fun <T> LiveData<T>.mutate(): MutableLiveData<T> { return when (this) { is LiveDataWrapper -> this.wrappedLiveData is MutableLiveData -> this else -> throw IllegalArgumentException("the given live data is not wrapped. Please use liveData delegation at declarations.") } } protected inline fun <reified T> ViewModel.lazyLiveData(crossinline initializer: () -> LiveData<T>): Lazy<LiveData<T>> { return lazy { val liveData = initializer() if (liveData is MutableLiveData) { LiveDataWrapper(liveData) } else { liveData } } } protected inline fun <reified T> ViewModel.liveData(liveData: LiveData<T>): Lazy<LiveData<T>> { return lazyOf( if (liveData is MutableLiveData) { LiveDataWrapper(liveData) } else { liveData } ) } protected class LiveDataWrapper<T>(val wrappedLiveData: MutableLiveData<T>) : LiveData<T>() { private val observer: Observer<T> = Observer { super.setValue(it) } @CallSuper override fun onActive() { wrappedLiveData.observeForever(observer) } @CallSuper override fun onInactive() { wrappedLiveData.removeObserver(observer) } override fun postValue(value: T) { wrappedLiveData.postValue(value) } override fun setValue(value: T) { wrappedLiveData.value = value } } }
この ViewModel を使って、任意の ViewModel を構築していく。
class TestViewModel(application: Application) : ViewModel(application) { val exposedLiveData: LiveData<Int> by liveData { MutableLiveData<Int>() } fun testUpdate() { // compilation error : cannot access the function // exposedLiveData.value = 0 exposedLiveData.reify().setValue2(2) } } fun test(application: Application) { val vm = TestViewModel(application) vm.apply { // compilation error : cannot access the function // exposedLiveData.reify() // compilation error : cannot access the function // exposedLiveData.value = 0 // will crash exposedLiveData as MutableLiveData<*> } }
画面更新有りのサンプル
http://jmatsu.hatenablog.com/entry/2018/12/01/171740 · GitHub
こんな感じでやればいけそうな気がする。Reflectionによる更新はさすがに考えない。
ところで Kotlin の List/MutableList は JDK との都合上で仕方ないとして、LiveData/MutableLiveData のネーミングはどうにかならなかったのだろうか・・・
最初に貼ってたやつは wrapper を更新していて、何の意味もなかった。
package xxxx import android.app.Application import androidx.annotation.AnyThread import androidx.annotation.CallSuper import androidx.annotation.MainThread import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.Observer open class ViewModel(application: Application) : AndroidViewModel(application) { // ViewModel クラス内でのみ Wrapper クラスを参照できるようにする protected fun <T> LiveData<T>.reify(): LiveDataWrapper<T> { return requireNotNull(this as? LiveDataWrapper) { "the given live data is not wrapped. Please use liveData delegation at declarations." } } // LiveData を wrap する delegation を用意する protected inline fun <reified T> ViewModel.lazyLiveData(crossinline initializer: () -> LiveData<T>): Lazy<LiveData<T>> { return lazy { LiveDataWrapper(initializer()) } } protected inline fun <reified T> ViewModel.liveData(liveData: LiveData<T>): Lazy<LiveData<T>> { return lazyOf(LiveDataWrapper(liveData)) } // LiveData を wrap して、MutableLiveData 及びその派生を直接 expose しない。 protected class LiveDataWrapper<T>(private val liveData: LiveData<T>) : LiveData<T>() { private val observer: Observer<T> = Observer { setValue2(it) } @CallSuper override fun onActive() { liveData.observeForever(observer) } @CallSuper override fun onInactive() { liveData.removeObserver(observer) } // Reflection での更新を防止しておく。 // KotlinのDeprecatedを使って、誤った利用を避ける。 @Deprecated( message = "use postValue2 instead. this method always cause a crash", replaceWith = ReplaceWith( "postValue2(value)", imports = ["xxxx.ViewModel.LiveDataWrapper.postValue2"] ), level = DeprecationLevel.ERROR ) override fun postValue(value: T) { throw IllegalAccessError("cannot be invoked even through reflection") } @Deprecated( message = "use setValue2 instead. this method always cause a crash", replaceWith = ReplaceWith( "setValue2(value)", imports = ["xxxx.ViewModel.LiveDataWrapper.setValue2"] ), level = DeprecationLevel.ERROR ) override fun setValue(value: T) { throw IllegalAccessError("cannot be invoked even through reflection") } // 更新用のメソッドを新たに生やしておく @AnyThread fun postValue2(value: T) { super.postValue(value) } @MainThread fun setValue2(value: T) { super.setValue(value) } } }