UIStateの基本とさまざまな工夫　

# この記事は
日々の開発の中で「これもUI Stateなの？！」とか「こんな書き方ができるんだ！」というものが増えてきたのでその知見をまとめたものです。

# UI Stateって何？
**UIの状態を表すひとかたまりのclass**と考えます。

## 私が思ってた UI State
少し前まで私が思っていたUI Stateは次のようなイメージです。
- ComposeでUIを実装した時に使うやつ
    - Android Viewの時は使わない！
- その画面に表示する全てのデータ(UIの状態)を１つのクラスにまとめたもの
    - `HogeUiState`みたいな命名の`data class`とか`sealed class`的なの

しかし、実際はAndroid Viewの場合でもUI Stateの概念は有効に活用させることができます。

## なぜAndroid Viewの場合でもUI Stateが使える？
もしこの記事を読んでいる皆さんの中に、かつての私のように

> - ComposeでUIを実装した時に使うやつ
>    - Android Viewの時は使わない！

このような考えを持っている方がいることを想定して、少し説明させてください。

## Android Viewで良いUI StateとダメなUI State
結論から言うと、 

*Composeの場合によく見るような１つの画面の状態を全て１つのクラスにまとめるようなUI Stateの作り方はAndroid Viewには向いていません。しかし、画面の中の一部のUIの状態をUI Stateとして管理することはAndroid Viewにおいても非常に有効* 

です。

少し見づらい図で申し訳ありませんが、下の図の場合について考えてみましょう。
これは私が個人開発で考えたアプリの画面で、2%くらい実装してすぐやめちゃったやつです。

Composeで実装する場合、このホーム画面に表示する全てのデータの状態を１つのクラス(UI State)として管理しても良いですが、Android Viewの場合は、１つの画面のUI状態を1つのクラスにまとめてしまうのは危険です。代わりに、例えば図の`PROFILE CARD`のように、ひとかたまりのUIに対してUI Stateを作成すると良いです。

![スクリーンショット 2023-06-09 1.22.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906559/815f6ea7-5204-5ba5-4144-8d3f8cb6987f.png)

### なぜAndroid Viewでは１つの画面に表示するデータの状態をUI Stateとして全て1つのクラスにまとめるのが推奨されないか

#### Android Viewの特徴

AndroidViewの場合、**AndroidView自体が状態を持つ** ことになります。
このことを **ステートフル(stateful)** といいます。

Android ViewであるTextViewにsetText(hoge)した時のことを考えてみてください。このとき、TextViewは表示内容textをクラス内部に覚えています。Compose登場以前のViewは状態をもつこと（ステートフルであること）が当たり前でした。

##### 状態の二重管理
Android Viewのようにステートを内部で持つViewシステムの場合、UIStateでの状態管理とAndroid Viewでの状態管理で **状態の二重管理** に注意しなければなりません。

１画面全体のUI状態を１つのUIStateクラスにまとめることは便利ですが、UIStateでの状態管理とAndroid Viewでの状態管理で、どちらの状態が正であるのか管理の問題が発生します。

ロジックとしてはUIStateがもつ状態が正であるべきですが、表示されるUIを考えるとAndroid Viewで表示している情報こそが正となるからです。

##### レンダリングコスト
さらにレンダリングコストの問題もあるでしょう。例えばAndroid Viewでbinding.hogeText.text = hogeなどのように書かれている場合、変数hogeの中の値が変化していなくても更新されてしまうようなレンダリングの仕組みになっているので、UIStateの中に値が変化していないプロパティが含まれていたとしても、UIState全体で更新処理が行われてしまいます。

しかし、たくさんあるコンポーネントを全てViewModelでハンドリングするのは大変です。
なので全画面ではなく、あるまとまったUIの状態をUIStateとして管理する分では効果を発揮します。

#### Composeの特徴

一方Composeの場合、**Compose自体は状態を持っていません**。
これを **ステートレス(stateless)** といいます。

Compose自体が状態を持っていないので、UIStateに状態を管理させています。
だからAndroidViewと違ってComposeはUIStateと相性がいいのです。

