← All shorts
23Compose 1.0+2 min read

LaunchedEffect vs DisposableEffect vs SideEffect

Three effect APIs for three jobs: coroutine work, cleanup-requiring subscriptions, and publishing state out of Compose.

Call side-effecting code straight from a composable's body and it runs on every recomposition, with no cleanup. Compose gives you three purpose-built escape hatches instead. Pick the wrong one and you either leak resources or run the work at the wrong frequency.

#The old way

@Composable
fun LocationScreen(locationClient: LocationClient) {
    locationClient.startUpdates()
    Text("Tracking location")
}

startUpdates() runs again on every recomposition, and it never tears itself down. Duplicate subscriptions. Leaked listeners the moment this leaves composition.

#The new way

@Composable
fun LocationScreen(
    userId: String,
    locationClient: LocationClient,
    analytics: Analytics
) {
    var location by remember { mutableStateOf<Location?>(null) }

    LaunchedEffect(userId) {
        location = locationClient.fetchInitialLocation(userId)
    }

    DisposableEffect(locationClient) {
        val listener = LocationListener { location = it }
        locationClient.addListener(listener)
        onDispose { locationClient.removeListener(listener) }
    }

    SideEffect {
        analytics.setUserProperty("last_screen", "location")
    }

    Text(text = location?.toString() ?: "Locating…")
}

#Why it matters

  • LaunchedEffect runs a coroutine scoped to composition. Change its keys and that coroutine is cancelled and restarted.
  • DisposableEffect pairs setup with onDispose, so subscriptions to non-Compose callback APIs — addListener/removeListener pairs like the one above — always get torn down.
  • SideEffect runs after every successful recomposition. Reach for it when you need to publish Compose state to something Compose doesn't own: an analytics SDK, a legacy View.

#Gotcha

LaunchedEffect(Unit) never restarts on recomposition. Not when userId changes, not when anything else does. If the effect body reads a value that should trigger a restart, that value belongs in the key list — being in scope is not enough, and its changes will be silently ignored.

androidcomposestate-management

Related shorts