MutableなLiveDataを特定のクラス外から更新できなくする

LiveDataの値を更新したい時、MutableLiveDataを使うのが一般的だと思います。

class MainViewModel {
    val hoge = MutableLiveData<Int>()
}

この書き方だと、外のクラスから値を更新することが可能です。

val viewModel = MainViewModel()

// ok
viewModel.hoge.postValue(10000)

外のクラスからは更新出来ないようにするためにはLiveDataに型変換する必要があります。

例えば次のように書きます。

class MainViewModel {
    private val _hoge = MutableLiveData<Int>()
    val hoge: LiveData<Int> = _hoge // ここでLiveDataに型変換
}

こうすることで、外のクラスからはMutableLiveDataが直接見えなくなり、明示的に型変換をしない限りLiveDataの値を更新できなくなります。

ただこの書き方はフィールドの定義が増えるのでとてもめんどくさいです。 なので、それの解決策を以下で紹介します。

その1

まずコードをのせます。

abstract class ViewModel2 {
  protected fun <T> ViewModelLiveData2<T>.postValue(value: T) {
    postValue(value)
  }

  protected fun <T> ViewModelLiveData2<T>.setValue(value: T) {
    setValue(value)
  }
}
// ViewModel2と同じパッケージに定義
public class ViewModelLiveData2<T> extends LiveData<T> {
  @Override
  protected void postValue(T value) {
    super.postValue(value);
  }

  @Override
  protected void setValue(T value) {
    super.setValue(value);
  }
}

が定義になります。次に使い方です。

class MainViewModel2 : ViewModel2() {
  val userName = ViewModelLiveData2<String>()

  fun update() {
    userName.setValue("test")
    userName.postValue("test2")
  }
}

fun main2() {
  val viewModel = MainViewModel2()

  // compile error!!
  // viewModel.userName.setValue("")

  viewModel.update()
  viewModel.userName.observeForever { }
}

ViewModelLiveData2ViewModel2を作りました(名前は適当です)。

ViewModelLiveData2クラスでpostValueメソッドとsetValueメソッドをオーバーライドし、 ViewModel2クラスと同じパッケージに入れることで、ViewModel2からそれらのメソッドをコール出来るようになり、 ViewModel2を継承したクラスからのみLiveDataの値を更新できます。

viewModel.userName.setValue("")とクラス外からsetValueメソッドをコールするとコンパイルエラーになります。

protectedメソッドが同一パッケージ内からアクセスすることが出来ることを利用したコードになります。

その2

こちらもまずコードをのせます。

abstract class ViewModel3 {
  protected fun <T> ViewModelLiveData3<T>.postValue(value: T) {
    internalPostValue(value)
  }

  protected fun <T> ViewModelLiveData3<T>.setValue(value: T) {
    internalSetValue(value)
  }
}
class ViewModelLiveData3<T> : LiveData<T>() {
  internal fun internalPostValue(value: T) {
    postValue(value)
  }

  internal fun internalSetValue(v: T) {
    value = v
  }
}

が定義になります。次に使い方です。

class MainViewModel3 : ViewModel3() {
  val userName = ViewModelLiveData3<String>()

  fun update() {
    userName.setValue("test")
    userName.postValue("test2")
  }
}

fun main3() {
  val viewModel = MainViewModel3()

  // compile error
  // viewModel.userName.setValue("")

  viewModel.update()
  viewModel.userName.observeForever { }
}

ViewModelLiveData3ViewModel3を作りました(名前は適当です)。

ViewModelLiveData3クラスとViewModel3クラスを適当なサブモジュール内で定義します。 そして、Kotlinのinternalを修飾子を使うことで、外のモジュールからは直接値を更新することができなくなります。

viewModel.userName.setValue("")とクラス外からsetValueメソッドをコールしようとするとコンパイルエラーになります。

補足

abstract classをinterfaceにして上記のメソッドをデフォルトメソッドにすると次のように書くことが出来ます。

interface ViewModel2 {
  fun <T> ViewModelLiveData1<T>.setValue(value: T) {
    this.value = value
  }

  fun <T> ViewModelLiveData1<T>.postValue(value: T) {
    postValue(value)
  }
}

fun main() {
  ...

  // compile error
  // viewModel.userName.setValue("")

  // ok
  with(viewModel) {
    userName.setValue("")
  }
}

applyやwithを使ってViewModel2がreceiverになると、setValueメソッドがコール出来るため、外から値を更新することが出来てしまいます。

まとめ

今回の検証に用いたサンプルコードはここにあります😃

Written by