produceState and snapshotFlow in Jetpack Compose

Quick answer: produceState moves data into Compose by producing a State<T> from a composition-scoped coroutine. snapshotFlow moves observed Compose snapshot reads out as a cold Flow. Use the first for a reusable UI-facing adapter and the second when a Compose value needs Flow operators or a side effect.

The official side-effects documentation makes the direction explicit: produceState converts non-Compose state into Compose state, while snapshotFlow converts Compose state into Flow.

Pick the direction of travel

NeedAPIResult
Load or subscribe to external data for a composableproduceStateState<T> that drives rendering
Observe Compose state for analytics or a controlled effectsnapshotFlow in LaunchedEffectCold Flow<T>
Render StateFlow UI state on AndroidcollectAsStateWithLifecycleLifecycle-aware State<T>

Adapt a suspend data source with produceState

import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState

sealed interface ImageResult {
    data object Loading : ImageResult
    data class Success(val image: Image) : ImageResult
    data object Error : ImageResult
}

@Composable
fun loadImage(url: String, repository: ImageRepository): State<ImageResult> =
    produceState<ImageResult>(
        initialValue = ImageResult.Loading,
        key1 = url,
        key2 = repository,
    ) {
        value = repository.load(url)?.let(ImageResult::Success) ?: ImageResult.Error
    }

The producer starts when the composable enters composition, is cancelled when it leaves, and restarts if a key changes. The returned state conflates equal values, so assigning the same value does not recompose readers. Use this for UI-scoped adapters; repository and business state generally still belong in a ViewModel.

For callback-based sources, register inside produceState and call awaitDispose { unsubscribe() } so the subscription is removed when the producer is cancelled.

Observe a snapshot read with snapshotFlow

import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import androidx.compose.foundation.lazy.LazyListState
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter

@Composable
fun TrackScrollPastFirstItem(listState: LazyListState, onPastFirst: () -> Unit) {
    LaunchedEffect(listState) {
        snapshotFlow { listState.firstVisibleItemIndex > 0 }
            .distinctUntilChanged()
            .filter { it }
            .collect { onPastFirst() }
    }
}

snapshotFlow reads Compose snapshot state when collected and emits a changed result when its observed reads mutate. The official example uses this pattern for scroll analytics. It is not a replacement for ordinary UI rendering; read state directly in UI and use Flow only when stream operators or an effect are required.

Avoid the common inversions

  • Do not use snapshotFlow to render a Text value; direct state reads are simpler.
  • Do not use produceState just to collect a StateFlow on Android; prefer collectAsStateWithLifecycle.
  • Include inputs that should restart a producer as keys, as with LaunchedEffect.
  • Keep snapshotFlow collection in a controlled effect and ensure analytics or other work is idempotent.

For recomposition-threshold UI values, use derivedStateOf instead. For effect lifecycle and key selection, see LaunchedEffect.

FAQ

Does snapshotFlow emit duplicates?

It emits when the result differs from the previous emission, similarly to distinctUntilChanged. Additional Flow operators are still useful when mapping changes the value you care about.

Can produceState handle a listener API?

Yes. Register the listener in its producer and use awaitDispose to unregister it when the composition cancels the producer.