SideEffect & produceState
LaunchedEffect and DisposableEffect cover most side-effect needs, but two
more specialized situations come up often enough to have their own dedicated APIs. The first: you
need to hand a piece of Compose state to some non-Compose object every single time a
recomposition successfully commits — not once, not on cancellation, every time. The second: you have
some non-Compose, callback-based data source (an old-style API that calls a listener whenever new
data is ready) and you want its latest value available as ordinary Compose State.
SideEffect handles the first case; produceState handles the second.
SideEffect: Publish State on Every Successful Recomposition
SideEffect { } runs its block after every recomposition of the composable it's
in — but only once that recomposition has actually been applied. It doesn't take a key and it isn't
for suspend work; it exists specifically for "push this current value out to something Compose
doesn't manage." A classic case: keeping a third-party analytics SDK's "current screen" property in
sync with whichever RecipeBox screen is actually showing.
@Composable
fun RecipeDetailScreen(recipe: Recipe, analytics: AnalyticsTracker) {
// Runs after every successful recomposition of this composable —
// keeps the analytics SDK's mutable property in sync with Compose state
SideEffect {
analytics.currentScreen = "recipe_detail/${recipe.id}"
}
// ...render the recipe's title, ingredients, steps
}
SideEffect documents the intent clearly (this line is a deliberate escape hatch to the outside world) and guarantees it only runs after Compose has actually committed the recomposition, not during speculative recomposition passes that Compose might throw away.produceState: Turn a Callback API Into Compose State
Going the other direction, produceState lets you wrap an asynchronous, non-Compose data
source and expose its latest value as a normal State<T> that any composable can
read with .value, triggering recomposition automatically whenever it updates. Inside its
builder lambda, you get a mutable value property to assign to, and the block itself
behaves like a coroutine (so you can register callbacks and use awaitDispose to clean up
when the producing composable leaves composition).
Worked Example: Wrapping a Callback-Based Location API
Suppose RecipeBox's photo-tagging feature is backed by an older, callback-style location API rather
than a suspend function or a Flow. produceState is exactly the tool for adapting it into
something a composable can just read as state:
@Composable
fun rememberCurrentLocation(locationProvider: RecipeLocationProvider): State<Location?> {
return produceState<Location?>(initialValue = null, locationProvider) {
val listener = RecipeLocationProvider.Listener { newLocation ->
// Assigning to value updates the State, which recomposes any reader
value = newLocation
}
locationProvider.registerListener(listener)
// Runs when this leaves composition, or locationProvider changes —
// same cleanup guarantee as DisposableEffect's onDispose
awaitDispose {
locationProvider.unregisterListener(listener)
}
}
}
@Composable
fun AddEditRecipeScreen(locationProvider: RecipeLocationProvider) {
val currentLocation by rememberCurrentLocation(locationProvider)
currentLocation?.let { location ->
Text(text = "Tagged near ${location.name}")
}
}
Notice the shape: produceState takes care of registering the callback, exposing a
State, and unregistering on cleanup — all in one place — so the rest of the app never has
to know RecipeLocationProvider is callback-based at all. It just reads
currentLocation like any other piece of Compose state.
onClick, which isn't a composable context at all? That's rememberCoroutineScope.