🖥️

画面の状態を UI State で管理する

2024/03/21に公開1

UI State で状態を管理する理由

これまでの私は ViewModel のインスタンスを Composable に渡し、その内部から ViewModel のプロパティやメソッドに直接アクセスしていました。しかし、この実装では Composable が ViewModel の実装内容を把握している必要がありました。
UI の状態を UI State としてまとめ、関数をラムダ式によって渡すアプローチをとることにより、UI は ViewModel の実装内容を把握せずに渡された UI State をもとに画面を構築し、ボタンのクリックなどのイベントの発生時には渡された関数を実行するというシンプルで ViewModel に依存しない実装にすることが可能になります。

簡単なアプリを作ってためす

ボタンを押して UI State を更新し、UI に反映させるだけのアプリです。


https://github.com/dennoko/UI_State_Test

アプリの構成

UI State

今回、UI State は以下のように定義しています。

data class AppUiState(
    val count: Int = 0,
    val txt: String = "",
    val numList: List<Int> = listOf()
)

単純な Int や String の変更の観察と、List の変更を UI に反映させることを目的とします。

ViewModel

UI State のインスタンスを MutableStateFlow で管理します。

private val _appUiState = MutableStateFlow(AppUiState())

val appUiState = _appUiState.asStateFlow()

StateFlow を使い UI State を流します。UI State の変更は ViewModel を介してしか行えないようにします。

StateFlow が変更を通知するのは、対象のインスタンスが変更された時です。そこで UI State の値を変更する際に、データクラスにの copy() メソッドを用いて、値を変更した新しいインスタンスを生成して代入します。
例えば count を増やすメソッドは以下のようになります。

fun countUp() {
    _appUiState.update { it.copy(count = it.count + 1) }
}

ここでは、元々のインスタンスに対して copy メソッドを使用し、値を変更したインスタンスを生成して代入しています。
StateFlow ではこのように変更を監視できるので、List のようなコレクション型に対しても同様に値の変更を伝えることができます。

fun addNumList() {
    _appUiState.update { it.copy(numList = it.numList + listOf(it.numList.size)) }
}

UI

Composable 関数は ViewModel を受け取りません。

@Composable
fun AppScreen(
    uiState: AppUiState,
    countUp: () -> Unit,
    addHoge: () -> Unit,
    addListItem: () -> Unit,
    changeListItem: () -> Unit,
    reset: () -> Unit
) {
// ここで uiState の値を使用して UI を描写し、引数として与えられたメソッドを呼びだす
}

メソッドもラムダ式で渡すことで、UI は ViewModel を意識しなくて済みます。

MainActivity

MainActivity では、ViewModel のインスタンスを作成し、Composable に UI State とメソッドを渡します。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val vm: AppViewModel by viewModels()

        setContent {
            val uiState by vm.appUiState.collectAsState()

            UI_State_TestTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    AppScreen(
                        uiState = uiState,
                        countUp = { vm.countUp() },
                        addHoge = { vm.addTxt() },
                        addListItem = { vm.addNumList() },
                        changeListItem = { vm.changeNumList() },
                        reset = { vm.reset() }
                    )
                }
            }
        }
    }
}

さいごに

UI の状態を UI State に集約し、Composable 関数が状態やアクションをラムダ式として受け取り ViewModel の詳細を知る必要が無くなったことで、Composable 関数の実装時に考えることが減るということと、状態の更新を UI State の更新のみで行うことで再コンポーズの管理がしやすくなったように感じました。

私は今回が初めての記事投稿でしたが、今年はこのように学んだ内容をまとめていきたいと思います。
最後まで読んでいただきありがとうございました。

今回作成したアプリのコードはこちらにあります。
https://github.com/dennoko/UI_State_Test

Discussion