StateFlow

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経験者なら、この対応で覚えると分かりやすいです。

SwiftUIJetpack Compose
@StatemutableStateOf()
@ObservableViewModel + StateFlow
@PublishedMutableStateFlow
@EnvironmentObjectActivityに紐付いた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との対応で考えると、次のようにイメージできます。

SwiftUIJetpack Compose
struct(値型)data class
新しいstructを作って状態更新copy()で新しいdata classを作る
@State@Observableが値の変更を通知MutableStateFlowが変更を通知
Viewが状態を監視して再描画collectAsState()StateFlowを監視して再描画

SwiftUIもstructを新しく作る考え方が基本なので、Composeのdata class + copy()は、SwiftUIの「新しい値を作って状態を更新する」という考え方と非常によく似ています。

コメント

タイトルとURLをコピーしました