produceState and snapshotFlow in Jetpack Compose

Quick answer:
produceStatemoves data into Compose by producing aState<T>from a composition-scoped coroutine.snapshotFlowmoves observed Compose snapshot reads out as a coldFlow. 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
| Need | API | Result |
|---|---|---|
| Load or subscribe to external data for a composable | produceState | State<T> that drives rendering |
| Observe Compose state for analytics or a controlled effect | snapshotFlow in LaunchedEffect | Cold Flow<T> |
Render StateFlow UI state on Android | collectAsStateWithLifecycle | Lifecycle-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
snapshotFlowto render aTextvalue; direct state reads are simpler. - Do not use
produceStatejust to collect aStateFlowon Android; prefer collectAsStateWithLifecycle. - Include inputs that should restart a producer as keys, as with
LaunchedEffect. - Keep
snapshotFlowcollection 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.