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
}
Why not just set the property directly in the composable body? You could — but doing it inside 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.

Next up: All of the effects so far run from inside a composable's body. What about launching a coroutine from a button's onClick, which isn't a composable context at all? That's rememberCoroutineScope.