0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Android】Jetpack Compose の snapshotFlow をちゃんと理解する — State と Flow をつなぐ「観測ブリッジ」

Last updated at Posted at 2025-11-28

はじめに

  • snapshotFlow { ... }Compose の Snapshot 読み取りを監視して Flow に変換する仕組み
  • ブロック内で 読まれた State が更新されると、そのたびにブロックが再実行されて emit される
  • 内部的には Snapshot の依存トラッキング+ApplyObserver を使っている(Recomposer と同じ通知経路)
  • 「Compose の State → コルーチン世界の Flow」へ橋渡しする時にめちゃくちゃ便利
  • ただし 重い処理をブロック内に書きすぎる・大量の State を読む とパフォーマンス悪化するので注意

1. snapshotFlow とは何か?

公式のイメージだと:

State(mutableStateOf 等)を監視して Flow に変換するユーティリティ

ですが、本質的にはこうです:

「Snapshot の read-tracking 機能を利用して、
ブロック内で読まれた State の変更を Flow に流すトリガー装置」

なので、単純な「State を Flow に変換するラッパ」ではなく、
Compose の内部機構(Snapshot)をそのまま Flow 側で再利用している感じに近いです。


2. まずは基本の書き方

よくあるパターン:Compose の State → Flow → 非同期処理

@Composable
fun SearchScreen(viewModel: SearchViewModel) {
    val query by viewModel.query.collectAsState()

    LaunchedEffect(Unit) {
        snapshotFlow { query }
            .debounce(300)
            .distinctUntilChanged()
            .collect { q ->
                viewModel.search(q)
            }
    }

    // TextField など UI は省略
}

ここで起きていること:

  1. snapshotFlow { query }
    → ブロック内で query(= State)を読む
  2. query が更新されるたびに
    → Snapshot が「この State 変わったよ」と snapshotFlow に通知
  3. snapshotFlow がブロックを再実行して 新しい query を emit
  4. Flow 演算子(debounce 等)を通して非同期処理へ

これは 「UI の State 変化を安全に Flow に渡す」 という、Compose では頻出のパターンです。


3. snapshotFlow の内部仕組み

3.1 ざっくり流れ

  1. snapshotFlow { block } が呼ばれる
  2. ブロックを read-only Snapshot 上で実行
  3. 実行中に読まれた State を Snapshot システムが記録
  4. 実行後、その State 群に対して「変化したら教えてね」という ApplyObserver を登録
  5. その後、いずれかの State が変化
    → Snapshot が ApplyObserver を呼ぶ
    snapshotFlow がブロックを再度 Snapshot 上で実行
    emit(newValue) される

つまり、

「ブロック内で読んだ State の集合に対する、差分トリガー付き Flow」

です。


3.2 シーケンス図で見る SnapshotFlow

Mermaid で内部フローを図解してみます。

ポイント:

  • ブロックは「状態が変わったときだけ」再実行される
  • 並列実行ではなく、状態変化ごとに順に実行される(Flow の collect に従う)
  • 依存関係の管理は Snapshot システム側が担当(Compose 内部と同じ)

4. Recomposer と snapshotFlow の関係

4.1 共通点:どちらも Snapshot の通知を見ている

  • Recomposer
    → 「State が変わった → どの Composable がその State を読んでいたか?」をもとに再コンポーズをスケジュール
  • snapshotFlow
    → 「ブロック内で読まれた State」が変わったら、ブロックを再実行して Flow に emit

どちらも内部では:

  • Snapshot の ApplyObserver
  • State の read-tracking

を使っています。

違うのは「通知をどう使うか」。

役割 Recomposer snapshotFlow
目的 UI の再コンポーズ Flow に値を流す
読み取り対象 Composable 内で読まれた State snapshotFlow {} ブロック内で読まれた State
変化時のリアクション SlotTable 更新 → UI 再描画 Flow の emit(value)
動作する場所 Compose ランタイム内部 任意の coroutine / LaunchedEffect などから利用可

4.2 だから何が嬉しいのか

  • UI 側の State と、非 UI なコルーチン処理(API 呼び出し、保存処理など)の間を
    「同じ Snapshot メカニズムで」つなげる
  • State の同期ずれや、collectAsState / LaunchedEffect での発火条件の混乱を減らせる
  • State 変化の Flow 化を、かなり宣言的に書ける

5. よくある実戦パターン

5.1 検索クエリのデバウンス

さっきの例を少しだけ一般化すると:

@Composable
fun SearchScreen(viewModel: SearchViewModel) {
    val uiState by viewModel.uiState.collectAsState()

    LaunchedEffect(Unit) {
        snapshotFlow { uiState.query }
            .debounce(300)
            .distinctUntilChanged()
            .collect { query ->
                viewModel.search(query)
            }
    }
}
  • uiState.query をブロック内で読むだけ
  • query が変わったときだけ emit
  • それを debounce + distinctUntilChanged で API 呼び出しの回数を制御

