SwiftUIの @Observable や @Published を知っているなら、それと比較するとかなり理解しやすいです。
StateFlowとは?
一言でいうと
「変更を監視できるデータ」
です。
例えば
var count = 0
これは普通の変数なので、
0 → 1 → 2
と変わってもComposeは知りません。
そこでStateFlowを使います。
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count
これなら
_count.value++
すると
「値が変わりました!」
という通知が飛びます。
Composeとの関係
Composeは
val count by viewModel.count.collectAsState()
と書くことで
StateFlow
↓
collect
↓
State
↓
Compose再描画
という流れになります。
つまり
StateFlowが変わる
↓
Composeが自動更新
です。
実際のコード
ViewModel
class ChatViewModel : ViewModel() {
private val _message = MutableStateFlow("こんにちは")
val message: StateFlow<String> = _message
fun changeMessage() {
_message.value = "こんばんは"
}
}
Compose
@Composable
fun ChatScreen(
viewModel: ChatViewModel = viewModel()
) {
val message by viewModel.message.collectAsState()
Column {
Text(message)
Button(
onClick = {
viewModel.changeMessage()
}
) {
Text("変更")
}
}
}
すると
こんにちは
↓
ボタン
↓
こんばんは
に勝手に変わります。
なぜMutableStateFlowとStateFlowを分けるの?
これは非常によく使われる書き方です。
private val _message = MutableStateFlow("")
これは
ViewModelだけが変更できる
ようにしています。
外には
val message: StateFlow<String> = _message
だけ公開します。
するとCompose側では
viewModel.message.value = "勝手に変更"
はできません。
つまり
UI
↓
読むだけ
ViewModel
↓
変更できる
という役割分担になります。
これはMVVMではほぼ定番です。
MutableStateとの違い
Composeには
var name by mutableStateOf("")
もあります。
これも再描画されます。
違いは用途です。
mutableStateOf
Compose専用です。
Compose
↑
mutableStateOf
UIでだけ使う状態に向いています。
StateFlow
Compose以外からも利用できます。
Repository
↓
ViewModel
↓
Compose
Flowなので
- Coroutine
- Repository
- Room
- DataStore
などとも相性が良いです。
だからGoogleも
ViewModelではStateFlowを使うことを推奨
しています。
イメージすると
普通の変数は
箱
です。
StateFlowは
📦📢
箱だけど
「中身が変わったよ!」
と周りへ知らせる箱
です。
Composeはその放送を聞いていて
「あ、変わった」
↓
再描画
しています。
SwiftUIとの比較
SwiftUI経験者なら、この対応で覚えると分かりやすいです。
| SwiftUI | Jetpack Compose |
|---|---|
@State | mutableStateOf() |
@Observable | ViewModel + StateFlow |
@Published | MutableStateFlow |
@EnvironmentObject | Activityに紐付いたViewModel |
| Viewの自動更新 | collectAsState() |
特に MutableStateFlow は @Published にかなり近い役割だと考えるとイメージしやすいです。
これからComposeを学ぶなら、覚えるべき黄金パターン
class ChatViewModel : ViewModel() {
private val _uiState = MutableStateFlow(ChatUiState())
val uiState: StateFlow<ChatUiState> = _uiState
fun onClick() {
_uiState.update {
it.copy(
count = it.count + 1
)
}
}
}
そしてCompose側では
val uiState by viewModel.uiState.collectAsState()
Text(uiState.message)
という形が、現在のJetpack Composeで最もよく使われる設計です。
「なぜ copy() を使うのか」「update {} がなぜ推奨されるのか」「UiStateクラスにまとめる理由」
この3つは、Jetpack Composeらしい設計を理解する上で最も重要なポイントです。
実はこの3つはバラバラではなく、セットで使うことを前提に考えられています。
まずUiStateとは?
例えばチャット画面なら、
- メッセージ一覧
- 入力中の文字
- ローディング中か
- エラーメッセージ
など、画面には複数の状態があります。
最初はこう書きたくなります。
class ChatViewModel : ViewModel() {
private val _messages = MutableStateFlow(listOf<String>())
val messages: StateFlow<List<String>> = _messages
private val _text = MutableStateFlow("")
val text: StateFlow<String> = _text
private val _loading = MutableStateFlow(false)
val loading: StateFlow<Boolean> = _loading
}
これでも動きます。
しかし状態が20個になると…
_messages
_text
_loading
_error
_isLogin
_isDarkMode
...
となって管理が大変になります。
UiStateにまとめる
そこで1つのデータクラスにします。
data class ChatUiState(
val messages: List<String> = emptyList(),
val text: String = "",
val loading: Boolean = false
)
ViewModelは
private val _uiState = MutableStateFlow(ChatUiState())
val uiState: StateFlow<ChatUiState> = _uiState
だけになります。
Compose側も
val uiState by viewModel.uiState.collectAsState()
Text(uiState.text)
と非常に分かりやすくなります。
なぜdata classなの?
ここでcopy()が登場します。
例えば
data class ChatUiState(
val count: Int = 0,
val name: String = "なまえ"
)
この状態で
count = 0
name = なまえ
だったとします。
countだけ変えたい。
普通のクラスなら
count = 1
ですが、
UiStateは**基本的に変更不可(immutable)**として扱います。
つまり
val count: Int
なので書き換えられません。
そこで
_uiState.value = _uiState.value.copy(
count = 1
)
を書きます。
copy()は
countだけ変更
他は全部そのまま
という新しいオブジェクトを作ってくれます。
つまり
旧オブジェクト
count=0
name=なまえ
↓
新オブジェクト
count=1
name=なまえ
になります。
なぜ変更不可にするの?
これはバグを減らすためです。
例えば
class User(
var name: String,
var age: Int
)
だと、
どこからでも
user.age = 99
できます。
すると
「誰が変更したの?」
が分からなくなります。
一方
data class User(
val name: String,
val age: Int
)
なら、
変更したいときは
user.copy(age = 20)
しかありません。
つまり
変更が必ず「新しい状態を作る」という形になるため、状態の流れを追いやすくなります。
update {}とは?
ここが一番重要です。
まず普通に書くと
_uiState.value =
_uiState.value.copy(
count = _uiState.value.count + 1
)
これでも動きます。
でも実は
① valueを読む
② countを計算
③ valueを書き込む
という3段階になっています。
もし別のCoroutineが
同じタイミング
で更新すると…
Aが読む
↓
Bが更新
↓
Aが古い値を書き戻す
ということが起こる可能性があります。
updateは安全
そこで
_uiState.update {
it.copy(
count = it.count + 1
)
}
を使います。
itは
その瞬間の最新状態
です。
さらにupdateは**内部でアトミック(原子的)**に更新するので、複数のCoroutineから同時に更新されても、更新の取りこぼしが起きにくくなります。
つまり
読む
↓
変更
↓
書く
を安全に一体化してくれます。
なのでGoogleも
update { }
を推奨しています。
updateのイメージ
例えばカウンターが
5
だったとします。
Aさん
+1
Bさん
+1
普通なら
A読む 5
B読む 5
A書く 6
B書く 6
本当は7になるべきなのに6になります。
しかし
update {
it.copy(count = it.count + 1)
}
なら
5
↓
6
↓
7
となり、更新が失われにくくなります。
この3つはセットで使う
現在のComposeでは、次のような形が定番です。
data class ChatUiState(
val messages: List<String> = emptyList(),
val text: String = "",
val loading: Boolean = false
)
class ChatViewModel : ViewModel() {
private val _uiState = MutableStateFlow(ChatUiState())
val uiState: StateFlow<ChatUiState> = _uiState
fun updateText(newText: String) {
_uiState.update {
it.copy(text = newText)
}
}
fun setLoading(loading: Boolean) {
_uiState.update {
it.copy(loading = loading)
}
}
}
Compose側では
@Composable
fun ChatScreen(viewModel: ChatViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsState()
Text(uiState.text)
if (uiState.loading) {
CircularProgressIndicator()
}
}
となります。
SwiftUIとの比較
SwiftUIとの対応で考えると、次のようにイメージできます。
| SwiftUI | Jetpack Compose |
|---|---|
struct(値型) | data class |
新しいstructを作って状態更新 | copy()で新しいdata classを作る |
@Stateや@Observableが値の変更を通知 | MutableStateFlowが変更を通知 |
| Viewが状態を監視して再描画 | collectAsState()がStateFlowを監視して再描画 |
SwiftUIもstructを新しく作る考え方が基本なので、Composeのdata class + copy()は、SwiftUIの「新しい値を作って状態を更新する」という考え方と非常によく似ています。
コメント