おでーぶでおでーぶ

いろいろ書く。いろいろ。

静的解析(Lint等)に頼らず、ViewModelに載ってる LiveData を ViewModel 外で更新させないようにすることを考える

これはまだプロダクションで動かしてないので気をつけてください

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)
        }
    }
}