#この記事について
MVVM + DataBindingを使用する際に、ViewModel内でクリック処理を記述することがあるかと思います。その中には画面遷移など、ViewModelからActivityもしくはFragmentへクリックイベントを通知するケースも存在します。
今回はLiveDataの状態監視を用いてクリックイベントをハンドリングしていきます。
#【NGケース】直接LiveDataで遷移状態を保持してみる
まずは悪い例から挙げてみます。
例えば、以下のように直接LiveData内で遷移信号を保持した場合
class FirstViewModel : ViewModel {
private val _navigateToSecond = MutableLiveData<Boolean>()
val navigateToSecond : LiveData<Boolean>
get() = _navigateToSecond
fun userClicksOnButton() {
_navigateToSecond.value = true
}
}
View(Activity or Fragment)では以下のように受け取ることになります。
firstViewModel.navigateToSecond.observe(this, Observer {
if (it) startActivity(SecondActivity...)
})
このときの問題は、_navigateToSecondが長期間trueのままとなってしまうことです。
そうなると、例えば遷移先であるSecondActivityから戻ってきた際に再びObserverがアクティブになり、SecondActivityへと再度遷移してしまいます。
解決策として、userClicksOnButtonを
fun userClicksOnButton() {
_navigateToSecond.value = true
_navigateToSecond.value = false
}
のように変えることもできますが、LiveDataでは受け取ったすべての値を放出することを保証されていません。例えば、Observerがアクティブでないときに値が設定されることがあり、その場合は新しい値が置き換えられるだけです。
それに、正直何をやってるのかわかりづらい。。。
なので、これはダメ!
そこで、Eventクラスを作成します。
#【OKケース】Eventクラスで状態管理する
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set
/**
* contentを返却すると同時に、再度使用できないようにする
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* すでに処理されている場合でも、contentを返す
*/
fun peekContent(): T = content
}
**getContentIfNotHandled()**では、呼び出し元にcontentを返すと同時に、再度使用されることを防いでいます。
反対に、**peekContent()**を使用する場合には、すでに一度呼び出されていてもcontentを返すようになっています。
このEventクラスを使ってViewModelとViewを実装していきます。
class FirstViewModel : ViewModel {
private val _navigateToSecond = MutableLiveData<Event<String>>()
val navigateToSecond : LiveData<Event<String>>
get() = _navigateToSecond
fun userClicksOnButton() {
_navigateToSecond.value = Event("ToSecond")
}
}
firstViewModel.navigateToSecond.observe(this, Observer {
it.getContentIfNotHandled()?.let { // イベントが処理されていない場合のみ発火
startActivity(SecondActivity...)
}
})
これでFinishです!
Eventクラスに渡す引数を変えることで、同一のLiveDataオブザーバーから遷移先を分けることもできます↓↓
class FirstViewModel : ViewModel {
private val _navigate = MutableLiveData<Event<String>>()
val navigate : LiveData<Event<String>>
get() = _navigate
fun clickButtonToSecond() {
_navigate.value = Event("ToSecond")
}
fun clickButtonToThird() {
_navigate.value = Event("ToThird")
}
}
firstViewModel.navigate.observe(this, Observer {
it.getContentIfNotHandled()?.let { event ->
when(event){
"ToSecond" -> startActivity(SecondActivity...)
"ToThird" -> startActivity(ThirdActivity...)
}
}
})
#参考記事
LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)