Open8

Jetpack ComposeのViewModelでCompose Stateを使うべきか、StateFlowを使うべきか

AniokraitAniokrait

Jetpack Composeで使える状態を扱うものたち

Jetpack Composeは専用に用意されたCompose State以外にも、StateFlowやLiveData、RxJavaが使える。
RxJavaは時代の流れ的にもう採用されることはないと思うし、LiveDataもStateFlowの方が上位互換として使えるので実質選択肢はCompose StateとStateFlowの2択。

AniokraitAniokrait

RxJava: 学習コストが高い。直感的にコードが読みづらい。記述量が多い。
LiveData: バックグラウンドスレッドで実行されているかメインスレッドで実行されているか意識する必要がある。NullableなのでNullを意識する必要がある。

AniokraitAniokrait

Compose StateとStateFlowの書き方の比較

ViewModel

class MainViewModel: ViewModel() {
    //StateFlowの宣言
    private val _flowCount = MutableStateFlow(0)
    val flowCount = _flowCount.asStateFlow()

    //Compose Stateの宣言
    var composeCount by mutableStateOf(0)
        private set

    fun generateNewComposeCount() {
        val newCount = Random.nextInt(1000)
        composeCount = newCount
    }

    fun generateNewFlowCount() {
        val newCount = Random.nextInt(1000)
        _flowCount.value = newCount
    }
}

StateFlowの方は変数を2つ宣言する必要があり少し冗長。
一方Compose Stateはsetterをprivate宣言するだけで良く、StateがViewModel内からしか更新されないことが自然に記述されており認知不可が軽い。

UI

class MainActivity: ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val vm: MainViewModel = viewModel()
            val composeCount = vm.composeCount
            val flowCount by vm.flowCount.collectAsState()

            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.spacedBy(8.dp),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Button(onClick = { vm.generateNewComposeCount() }) {
                    Text("Compose Stateのカウント: $composeCount")
                }

                Button(onClick = { vm.generateNewFlowCount() }) {
                    Text("flowCountのカウント: $flowCount")
                }
            }
        }
    }
}

ボタンをクリックしたときにランダムな値を取得し、Stateを更新している。
上記はどちらも同じ挙動をする。

AniokraitAniokrait

savedStateHandleを使う場合の比較

Compose StateもStateFlowもアプリのプロセスが死んだときには値が破棄され初期値に戻ってしまう。
それを防ぐためにはsavedStateHandleに値を退避しておく必要がある。

退避方法を比較

class MainViewModel constructor(
    private val savedStateHandle: SavedStateHandle
): ViewModel() {
    //StateFlowの宣言
//    private val _flowCount = MutableStateFlow(0)
//    val flowCount = _flowCount.asStateFlow()
    //StateFlowの宣言(savedStateHandleを使う場合)
    val flowCount = savedStateHandle.getStateFlow("count", 0)

    //Compose Stateの宣言
    var composeCount by mutableStateOf(
//        0
        //savedStateHandleを使う場合
        savedStateHandle.get<Int>("count") ?: 0
    )
        private set

    fun generateNewComposeCount() {
        val newCount = Random.nextInt(1000)
        //savedStateHandleを使う場合
        savedStateHandle["count"] = newCount
        //savedStateHandleを使う場合もCompose Stateへの代入は必要
        composeCount = newCount
    }

    fun generateNewFlowCount() {
        val newCount = Random.nextInt(1000)
//        _flowCount.value = newCount
        //savedStateHandleを使う場合
        savedStateHandle["count"] = newCount
    }
}

StateFlowはsavedStateHandle.getStateFlow関数を使用した変数の宣言と、savedStateHandle[キー]への代入だけで済む。
一方、Compose StateはStateの更新時にStateに加えsavedStateHandle[キー]への代入もしなければならない。
記述の簡潔さからsavedStateHandleを使う場合はStateFlowの方に軍配が上がる。

AniokraitAniokrait

ちなみにviewModel()(またはhiltViewModel())でViewModelを生成する場合はコンストラクタで宣言するだけでsavedStateHandleを注入することができる。
ちなみついでに上記のコードは両方のパターンでボタンクリック時にsavedStateHandleを更新しているので、Compose Stateのボタンを押したときもStateFlowのボタンのカウントが増える。逆の場合は増えない(savedStateHandleへ値を代入しただけではMutableStateは反応しない)。

AniokraitAniokrait

プロセス破棄による意図しない挙動を防ぐならsavedStateHandle対応は必須。
なのでViewModelで状態を持つのであれば、Compose StateよりもStateFlowを使ったほうがよさそう。