はじめに

前回でsealed classを使ってViewModelをすっきり書けるようになりました。今回はさらにすっきり書けるようにUiModelについて説明します。

対象

  • ViewModelを利用している方
  • MediatorLiveDataを利用している方
  • kotlinが読める方

ViewModel、MediatorLiveDataについてはドキュメントがネット上に多くありますので、ここでは説明しません。


LiveDataが複数必要になった

前回はうまくsealed classを使って1つのLiveDataに複数の意味を持たせて、まとめることができました。しかしこれ以上にLiveDataが必要になった場合はどうしましょう?1つの画面内でAPIから取得したデータをRecyclerViewを表示し、現在の状態をTextViewに表示し、Toolbarメニューの表示内容を切り替えることもあるでしょう。そうすると複数のLiveDataがどうしても必要になります。そのような場合にUiModelを使ってみましょう。

UiModelとは?

簡単に言うと「複数のLiveDataをMediatorLiveDataで合体させて、まとめて観測する仕組み」です。DroidKaigi2020のConferenceAppに使われているので、参考にすると良いです。UiModelを使うときはこの拡張関数をプロジェクト内に置きましょう。
UiModelについてはAndroidアプリエンジニア共通認識ではない為、複数人で開発しているプロジェクトに導入する際は注意しましょう。使いこなすのがちょっと難しいので開発スピードの鈍化やバグを生んでしまう可能性もあります。またMediatorLiveDataを使いこなしている人には不要かもしれません。

追加のLiveDataを用意する

前回の状態からさらに画面内にRecyclerViewにListを表示する必要があるとします

class MyViewModel : ViewModel() {
    private val _textState = MutableLiveData<TextState>(TextState.Loading)
    val textState: LiveData<TextState> = _textState
    private val _listItems = MutableLiveData<List<String>>(listOf())
    val listItems: LiveData<List<String>> = _listItems
    …
}

UiModelを用意する

MyViewModel内のLiveDataをまとめるMyUiModelクラスを定義します。

data class MyUiModel(
    val textState: TextState = TextState.Loading,
    val listItems: List<String> = listOf()
) {
    companion object {
        operator fun invoke(
            current: MyUiModel,
            textState: TextState,
            listItems: List<String>
        ) = MyUiModel(
            textState = textState,
            listItems = listItems
        )
    }
}  

ViewModelにUiModelを宣言する

class MyViewModel : ViewModel() {
    // UiModelを利用する時はMutableLiveDataの初期化時に初期データを入れてください
    private val _textState = MutableLiveData<TextState>(TextState.Loading)
    // UiModelを利用する時はLiveData不要
    //val textState: LiveData<TextState> = _textState

    // UiModelを利用する時はMutableLiveDataの宣言時に初期データを入れてください
    private val _listItems = MutableLiveData<List<String>>(listOf())
    // UiModelを利用する時はLiveData不要
    //val listItems: LiveData<List<String>> = _listItems

    val uiModel: LiveData<MyUiModel> by lazy {
        combine(
                MyUiModel(),
                _textState,
                _listItems
        ) { current, textState, listItems ->
            MyUiModel(
                    current = current,
                    textState = textState,
                    listItems = listItems
            )
        }
    }
    …
}

※MutableLiveDataを上記のように初期化するにはimplementation "androidx.lifecycle:lifecycle-extensions:$version"が必要です


FragmentでUiModelを観測する

class MyFragment : Fragment() {
    override fun onViewCreated((view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        myViewModel.uiModel.observe(viewLifecycleOwner) { uiModel ->
            // uiModel.textState表示処理
            textView.text = when (uiModel.textState) {
                is TextState.Loading -> "loading"
                is TextState.Loaded -> textState.text
            }
            // uiModel.listItems表示処理
            recyclerViewAdapter.notifyDataSetChanged()
            listItemsCount.text = uiModel.listItems.size.toString()
        }
    }
    …
}

ViewModel側でpostValueする

ViewModel側でデータの更新があった場合は_textState.postValue()、_listItems.postValue()を実行する度にUiModelを観測しているFragmentが発火します。
例えば初期状態から_textState.postValue(TextState.Loaded(“loaded”))を実行すると_textStateは初期状態のTextState.LoadingからTextState.Loadedに値が変化したので、_textState = TextState.Loaded(“loaded”)及び_listItems = listOf()がFragmentに返されます。
さらにその状態から_listItems.postValue(listOf(“first”, “second”))を実行すると_listItemsは初期状態のlistOf()からlistOf(“first”,”second”)に値が変化したので、textState = TextState.Loaded(“loaded”)及び_listItems = listOf(“first”,”second”)がFragmentに返されます。


まとめ

今まではデータの数が増える度にLiveDataとそれに対応するMutableLiveDataを用意していましたが、UiModelを使うことでViewModel内ではLiveDataのみ宣言しUiModelで合体、Fragment側ではLiveDataの数だけ観測するコードも不要になり、一度のviewModel.uiModel.observe(viewLifecycleOwner){}で済むようになりました。