概要
ViewModel / LiveData / Observer を使用するとViewModelからのイベントを次のようにView側(Activity/Fragment)に通知することができます。
しかし、通常の実装では 一度だけ実行したい処理(Fragmentの遷移時等)に不都合 が発生する場合があります。
SampleFragmentでボタンクリック→ViewResultFragment遷移→戻るボタンでSampleFragmntに戻る
この様な状況では SampleFragmentがObserveしているLiveDataの値が代入されているので再度ViewResultFragmentに遷移する処理が走ってしまいます。
このような問題を解決するために今回はEventラッパークラスを作成して対応していくサンプルを作成したいと思います。
問題の挙動を確認してみる
ここでは簡単にボタンをタップするとViewModelでデータを取得後、次Fragmentに遷移し、先ほど取得したデータを表示するサンプルを作成します。
class SampleFragment : Fragment() {
private val viewModel: SampleViewModel by viewModels()
// <略>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setObserver()
binding.btn.setOnClickListener {
viewModel.fetchData()
}
}
private fun setObserver() {
viewModel.fetchData.observe(viewLifecycleOwner) { resultData ->
val transaction = parentFragmentManager.beginTransaction()
transaction.replace(R.id.container, ViewResultFragment(resultData))
transaction.addToBackStack(null)
transaction.commit()
}
}
}
class SampleViewModel : ViewModel() {
private var _fetchData = MutableLiveData<String>()
val fetchData: LiveData<String>
get() = _fetchData
fun fetchData() {
viewModelScope.launch {
delay(1000) // データ取得仮実装
_fetchData.value = "result Data"
}
}
}
class ViewResultFragment(
private val data: String
) : Fragment() {
// <略>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.testText.text = data
}
}
この様な実装ではViewResultFragmentからバックボタンで戻ろうとすると一瞬SampleFragmentが表示されますが、再度ViewResultFragmentに遷移する処理が走ってしまいます。
簡単に対処してみる
fun fetchData() {
viewModelScope.launch {
delay(1000) // データ取得仮実装
_fetchData.value = "result Data"
_fetchData.value = null
}
}
private fun setObserver() {
viewModel.fetchData.observe(viewLifecycleOwner) { resultData ->
resultData?.let {
val transaction = parentFragmentManager.beginTransaction()
transaction.replace(R.id.container, ViewResultFragment(resultData))
transaction.addToBackStack(null)
transaction.commit()
}
}
}
ViewModelでLiveDataに値を代入後、 再度null を代入。
Observer側ではresultDataがnullでなければ処理を続行する。
この用に書くと一応理想の挙動にはなる。
ただ、LiveDataに値を代入後、毎度nullを再代入するのもかっこ悪いしなあ。。。
もっとスマートに書きたいと思って色々調べて見たところ、対処方法を見つけたので実装していきます。
Event 拡張関数を作成する
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
fun peekContent(): T = content
}
このクラスは非常にシンプルな構造になっており、簡単にEventラッパークラスを説明すると、初めて値を取り出す場合はその値を返し、値を取り出したことがある場合はnullを返します。
Observer側ではEventから値を取り出してnullでなければ処理を実行するように実装します。
private var _fetchData = MutableLiveData<Event<String>>()
val fetchData: LiveData<Event<String>>
get() = _fetchData
fun fetchData() {
viewModelScope.launch {
delay(1000) // データ取得仮実装
_fetchData.value = Event("result Data Event Wrapper")
}
}
private fun setObserver() {
viewModel.fetchData.observe(viewLifecycleOwner) {
it.getContentIfNotHandled()?.let { result ->
val transaction = parentFragmentManager.beginTransaction()
transaction.replace(R.id.container, ViewResultFragment(Event(result)))
transaction.addToBackStack(null)
transaction.commit()
}
}
}
さいごに
今回はLiveDataで戻るボタンで前画面に遷移した際に再度同じ処理が走ってしまう問題を解決し、更にEventラッパーを使用することでLiveDataにnullを代入する実装も避けることができました。
しかし、observer側でnullチェックをする処理は変わっていないので更にスマートになる方法があればまた記事にしたいと思います。
ではまたっ!!
参考