UIの状態管理方法とViewModelイベントの実装例

## はじめに
2021年12月に公式の[アーキテクチャガイド](https://developer.android.com/jetpack/guide?hl=ja)が大幅に更新されました。

この記事では、[UI層](https://developer.android.com/jetpack/guide/ui-layer?hl=ja)の大まかな解説と、[ViewModelイベント](https://developer.android.com/jetpack/guide/ui-layer/events)について、LiveDataとFlowの2パターンでの実装例を示しながら解説を行います。


## UI層の大まかな解説
公式のアーキテクチャガイドをおさらいしてみると、 `UI層、Domain層、Data層`の3つに分かれており、それぞれ`一方向に依存している`ことが分かります。

この一方向に依存しているというのが、後から重要になってくるので覚えておいてください。
![mad-arch-overview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2243505/fc24043d-685f-09e7-595c-aa1bd51d3449.png)

さらに、UI層はViewを表示するUI elemntsと状態管理を行う `State holders`に分かれていることが分かります。
![mad-arch-overview-ui.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2243505/b2bd101a-f006-3d08-c711-12343db5cc63.png)

このStateholderについて少し掘り下げて解説を行います。

### State holdersとは
State holdersは`状態変数を管理するレイヤー`のことであり、状態変数をState holdersに集めることで、状態管理をスッキリと書くことができるようになります。
```kotlin:State holdersの例
data class UiState(
    //　APIからの返り値を格納
    val data: APIResult?,
    // eventを格納
    val events: List<Event> = emptyList(),
    // loadingを格納
    val proceeding: Boolean
)

sealed interface Event {
    object Success : Event
    data class Error(val message: String) : Event
    object NextPage: Event
}
```
このように状態変数を1つにまとめることで、以下のように `1つの変数のみ`で状態管理を行うことができるようになります。
```kotlin:Flow
 private val _uiState = MutableStateFlow(UiState(//初期値))
```
```kotlin:LiveData
private val _uiState = MutableLiveData(UiState(//初期値))
val uiState: LiveData<UiState>
        get() = _uiState
```

## ViewModelイベント
UIの基本構造を理解したところで、続いて大きく変更のあった[ViewModelイベント](https://developer.android.com/jetpack/guide/ui-layer/events)について解説を行います。

ViewModelイベントとは、`ViewModel側からUIの操作を行う`ことを意味しており、 `UIの状態やライフサイクルに沿ってUIを更新する必要がある`という注意点があります。

ゆえに、UIの状態を意識せずにコードを書くと以下のようなAntiPatternに遭遇してしまいます。

### Anti Pattern
`APIからErrorを受け取ったときにダイアログを表示する`という例を用いて、ViewModelイベントでのAntiPatternを紹介します。

**以下のような流れで実装をしてみたとします。**
> APIからErrorを受け取る → LiveDataにErrorの変数を格納する → LiveDataを監視して、ダイアログを表示する

**実装例**
以下のようなSealedClassを用意してErrorの変数を定義し、
```kotlin:UIState
// 結果を返すクラス
sealed class APIResult<out R> {
    // 成功した場合
    data class Success<out T>(val data: T) : APIResult<T>()

    // 失敗した場合
    data class Error(val exception: Throwable) : APIResult<Nothing>()
}
```
ViewModel側で以下のようなLiveDataにErrorを格納し、
```kotlin:ViewModel
// Errorを格納するLiveData
private val _result = MutableLiveData<APIResult?>(null)
val Result: LiveData<APIResult?>
    get() = _weather
```
これを監視して、Errorが生じていたときにダイアログを表示するという実装です。
```kotlin:UI
// LiveDataを監視
viewModel.result.observe(this) {
            // エラーが生じていた場合 -> エラー画像を表示
            is APIResult.Error -> {
                // ダイアログを表示する
                showNoticeDialog()
                Log.e("API fetch error", weather.exception.toString())
            }
```

このような実装は一見問題がないように見えますが、`横画面にすると、ダイアログが2回重なって表示`されてしまうという問題が生じてしまいます。

（２回タップしてダイアログを閉じている様子）
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2243505/28a076a4-ae55-c613-1e78-318fb87fb196.gif" width=400>

**なぜこんなことが起きたのか**

実はこの実装では、横画面になった時にErrorの変数がもう一度参照されてしまうために、ダイアログが再生成されてしまうのです。

詳しい解説をすると、横画面になったときに以下のような[ライフサイクル](https://developer.android.com/guide/components/activities/activity-lifecycle?hl=ja)を辿るため、２回ダイアログが表示されてしまうようです。

> onSaveInstanceState() → Viewの状態を保存 （ここで以前表示されていたダイアログが保存される)
onDestroy() → Viewを破棄
onCreate() → Viewを生成（LiveDataに格納された変数を参照するため、新しくダイアログが生成される）
**→ ダイアログを1個表示**
onStart()
onRestoreInstanceState() → 保存した状態を復元（保存されたダイアログが復元される）
**→ ダイアログを2個表示してしまった…**
onResume()

このような状態を防ぐためには、ダイアログを表示するというViewModelの変数を、`ダイアログが表示された後に破棄する（消費する）`ような実装が必要になってきます。


## イベント管理方法のBetterパターン
イベントを消費する方法について解説を行います。
### かつての方法（現在は未推奨）
以前に推奨されていたイベント管理の方法は、[このDeveloperブログ](https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150)によると

Eventクラスを用意して

```kotlin:Eventクラス
open class Event<out T>(private val content: T) {
    // イベントが消費されているかどうかフラグ
    var hasBeenHandled = false
        private set // 外から値の変更はできない

    // Eventがラップする値がまだ一度も消費されていない場合にnon-nullな値を返す関数
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }
}
```
Event型でViewModelイベントをラップし、
```kotlin:ViewModel側
private val _navigateToDetails = MutableLiveData<Event<String>>()

val navigateToDetails : LiveData<Event<String>>
    get() = _navigateToDetails

```
UIでgetContentIfNotHandledの関数を行うことでイベントの消費を行うというものでした。
```kotlin: UI
// APIResultが来たときに発火する処理
viewModel.navigateToDetails.observe(this) {
    it.getContentIfNotHandled()?.let { APIResult ->
        handleWeatherUpdate(APIResult)
    }
}
```

イベントを消費する関数をEvent型の中に定義することで、適切に変数を破棄するという方法をとっています。

#### 問題点
しかし、この方法では UIelements→Stateholders という`単方向の依存が成り立たなくなってしまう`という問題が生じてしまいます。（少し前までは許容されてたからややこしいよね…）

そこで、以下のような方法が提唱されています。

## 現時点でのBestパターン
公式の方法をさらにスッキリとまとめた記事が[abe kenjiさんのブログ記事](https://star-zero.medium.com/viewmodel%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%81%AE%E5%AE%9F%E8%A3%85-74dd814deb97)で紹介されているため、この記事を参考に現時点でのBestパターンを紹介します。

### 実装の流れ
以下のような流れでViewModelイベントを扱っていきます。

1. 状態をSealed interfaceで意義する
2. UIStateでEventを格納するListを定義する
3. イベントを消費する関数を定義する
4. イベントを発行する
5. View側からイベントを消費する

### Flowでの実装例
上の流れに沿って実装例を紹介します。
(Flowのコードはスタぜろさんの記事からの引用です）

### 1. 状態を定義する
まず、ViewModelイベントで扱う状態を定義します。
```kotlin:状態の定義
sealed interface Event {
    object Success : Event
    data class Error(val message: String) : Event
}
```

### 2. UIStateでEventを格納するListを定義する
Listに変数を格納することがポイント
```kotlin:UiStateに定義
data class UiState(
    // APIからの返り値を格納
    val data: APIResult?,
    // イベントを格納（ここ重要）
    val events: List<Event> = emptyList(),
    // ローディング画面を制御
    val proceeding: Boolean
)
```

### 3. ViewModelでEventを保持する
UIState内にまとめて1つの変数で保持するというのが定石でしたね（上で解説しました）
```kotlin:イベントを消費する関数を定義する
private val _uiState = MutableStateFlow(UiState(//初期値))
val uiState: StateFlow<UiState> = _uiState
```

### 4. イベントを消費する関数をViewModel内に定義する
イベントを消費する関数を`ViewModel内に定義する`というのが単方向の依存においてポイントになります。
```kotlin:イベントを消費する関数を定義する
fun consumeEvent(event: Event) {
    val newEvents = uiState.events.filterNot { it == event }
    uiState = uiState.copy(events = newEvents)
}
```

### 4. ViewModelイベントを発行する
copy関数を用いてイベントの発行を行います。
```kotlin:ViewModelイベントを発行する
// APIから正しい返り値があった場合
val newEvents = uiState.events + Event.Success
_uiState.value.copy(events = newEvents, data = result.data)

// Errorが生じた場合
val newEvents = _uiState.value.events + (Event.Error(result.exception.toString()))
_uiState.value.copy(events = newEvents)
```

### 5. View側からイベントを消費する
View上でUIを更新した後に消費を行うことがViewModelEventの肝となる部分です。
```kotlin:View側からイベントを消費する
viewModel.uiState.events.firstOrNull()?.let { event ->
    when (event) {
        is Event.Success -> {
            LaunchedEffect(event) {
                snackbarHostState.showSnackbar("Success")
                // イベントを消費
                viewModel.consumeEvent(event)
            }
        }
```

## LiveData
上で紹介した方法をLiveDataに落とし込むと以下のようになる。

ただし、初期値を代入できないためにnull対応がめんどくさくなっているので、StateFlowでの実装をおすすめします。

### 1. 状態を定義する
```kotlin:状態の定義
// Flowと同様のため省略
```

### 2. UIStateでEventを格納するListを定義する
```kotlin:UiStateに定義
// Flowと同様のため省略
```

### 3. ViewModel内でEventを保持する
```kotlin:イベントを消費する関数を定義する
private val _uiState = MutableLiveData(UiState(//初期値)
val uiState: LiveData<UiState>
    get() = _uiState
```

### 3. イベントを消費する関数を定義する
```kotlin:イベントを消費する関数を定義する
fun consumeEvent(event: Event) {
        val newEvents = _uiState.value?.events?.filterNot { it == event }
        _uiState.value = uiState.value?.copy(events = newEvents ?: emptyList())
}
```

### 4. イベントを発行する
```kotlin:イベントを発行する
// APIから正しい返り値があった場合
val newEvents = _uiState.value?.events?.plus(Event.Success)
_uiState.value?.copy(events = newEvents ?: emptyList(), data = result.data)

// Errorが生じた場合
val newEvents = _uiState.value?.events?.plus(Event.Error(result.exception.toString()))
_uiState.value?.copy(events = newEvents ?: emptyList())
```

### 5. View側からイベントを消費する
```kotlin:View側からイベントを消費する
viewModel.uiState.observe(this) { uiState ->
if (uiState.events.firstOrNull() != null) {
    when (val event = uiState.events.firstOrNull()) {
        is Event.Success -> {
            // イベントを消費
            viewModel.consumeEvent(event)
        }
        is Event.Error -> {
        ..
}
```

### まとめ
- UI層はUI elemntsと状態管理を行う `State holders`に分かれている。
- State holdersに状態変数をまとめることで、状態管理をスッキリと行うことができる。
- ViewModelイベントの取り扱いには注意が必要であり、イベントの`消費`を行う必要がある。

最後まで読んでいただきありがとうございました。
よければtwitterのフォローよろしくお願いします。
(https://twitter.com/takashiho_2)

### 参考文献
https://star-zero.medium.com/viewmodel%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%81%AE%E5%AE%9F%E8%A3%85-74dd814deb97