10
10

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 1 year has passed since last update.

【Android】LiveDataでの初期化処理をEvent<T>で対応する

Last updated at Posted at 2022-09-05

概要

ViewModel / LiveData / Observer を使用するとViewModelからのイベントを次のようにView側(Activity/Fragment)に通知することができます。

しかし、通常の実装では 一度だけ実行したい処理(Fragmentの遷移時等)に不都合 が発生する場合があります。

SampleFragmentでボタンクリック→ViewResultFragment遷移→戻るボタンでSampleFragmntに戻る

この様な状況では SampleFragmentがObserveしているLiveDataの値が代入されているので再度ViewResultFragmentに遷移する処理が走ってしまいます。

このような問題を解決するために今回はEventラッパークラスを作成して対応していくサンプルを作成したいと思います。

問題の挙動を確認してみる

ここでは簡単にボタンをタップするとViewModelでデータを取得後、次Fragmentに遷移し、先ほど取得したデータを表示するサンプルを作成します。

SampleFragment.kt
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()
        }
    }
}

SampleViewModel.kt
class SampleViewModel : ViewModel() {

    private var _fetchData = MutableLiveData<String>()
    val fetchData: LiveData<String>
            get() = _fetchData

    fun fetchData() {
        viewModelScope.launch {
            delay(1000) // データ取得仮実装
            _fetchData.value = "result Data"
        }
    }
}
ViewResultFragment.kt
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に遷移する処理が走ってしまいます。

test10.gif

簡単に対処してみる

SampleViewModel.kt
    fun fetchData() {
        viewModelScope.launch {
            delay(1000) // データ取得仮実装
            _fetchData.value = "result Data"
            _fetchData.value = null
        }
    }
SampleFragment.kt
    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でなければ処理を続行する。
この用に書くと一応理想の挙動にはなる。

test7.gif

ただ、LiveDataに値を代入後、毎度nullを再代入するのもかっこ悪いしなあ。。。
もっとスマートに書きたいと思って色々調べて見たところ、対処方法を見つけたので実装していきます。

Event 拡張関数を作成する

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でなければ処理を実行するように実装します。

SampleViewModel.kt
    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")
        }
    }
SampleFragment.kt
    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()
            }
        }
    }
test8.gif

さいごに

今回はLiveDataで戻るボタンで前画面に遷移した際に再度同じ処理が走ってしまう問題を解決し、更にEventラッパーを使用することでLiveDataにnullを代入する実装も避けることができました。

しかし、observer側でnullチェックをする処理は変わっていないので更にスマートになる方法があればまた記事にしたいと思います。

ではまたっ!!

参考

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?