Kotlin Flow を使って、UI State を宣言的に構築する

Jetpack Compose の ViewModel では、UI State を手動で管理し、init から load() を呼び出し、データが変わるたびに複数のプロパティを更新するコードをよく見かけます。
この方法でも問題なく動作します。しかし、アプリが複雑になるにつれて、複数の状態を同期させるためのコードが増え、管理が難しくなっていきます。
Kotlin Flow を使えば、もっと宣言的に UI State を構築できます。
Flow のオペレーターと stateIn を組み合わせることで、UI State がどの入力から、どのように導出されるのかをコードで表現できます。更新のたびに状態を手動で同期する必要はありません。
🤔 手動で UI State を管理するコスト
まずは、よくある ViewModel を見てみましょう。
class MyViewModel(
private val repository: MyRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()
init {
load()
}
private fun load() {
viewModelScope.launch {
val items = repository.loadItems()
_uiState.update {
it.copy(items = items)
}
}
}
}
これは正当な実装です。一度だけデータを取得するような処理なら、理解しやすく、十分に実用的です。
ただし、ViewModel は次のような責務を担っています。
・ 初回ロードを開始する。
・ データ取得後に UI State を更新する。
・ 変更していないフィールドを保持する。
・ 入力が増えたときも、複数の状態の整合性を維持する。
画面が複雑になるほど、こうした処理が繰り返し必要になります。
では、Repository がすでに Flow でデータを公開している場合はどうでしょうか。Repository の Flow を手動で収集し、その値を別の StateFlow にコピーし続ける必要があるのでしょうか。
多くの場合、その必要はありません。
🤔 そもそも load() を呼ぶ必要はあるのか?
Jetpack Compose では、次のようなコードをよく使います。
LaunchedEffect(Unit) {
viewModel.load()
}
あるいは、ViewModel の init ブロックから呼び出します。
init {
load()
}
どちらも適切な場面があります。ユーザーが明示的に実行する更新処理や、一度だけ行う操作、特定のイベントに応じた処理には、明示的なコマンドが必要です。
しかし、画面が Flow で公開されたデータをそのまま表示するだけなら、手動でロードを開始し、UI State に反映させるための処理は不要かもしれません。
ここで重要なのは、コマンドと状態を区別することです。
・ コマンドは「何を実行するか」を表す。
・ 状態は「現在どうなっているか」を表す。
既存の Flow から UI State を導出できるなら、stateIn を使うことで、命令的な更新処理を減らせます。
🤔 stateIn で Flow を UI State に変換する
Repository が次のような Flow を公開しているとします。
interface MyRepository {
val items: Flow<List<Item>>
}
この場合、ViewModel は UI State を直接導出できます。
class MyViewModel(
repository: MyRepository
) : ViewModel() {
val uiState: StateFlow<UiState> =
repository.items
.map { items ->
UiState(items = items)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = UiState()
)
}
何がなくなったでしょうか。
・ 同じ UI State を手動更新するための MutableStateFlow
・ 初回収集を開始するためだけの init ブロック
・ Repository の値をコピーするためだけの load() 呼び出し
・ Repository のデータと UI State を同期させる処理
コードは、データと UI State の関係を直接表現しています。
Repository Flow
|
v
map
|
v
UiState
|
v
stateIn
|
v
StateFlow<UiState>
|
v
Compose UI
Repository が新しいリストを発行すると、それに対応する UiState が生成されます。
stateIn は、上流の Flow をホットな StateFlow に変換し、最新の値を共有するとともに、初期値を提供します。
公式ドキュメントでも、この用途が説明されています。
👉 stateIn – Kotlin Programming Language
👉 StateFlow – Kotlin Programming Language
変更のたびに状態を同期する方法ではなく、状態がどのように導出されるのかをコードで宣言できるのです。
🤔 複数の入力から、一貫した UI State を導出する
このメリットは、画面が複数の入力に依存するときに、さらに明確になります。
例えば、検索機能のあるリスト画面を考えてみましょう。表示するアイテムは、Repository のデータと検索クエリの両方に依存します。
手動で状態を更新する場合、両方の変更を考慮して UI State を更新しなければなりません。
Flow なら、その依存関係を明示できます。
class MyViewModel(
repository: MyRepository
) : ViewModel() {
private val query = MutableStateFlow("")
val uiState: StateFlow<UiState> =
combine(
repository.items,
query
) { items, query ->
UiState(
items = items.filter {
it.matches(query)
},
query = query
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = UiState()
)
fun onQueryChanged(query: String) {
this.query.value = query
}
}
ここで注目したいのは、UI アクションが更新するのは query だけだという点です。公開されている uiState を直接変更することはありません。
次のどちらが起きても、UI State は自動的に再計算されます。
・ Repository が新しいアイテムを発行する。
・ ユーザーが検索クエリを変更する。
表示リストを再計算する処理を、変更箇所ごとに書く必要はありません。
依存関係は combine の中で一度だけ定義されています。
Repository.items ──┐
├── combine ──> UiState
query ─────────────┘
|
v
stateIn
|
v
StateFlow<UiState>
この設計では、入力を変更する処理と、その入力から UI State を導出する処理が明確に分離されています。
🤔 コマンドは明示的に、状態は宣言的に
もちろん、すべての操作を Flow で表現すべきという意味ではありません。
フォームの送信、アイテムの削除、ユーザー操作に応じた明示的な再取得などは、ViewModel がコマンドとして処理する必要があるでしょう。
重要なのは、導出できる状態を同期するためだけに、命令的な処理を増やさないことです。
複数の観測可能な入力から UI State が決まるなら、map や combine で依存関係を表現し、UI に StateFlow を公開する必要がある場合は stateIn を使います。
設計上の役割を整理すると、次のようになります。

依存関係がコードに表れるため、状態がどこで、なぜ変わるのかを理解しやすくなります。
🤔 適切な Sharing Strategy を選ぶ
stateIn では、上流の Flow をいつ開始・停止するかを指定します。
よく使われる設定の一つが、次のものです。
SharingStarted.WhileSubscribed(5_000)
これは、最初の購読者が現れたときに上流の収集を開始し、最後の購読者がいなくなってから 5 秒後に収集を停止する設定です。
👉 stateIn – Kotlin Programming Language
5 秒の猶予があることで、画面遷移などによる短時間の購読解除で、上流の収集がすぐに停止・再開するのを避けられます。
ただし、次の2つは同じではありません。
・ Flow の収集が再開されること。
・ ネットワークリクエストが再実行されること。
画面が再表示されたときに API が再度呼び出されるかどうかは、上流の Flow がどのように実装されているかによります。
また、stateIn 自体がデータを取得するわけではありません。上流の Flow が発行した値を共有し、最新の状態として保持する仕組みです。
よく見かける設定をそのまま採用するのではなく、アプリのライフサイクルやデータ取得の要件に合わせて選びましょう。
🤔 MutableStateFlow が必要な場面もある
MutableStateFlow をなくすこと自体が目的ではありません。命令的に変更する必要がある入力には、引き続き有用です。
例えば、ユーザーが操作する検索クエリです。
private val query = MutableStateFlow("")
ユーザーの入力に応じて query を変更するのは自然な設計です。
一方、Repository のデータと検索クエリから導出できる uiState は、別の可変状態として保持する必要がありません。
実践的なルールは次のとおりです。
・ 入力には MutableStateFlow を使う。
・ Flow オペレーターで入力から状態を導出する。
・ stateIn で結果を StateFlow として公開する。
これは可変状態を排除する設計ではありません。可変性が必要な場所にだけ、それを配置する設計です。
🤔 まとめ:stateIn に状態管理を任せよう
stateIn は、Flow を StateFlow に変換するだけの便利なオペレーターではありません。map や combine と組み合わせることで、ViewModel の状態管理を宣言的に設計するための重要な手段になります。
データを手動で収集し、値をコピーし、関連するフィールドを同期する代わりに、入力と UI State の依存関係を直接表現できます。
特に、複数のデータソースから画面の状態が決まる場合に、そのメリットが大きくなります。
コマンドは明示的に。状態は宣言的に。
Flow で依存関係を定義し、適切な場面で stateIn を使って UI State を公開しましょう。
Related Categories : Android・Developmemt・JetpackCompose・KMP・Kotlin・Kotlin Multiplatform Mobile・Recommended・Trending