Jetpack ComposeのViewModelでCompose Stateを使うべきか、StateFlowを使うべきか
Jetpack Composeで使える状態を扱うものたち
Jetpack Composeは専用に用意されたCompose State以外にも、StateFlowやLiveData、RxJavaが使える。
RxJavaは時代の流れ的にもう採用されることはないと思うし、LiveDataもStateFlowの方が上位互換として使えるので実質選択肢はCompose StateとStateFlowの2択。
RxJava: 学習コストが高い。直感的にコードが読みづらい。記述量が多い。
LiveData: バックグラウンドスレッドで実行されているかメインスレッドで実行されているか意識する必要がある。NullableなのでNullを意識する必要がある。
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を更新している。
上記はどちらも同じ挙動をする。
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の方に軍配が上がる。
ちなみにviewModel()(またはhiltViewModel())でViewModelを生成する場合はコンストラクタで宣言するだけでsavedStateHandleを注入することができる。
ちなみついでに上記のコードは両方のパターンでボタンクリック時にsavedStateHandleを更新しているので、Compose Stateのボタンを押したときもStateFlowのボタンのカウントが増える。逆の場合は増えない(savedStateHandleへ値を代入しただけではMutableStateは反応しない)。
プロセス破棄による意図しない挙動を防ぐならsavedStateHandle対応は必須。
なのでViewModelで状態を持つのであれば、Compose StateよりもStateFlowを使ったほうがよさそう。
githubに調査時のコード追加