5.2 リストスクロール位置を反映しつつ、Flow とも連携

@Composable
fun ListScreen(viewModel: ListViewModel, listState: LazyListState) {
    // スクロール位置を State に反映
    LaunchedEffect(listState) {
        snapshotFlow { listState.firstVisibleItemIndex }
            .collect { index ->
                viewModel.onScrollIndexChanged(index)
            }
    }

    // ViewModel 側で index に応じた処理を Flow で行う、など
}

LazyListState も Compose の State を内部に持っているので、
snapshotFlow { listState.firstVisibleItemIndex } で同じように監視できます。


6. 他の API との違い

6.1 collectAsState() とどう違う?

  • collectAsState()
    Flow → State(Compose 側) の橋渡し
  • snapshotFlow
    State(Compose)→ Flow の橋渡し

方向が逆。

UI 外にロジックや処理を逃がしたいときは snapshotFlow
UI に値を反映したいときは collectAsState


6.2 derivedStateOf との違い

derivedStateOf は:

  • 複数の State から「派生 State」を作るための API
  • Snapshot システム上で 「キャッシュ+依存トラッキング」 を行ってくれる
  • Flow とは無関係(あくまで State → State)

一方 snapshotFlow は:

  • State → Flow
  • Flow 演算子(debounce, map, combine, flatMapLatest, …)を活用できる
  • ネットワークや DB など「非同期処理」と一緒に使うのに向いている

組み合わせることもできる:

val derived by remember {
    derivedStateOf {
        "${uiState.firstName} ${uiState.lastName}"
    }
}

LaunchedEffect(Unit) {
    snapshotFlow { derived }
        .collect { fullName ->
            // ログ送信とか
        }
}

6.3 LaunchedEffect(key) とどう使い分ける?

LaunchedEffect(key) は、

  • key の変化ごとに coroutine ブロック全体を再起動する
  • = key が変わると 古い coroutine はキャンセルされ、新しく起動

一方、snapshotFlow { ... } は:

  • ブロック内で読んだ State の変化を契機に emit
  • coroutine 自体は同じスコープで生き続ける

なので、

  • key が変わったら「処理ごとキャンセルして作り直したい」
    LaunchedEffect(key)
  • key の変化を Flow として扱いたい(debounce, map, filter etc)
    snapshotFlow { key } + 1 つの LaunchedEffect(Unit)

というイメージで使い分けるとスッキリします。


7. よくある落とし穴・アンチパターン

7.1 ブロック内で「重い処理」をしない

snapshotFlow {
    // ❌ やりがち:重い処理を直接ここでやる
    importantCalculation(uiState)
}
    .collect { result ->
        // ...
    }

snapshotFlow ブロックは State が変わるたびに再実行される ので、
ここに重い計算を書くと、そのままパフォーマンス問題になります。

👉 対策:

  • ブロック内は「値の読み取り」だけにして、重い処理は collect 側 or Flow 演算子内で行う
snapshotFlow { uiState }
    .map { state -> heavyCalc(state) }
    .collect { result -> /* ... */ }

7.2 監視したくない State まで読み込んでしまう

snapshotFlow {
    // ❌ なんでもかんでも 1 つの snapshotFlow に詰め込む
    uiState  // huge データ
}

ブロック内で 読んだ State は全部監視対象になる ので、
本当に変化をトリガーにしたい値だけ読むように絞ると良いです。

👉 シンプルに「必要なスカラー値だけ」にするのが吉:

snapshotFlow { uiState.query to uiState.filter }

7.3 collect 側を Compose のライフサイクルと紐づけ忘れる

snapshotFlowただの Flow なので、
LaunchedEffect, rememberCoroutineScope, viewModelScope 等との組み合わせをちゃんと設計しないと、

  • 画面から離れても coroutine が動き続ける
  • 逆に、必要なときに収集されていない

といった問題が発生します。

Compose 内で使うときは基本:

LaunchedEffect(Unit) {
    snapshotFlow { ... }
        .collect { ... }
}

で、「画面が存在する間だけ収集」されるようにしておくのが安全です。


まとめ

  • snapshotFlowCompose の Snapshot を Flow に変換するための橋渡し API
  • 内部では State の read-tracking と Snapshot の ApplyObserver を使っている
  • Recomposer が UI を再コンポーズするのと 同じ通知経路 から、Flow 側へ値を流している
  • 検索クエリのデバウンス、スクロール位置監視、フォーム入力の検証などで非常に役立つ
  • ただし「ブロック内に重い処理を書かない」「読む State を絞る」という設計が重要

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?