また、Composeの場合だとAndroidViewと違い、`Composable`の引数に渡された値が変化した時のみ再レンダリングされるような仕組みになっています。

そのため、１画面のUI状態をUI Stateとして１つのクラスにまとめたとしても、過剰な更新が起こりません。

# 基本！：一番簡単なUIState

世界で一番カンタンなUIStateをみていきましょう。

例えばプロフィール画面のツイートのUIStateを考えてみます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906559/88add565-57f5-d893-575e-ce20b42d8603.jpeg" width=600>

- アイコン
- ユーザー名
- ユーザーID
- ツイート内容
- ツイート投稿時間
- コメント数
- いいね数
- リツイート数
- （ツイートアクティビティ数）

などがこのツイートアイテムのもっている情報です。

これをUIStateにしたものがこちらです。

```kotlin
data class TweetItemUiState(
	val userIcon: Hoge,
	val userName: String,
	val userId: String,
	val tweet: String,
	val postDate: String,
	val replyCount: String,
	val reTweetCount: String,
	val favorirteCount: String,
) {

}
```
（🚨 このサンプルコードでは、ツイートアクティビティ数は無視しています）

至って単純です。ただ、そのUIに必要なデータの状態を１つのクラスとしてまとめているだけです。

このように作られたUIStateは、データに変更があった場合ViewModelで更新されます。

```kotlin
class HomeViewModel: viewModel() {
	private val _uiState: MutableStateFlow<TweetItemUiState>
	val uiState: StateFlow = _uiState 

    init {
        // リプの数を更新する
        _uiState.update{ it.copy( replyCount = replyCount ) }
    }
}
```
（🚨 このサンプルコードはあくまで例なので、現実的な実装ではありません）

世界で一番カンタンなUIStateは**そのUIに表示するデータの状態を１つのクラスにまとめただけのもの**です。
これを基本としてもう少しUIStateを活用できる事例を紹介していきます。

# 中級！：データに関する処理はデータの近くに寄せる
以前にもQiitaで書いた内容なのですが、**データを加工するロジックをUIState内に置く**ことによって、例えばこのUIStateを他の場所でも使い回したい場合に、ロジックごと使い回すことができます。

逆に言えば、データを加工するロジックをUIState内に置かない場合、他の場所でもUIStateを使いまわしたいけど、そのUIStateが持っているデータに関連するロジックはコピペして持ってくることになります。

そうすると同じロジックが複数の場所に散らばり、仮にそのロジックに変更が入った場合複数の場所からそのロジックを探し出して修正しなければなりません。修正漏れがあれば、バグにつながります。

例えば、基本編のUIStateは`postDate`を持っていますが、ツイートアイテムに表示する時は実際の時刻ではなく、今から何時間/ 何日前のツイートなのかを表示しています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906559/56d53f3c-e3f6-af8d-ca84-13750ebb3650.jpeg" width=500>

例として、取得してきた情報をUI上に表示するために、`postDate`を加工して現在時刻から何時間前なのかを表示するロジックをUIStateに実装してみます。

```kotlin
data class TweetItemUiState(
	val userIcon: Hoge,
	val userName: String,
	val userId: String,
	val tweet: String,
	val postDate: String,
	val replyCount: String,
	val reTweetCount: String,
	val favorirteCount: String,
) {
    private val now = LocalDateTime.now()
    val hoursAgo = Duration.between(postDate, now).toHours()
}
```
（🚨 雰囲気でコードを書いています）

