🐻‍❄️

Jetpack Compose の snapshotFlow の動作を調べる

2022/02/26に公開

snapshotFlow とは

snapshotFlow を利用すると State<T> オブジェクトをコールド Flow に変換できる。

/**
 * Create a [Flow] from observable [Snapshot] state. (e.g. state holders returned by
 * [mutableStateOf][androidx.compose.runtime.mutableStateOf].)
 *
 * [snapshotFlow] creates a [Flow] that runs [block] when collected and emits the result,
 * recording any snapshot state that was accessed. While collection continues, if a new [Snapshot]
 * is applied that changes state accessed by [block], the flow will run [block] again,
 * re-recording the snapshot state that was accessed.
 * If the result of [block] is not [equal to][Any.equals] the previous result, the flow will emit
 * that new result. (This behavior is similar to that of
 * [Flow.distinctUntilChanged][kotlinx.coroutines.flow.distinctUntilChanged].) Collection will
 * continue indefinitely unless it is explicitly cancelled or limited by the use of other [Flow]
 * operators.
 *
 * @sample androidx.compose.runtime.samples.snapshotFlowSample
 *
 * [block] is run in a **read-only** [Snapshot] and may not modify snapshot data. If [block]
 * attempts to modify snapshot data, flow collection will fail with [IllegalStateException].
 *
 * [block] may run more than once for equal sets of inputs or only once after many rapid
 * snapshot changes; it should be idempotent and free of side effects.
 *
 * When working with [Snapshot] state it is useful to keep the distinction between **events** and
 * **state** in mind. [snapshotFlow] models snapshot changes as events, but events **cannot** be
 * effectively modeled as observable state. Observable state is a lossy compression of the events
 * that produced that state.
 *
 * An observable **event** happens at a point in time and is discarded. All registered observers
 * at the time the event occurred are notified. All individual events in a stream are assumed
 * to be relevant and may build on one another; repeated equal events have meaning and therefore
 * a registered observer must observe all events without skipping.
 *
 * Observable **state** raises change events when the state changes from one value to a new,
 * unequal value. State change events are **conflated;** only the most recent state matters.
 * Observers of state changes must therefore be **idempotent;** given the same state value the
 * observer should produce the same result. It is valid for a state observer to both skip
 * intermediate states as well as run multiple times for the same state and the result should
 * be the same.
 */
fun <T> snapshotFlow(
    block: () -> T
): Flow<T> {}

特徴

  • snapshotFlow の block に State を渡して値を取得するようにしておくと、State が変化したときに block が実行される。
  • block で取得した値は snapshoFlow から生成される Flow を通して 収集できるようになっている。

サンプル

以下のように LazyColumn の State に格納される firstVisibleItemIndex を収集する Flow を作成してみる。firstVisibleItemIndex は LazyColumn に一番上に表示されている項目の Index 値になります。

@Composable
fun SnapshotFlowSample() {
    // LazyColumn の State を rememberLazyListState で保持しておく
    val listState = rememberLazyListState() 

    // LazyColumn の State を Flow に変換する
    LaunchedEffect(listState) {
        snapshotFlow {
            Log.v("TEST", "Enter")
            listState.firstVisibleItemIndex
        }.collect {
            Log.v("TEST", "Collect FirstVisibleItemIndex $it")
        }
    }

    // listState オブジェクトを利用するように state に渡す
    LazyColumn(
        state = listState, 
        verticalArrangement = Arrangement.spacedBy(10.dp),
        modifier = Modifier.padding(8.dp)
    ) {
        repeat(10) {
            item {
                Card(
                    modifier = Modifier
                        .fillMaxSize()
                        .height(100.dp)
                ) {
                    Text(text = it.toString())
                }
            }
        }
    }
}

動作結果

  • LazyColumn をスクロールすると firstVisibleItemIndex が変化する
  • firstVisibleItemIndex が変化すると snapshotFlow の block 処理が動作する
  • block 処理が動作したあと Flow を通して firstVisibleItemIndex が通知される

demo.gif

2022-02-26 14:28:29.162 24001-24001/jp.kaleidot725.sample V/TEST: Enter
2022-02-26 14:28:29.162 24001-24001/jp.kaleidot725.sample V/TEST: Collect FirstVisibleItemIndex 0
2022-02-26 14:28:30.633 24001-24001/jp.kaleidot725.sample V/TEST: Enter
2022-02-26 14:28:30.633 24001-24001/jp.kaleidot725.sample V/TEST: Collect FirstVisibleItemIndex 1
2022-02-26 14:28:30.949 24001-24001/jp.kaleidot725.sample V/TEST: Enter
2022-02-26 14:28:30.949 24001-24001/jp.kaleidot725.sample V/TEST: Collect FirstVisibleItemIndex 2
2022-02-26 14:28:31.282 24001-24001/jp.kaleidot725.sample V/TEST: Enter
2022-02-26 14:28:31.282 24001-24001/jp.kaleidot725.sample V/TEST: Collect FirstVisibleItemIndex 3
2022-02-26 14:28:32.599 24001-24001/jp.kaleidot725.sample V/TEST: Enter
2022-02-26 14:28:32.599 24001-24001/jp.kaleidot725.sample V/TEST: Collect FirstVisibleItemIndex 2
2022-02-26 14:28:32.849 24001-24001/jp.kaleidot725.sample V/TEST: Enter
2022-02-26 14:28:32.849 24001-24001/jp.kaleidot725.sample V/TEST: Collect FirstVisibleItemIndex 1
2022-02-26 14:28:33.015 24001-24001/jp.kaleidot725.sample V/TEST: Enter
2022-02-26 14:28:33.015 24001-24001/jp.kaleidot725.sample V/TEST: Collect FirstVisibleItemIndex 0

参考文献

Discussion