【Android】ViewModel内でUiModelを使おう(その2)[UiModel編]

# はじめに
[前回](https://qiita.com/saitoyusuke/items/baef4f93f4a052b9639b)でsealed classを使ってViewModelをすっきり書けるようになりました。今回はさらにすっきり書けるようにUiModelについて説明します。

# 対象
- [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ja)を利用している方
- MediatorLiveDataを利用している方
- kotlinが読める方

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

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

# UiModelとは？
簡単に言うと「複数のLiveDataをMediatorLiveDataで合体させて、まとめて観測する仕組み」です。DroidKaigi2020の[ConferenceApp](https://github.com/DroidKaigi/conference-app-2020)に使われているので、参考にすると良いです。UiModelを使うときは[この拡張関数](https://github.com/DroidKaigi/conference-app-2020/blob/master/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt)をプロジェクト内に置きましょう。
UiModelについてはAndroidアプリエンジニア共通認識ではない為、複数人で開発しているプロジェクトに導入する際は注意しましょう。使いこなすのがちょっと難しいので開発スピードの鈍化やバグを生んでしまう可能性もあります。またMediatorLiveDataを使いこなしている人には不要かもしれません。

# 追加のLiveDataを用意する
[前回](https://qiita.com/saitoyusuke/items/baef4f93f4a052b9639b)の状態からさらに画面内にRecyclerViewにList<String>を表示する必要があるとします。

```Kotlin:MyViewModel
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クラスを定義します。

```Kotlin: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を宣言する

```Kotlin:MyViewModel
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を観測する

```Kotlin:MyFragment
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){}`で済むようになりました。