参考：[Androidで「ロジックとデータの一体化」を実践してみた](https://qiita.com/mi_iroha/items/fdc035600cefc7cadf84)

# 発展！：UIとしては同じだけど必要なデータが微妙に違う時にできる工夫

例えば次のようなカードを出しわけて画面に表示する場合を考えます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906559/904bf86a-724c-3115-9a01-dd89de5fe1a5.png" width=300>



この画面には３パターンのカードがあります。
上から順番にみていきましょう。

1. ユーザーがまだ1つもchallengesに取り組んでいない場合（`NotCompleted`）
    表示するもの
    - タイトル（`dialogTopic`）
    - challengesの数（`challengesCount`）


2. ユーザーがchallengesに取り組んだ場合（`Completed`）
    表示するもの
    - タイトル（`dialogTopic`）
    - challengesの数（`challengesCount`）
    - ユーザーが取り組んだchallengesの数（`completedChallengesCount`）

3. 未課金の場合（`Locked`）
    表示するもの
    - 鍵アイコンを表示
    - タイトル（`dialogTopic`）
    - challengesの数（`challengesCount`）

このように、ほとんど同じようなUIではありますがパターンによって微妙に表示させる内容が変わります。
こういった場合、どういうUIStateを作成するのが効率的でしょうか。

**状態を複数もつ**ということになると、data classだけではその状態を表現することができません。
そういった場合には、`sealed class`を使うことによって表示するカードそれぞれに過不足なく必要なデータを渡してあげられます。

次のサンプルコードは`sealed class`と`data class`の両方を活用して複数の状態をもつ場合のUIStateを実装しています。

```kotlin
sealed interface Item

sealed class DialogCardItem: Item {
    // sealed class内の全ての子クラスで使いたいプロパティはabstractにする
    abstract val dialogTopic: String
    abstract val challengesCount: Int

    data class Completed(
        // abstractで宣言したプロパティはoverrideして使う
        override val dialogTopic: String,
        override val challengesCount: Int,
        val completedChallengesCount: Int,
    ): DialogCardItem()

    data class NotCompleted(
        override val dialogTopic: String,
        override val challengesCount: Int,
    ): DialogCardItem()

    data class Locked(
        override val dialogTopic: String,
        override val challengesCount: Int,
    ): DialogCardItem()

    companion object {
        // create()はView側で呼び出す
        fun create(
            isLocked: Boolean,
            dialogTopic: String,
            challengesCount: Int,
            completedChallengesCount: Int,
        ): DialogCardItem {
            return if (isLocked) {
                Locked(
                    dialogTopic, 
                    challengesCount,
                )
            } else {
                if (completedChallengesCount == 0) {
                    NotCompleted(
                        dialogTopic,
                        challengesCount,
                    )
                } else {
                    Completed(
                        dialogTopic, 
                        challengesCount, 
                        completedChallengesCount, 
                    )
                }
            }
        }
    }
}
```
上記のサンプルコードについてもう少し詳細に解説していきます。

まず３つの状態（`Locked`, `NotCompleted`, `Completed`）自体はそれぞれ`data class`で表現したいです。

```kotlin
data class Completed(
    /* nothing */
): DialogCardItem()

data class NotCompleted(
    /* nothing */
): DialogCardItem()

data class Locked(
    /* nothing */
): DialogCardItem()
```

ですが、`Locked`, `NotCompleted`, `Completed`自体は全く別のUI状態ではなく、表示するデータが少し異なるだけでほとんど同じです。例えば、タイトル（`challengesCount`）やchallengesの数（`dialogTopic`）はどの状態のカードでもほしいデータです。

なので、３つの`data class`を１つの`sealed class`で管理し、`sealed class`のプロパティとしてタイトル（`challengesCount`）とchallengesの数（`dialogTopic`）をabstractで定義し、子の`data class`でそれらを継承します。これで必要なプロパティを必ず`data class`内で定義しないといけないようにさせることができます。要するに必須データの実装漏れが防げます。

```kotlin
sealed interface Item

sealed class DialogCardItem: Item {
    // sealed class内の全ての子クラスで使いたいプロパティはabstractにする
    abstract val dialogTopic: String
    abstract val challengesCount: Int

    data class Completed(
        // abstractで宣言したプロパティはoverrideして使う
        override val dialogTopic: String,
        override val challengesCount: Int,
        val completedChallengesCount: Int,
    ): DialogCardItem()

    data class NotCompleted(
        override val dialogTopic: String,
        override val challengesCount: Int,
    ): DialogCardItem()

    data class Locked(
        override val dialogTopic: String,
        override val challengesCount: Int,
    ): DialogCardItem()
}
```

あとはこの３つの状態をどのようなロジックで出し分けるかです。

まず、課金済みか未課金状態かどうかを出し分けるために、`isLocked`というBooleanのフラグが必要になりそうです。

あとは、ユーザーがchallengesに取り組んだかどうかの判断ができないと`Completed`と`NotCompleted`の出し分けもできないので`completedChallengesCount`というフラグも必要そうです。

例えば`completedChallengesCount`が0の場合はユーザーがchallengesに未着手なので`NotCompleted`の状態のカードを表示させることになりそうです。

あとは、`data class`を出し分ける際に、生成する`data class`のコンストラクタに渡す引数として`dialogTopic`と`challengesCount`も必要になりそうです。

これらを踏まえてcreateメソッドを以下のように実装し、`data class`の出しわけを行います。


```kotlin
companion object {
    // create()はView側で呼び出す
    fun create(
        isLocked: Boolean,
        dialogTopic: String,
        challengesCount: Int,
        completedChallengesCount: Int,
    ): DialogCardItem {
        return if (isLocked) {
            Locked(
                dialogTopic, 
                challengesCount,
            )
        } else {
            if (completedChallengesCount == 0) {
                NotCompleted(
                    dialogTopic,
                    challengesCount,
                )
            } else {
                Completed(
                    dialogTopic, 
                    challengesCount, 
                    completedChallengesCount, 
                )
            }
        }
    }
}
```

createメソッドはstaticメソッドとしてcompanion object内で作成し、View側(Activity/Fragment)から呼び出しを行います。

次のコードはサンプルコードなので、適当なデータを詰めているわけですが、createメソッドの使い方としてはこんな感じです。
View側からcreateを呼び出して必要なデータを引数に渡します。

```kotlin
// Fragment
val items = listOf(
    DialogAdapter.DialogCardItem.create(
        isLocked = false,
        dialogTopic = "At Hotel in Paris",
        challengesCount = 2,
        completedChallengesCount = 1,
        topicImage = "hogehoge"
    ),
    DialogAdapter.DialogCardItem.create(
        isLocked = false,
        dialogTopic = "On Sunday",
        challengesCount = 2,
        completedChallengesCount = 2,
        topicImage = "hogehoge"
    ),
    DialogAdapter.DialogCardItem.create(
        isLocked = true,
        dialogTopic = "Picnic with friends",
        challengesCount = 2,
        completedChallengesCount = 0,
        topicImage = "hogehoge"
    )
)
binding.dialogRecycler.adapter = DialogAdapter(items)
```
そうするとcreateメソッド内で、受け取った引数をもとにどの`data class`を出すのかを判定します。

⏬ createメソッドの中身

```kotlin
fun create(
    isLocked: Boolean,
    dialogTopic: String,
    challengesCount: Int,
    completedChallengesCount: Int,
): DialogCardItem {
    // 課金済みかどうか
    return if (isLocked) {
        Locked(
            dialogTopic, 
            challengesCount,
        )
    } else {
        // challengesに着手したかどうか
        if (completedChallengesCount == 0) {
            NotCompleted(
                dialogTopic,
                challengesCount,
            )
        } else {
            Completed(
                dialogTopic, 
                challengesCount, 
                completedChallengesCount, 
            )
        }
    }
}

```

このようにして、**複数の状態の出しわけを行いたい場合のUIState**では`sealed class`内に状態に応じた`data class`を用意し、出しわけのロジック自体は`companion object`にstaticメソッドとして持たせるという方法があります。


# 最後に
今回はさまざまな場面でのUIStateの活用について記事を書いてみましたが、いかがでしたでしょうか。
UIStateは奥が深く、UIの状態というものをどのような切り口で考えるかによっても構成が大きく異なりますが、今回は一般的な形から基本をベースにした応用的な工夫まで紹介いたしました。
この記事がみなさんの日々の開発に少しでも貢献できれば幸いです。