How to Use Keyed SideEffect in Jetpack Compose 1.12
Upgrade to Compose BOM 2026.08.00 and fire one-shot SideEffect keys instead of LaunchedEffect when you only need to publish state.
Jetpack Compose 1.12 ships a keyed overload of SideEffect. Use it when you need to publish Compose state to an analytics SDK, a logger, or another object that lives outside composition, and you do not need a coroutine or a dispose block.
The August 2026 Compose release notes state that keyed SideEffect can run up to 90 percent faster than LaunchedEffect and about 20 percent faster than DisposableEffect for that narrow job. This guide shows how to add the BOM, write the new call, and avoid the timing trap the same post warns about.
What changed in Compose 1.12
Before 1.12, SideEffect { } ran after every successful recomposition. That is still true for the no-key overload. The new overloads take key1, key1 plus key2, key1 plus key2 plus key3, or a vararg of keys.
The API reference is explicit: when a keyed SideEffect recomposes, the effect runs only if the key differs from the previous value. The unkeyed form still runs on every successful composition.
That is the contract you want for screen analytics. Log once when userId or screenName changes. Do not log again because an animation tick recomposed the same screen.
Upgrade the project first
Compose 1.12 lives in Compose BOM 2026.08.00. The official August 2026 blog post from Nick Butcher lists two breaking requirements:
compileSdkmoves to API 37.- You need a minimum Android Gradle Plugin of 9.1.1.
In the app module:
implementation(platform("androidx.compose:compose-bom:2026.08.00"))
implementation("androidx.compose.runtime:runtime")
implementation("androidx.compose.ui:ui")
Confirm the mapping on the Compose BOM mapping page before you ship. While you are in the same file, replace deprecated Modifier.onFirstVisible() with Modifier.onVisibilityChanged().
If you already added mesh gradients from Compose 1.12, keep that painter work on a separate branch of the pull request. The runtime change does not depend on MeshGradientPainter.
Write a keyed analytics tracker
The sample in the August 2026 release post is the right shape:
@Composable
fun AnalyticsTracker(userId: String, screenName: String) {
SideEffect(key1 = userId, key2 = screenName) {
analytics.logScreenView(userId, screenName)
}
}
Call it from the screen composable after you have a stable userId. Pass primitive keys or data-class copies that implement equals. Do not pass a new object instance on every composition or the effect will fire every frame.
The older pattern from the side-effects guide still works when the property must refresh on every successful composition:
SideEffect {
analytics.setUserProperty("userType", user.userType)
}
Use that unkeyed form only when the external object must match the latest composition every time. Prefer keys when the work is a one-shot publish.
Know when not to migrate
Google’s release notes add a timing warning. SideEffect runs its block before DisposableEffect and LaunchedEffect. LaunchedEffect work that must start after the current frame completes will change behavior if you swap APIs blindly.
Keep LaunchedEffect when you need:
- A coroutine (
delay, a network call, collecting a flow) - Automatic cancellation when the composable leaves the tree or the key changes
Keep DisposableEffect when you need:
- A listener registration with an
onDisposeteardown - A sensor or broadcast receiver that must unregister
Keyed SideEffect is for publishing already-known values. It is not a job scheduler.
Pair it with deferred two-stage motion
The same 1.12 drop graduates DeferredTargetAnimation and adds DeferredAnimatedContent plus DeferredAnimatedVisibility. Those composables cover a different problem: a gesture owns the first part of a transition, then the engine takes over with velocity transfer.
A predictive-back sketch from the official post looks like this:
val state = remember { DeferredTransitionState(initialScreen) }
val transition = rememberDeferredTransition(state)
if (predictiveBackInProgress) {
state.defer(targetScreen)
} else {
state.animateTo(targetScreen)
}
transition.DeferredAnimatedContent(
targetState = targetScreen,
mutableTransformSpec = {
MutableContentTransform {
initialContentTransform { scale = swipeProgress }
}
}
) { screen ->
ScreenContent(screen)
}
Do not log analytics from inside the mutableTransformSpec. Drive the animation from the gesture, and keep SideEffect at the screen boundary with keys for screen identity.
Test the effect without flaky clocks
Compose 1.12 also adds test helpers aimed at animation loops: hasPendingWork and runWithoutImplicitWait. Use them when you step the clock yourself.
@Test
fun logsOnceWhenScreenChanges() {
composeTestRule.setContent {
AnalyticsTracker(userId = "u1", screenName = "Home")
}
composeTestRule.waitForIdle()
// Assert the analytics fake received one Home event.
}
Do not advance the clock to “wait out” a keyed SideEffect. The block runs when composition applies, not on a dispatcher delay. If a test only fails when you tap a second screen, the key is working.
runWithoutImplicitWait is most useful when you query several nodes in one frozen frame. The release post notes that it avoids redundant sync on every query.
Practical checklist
- Bump the Compose BOM to
2026.08.00and setcompileSdk37 with AGP 9.1.1 or newer. - Find
LaunchedEffect(userId, screenName)blocks that only call a logger. - Replace those with
SideEffect(key1 = userId, key2 = screenName). - Leave network and listener code on
LaunchedEffectorDisposableEffect. - Confirm keys are stable values, not new list instances.
- Run the screen-change test once with the analytics fake.
If a screen logs twice on first open, a parent is passing a changing key. Print the keys in debug and look for a timestamp or a random UUID created in the composable body.
Limits you should expect
- Keys compare with
equals. Data classes work. Lambdas do not. - The effect still runs only after a successful composition. Discarded optimistic recompositions do not publish.
- There is no
onDisposecounterpart onSideEffect. You cannot unregister here. - Wide Color Gamut paint, mesh gradients, and Grid named areas are separate 1.12 features. They do not change when
SideEffectfires.
Startup time in this release is a separate claim. Google says Time to Initial Display in its hero benchmark is now comparable to Views. That is a framework result, not something keyed SideEffect invents in your app.
Conclusion
Treat keyed SideEffect as a cheap publish hook. Point it at analytics, feature flags, or a third-party SDK that must see the latest user and screen. Keep coroutines and teardown on the older effect APIs.
Upgrade the BOM, swap the one-shot loggers, and read the timing note once so you do not move a LaunchedEffect that waited for the next frame. The rest of Compose 1.12 — mesh paint, P3 color, Credential Manager semantics — can land in later diffs.
Sources
- What’s new in the Jetpack Compose August ‘26 release — Android Developers Blog
- SideEffect API reference
- Side-effects in Compose
- Compose BOM mapping
- Set up Compose dependencies and compiler
- What’s new in Jetpack Compose (YouTube) — Android Developers
Share this story