1
1

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 3 years have passed since last update.

MVVM + DataBinding でViewModelからクリックイベントを通知する

Last updated at Posted at 2021-03-16

#この記事について
MVVM + DataBindingを使用する際に、ViewModel内でクリック処理を記述することがあるかと思います。その中には画面遷移など、ViewModelからActivityもしくはFragmentへクリックイベントを通知するケースも存在します。
今回はLiveDataの状態監視を用いてクリックイベントをハンドリングしていきます。

#【NGケース】直接LiveDataで遷移状態を保持してみる

まずは悪い例から挙げてみます。
例えば、以下のように直接LiveData内で遷移信号を保持した場合

FirstViewModel.kt
class FirstViewModel : ViewModel {
    private val _navigateToSecond = MutableLiveData<Boolean>()

    val navigateToSecond : LiveData<Boolean>
        get() = _navigateToSecond


    fun userClicksOnButton() {
        _navigateToSecond.value = true
    }
}

View(Activity or Fragment)では以下のように受け取ることになります。

FirstActivity.kt
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クラスで状態管理する

Event.kt
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を実装していきます。

FirstViewModel.kt
class FirstViewModel : ViewModel {
    private val _navigateToSecond = MutableLiveData<Event<String>>()

    val navigateToSecond : LiveData<Event<String>>
        get() = _navigateToSecond


    fun userClicksOnButton() {
        _navigateToSecond.value = Event("ToSecond") 
    }
}
FirstActivity.kt
firstViewModel.navigateToSecond.observe(this, Observer {
    it.getContentIfNotHandled()?.let { // イベントが処理されていない場合のみ発火
        startActivity(SecondActivity...)
    }
})

これでFinishです!

Eventクラスに渡す引数を変えることで、同一のLiveDataオブザーバーから遷移先を分けることもできます↓↓

FirstViewModel.kt
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") 
    }
}
FirstActivity.kt
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)

1
1
0

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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?