August 12 is a busy day for Jetpack. Compose 1.12.0 hit stable, and it brought company: Fragment 1.9.0, Tracing 2.0.0, Navigation3 1.1.6, Paging 3.5.1, and Appcompat 1.8.0 all graduated on the same day. If you've been waiting for the right moment to bump your BOM and call it a production upgrade, this is that moment.
I covered the individual features across several posts as they shipped through beta and RC — MeshGradientPainter in beta02, SoundEffectOnInteraction and the passkey semantics in RC01. But stable graduation is a different conversation. This post is about what changed between RC01 and the final stable build, what actually breaks on upgrade, and what's genuinely new that didn't surface in any of the pre-release writeups.
Wide Color Gamut — The Feature Nobody Was Talking About
The flashiest 1.12 features got covered when beta landed. Wide Color Gamut (WCG) support didn't get a blog post or a tweet — it just showed up in the stable release notes, and it's actually one of the most meaningful changes for apps targeting high-end hardware.
Before 1.12.0, Compose quietly crushed non-sRGB colors during rendering. If you created a Color in the Display P3 or Adobe RGB color space, Compose would convert it to sRGB before painting — silently, without warning. On hardware that supports wide gamut displays (Pixel 9 series, high-end Samsung foldables), you were leaving real color fidelity on the table without knowing it.
In 1.12.0 on API 29+, non-sRGB colors are now preserved through the rendering pipeline and passed to the compositor in their native color space. On older APIs, it falls back safely to sRGB. You don't change any code to get this — if you were already using Display P3 colors, they'll automatically look better. If you've been using only sRGB, nothing changes.
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.colorspace.ColorSpaces
// On API 29+ with Compose 1.12.0, this is now preserved end-to-end
// On older APIs it falls back gracefully to sRGB — no crash, no action needed
val p3Red = Color(0.9f, 0.1f, 0.1f, colorSpace = ColorSpaces.DisplayP3)
val adobeRgbColor = Color(0.9f, 0.5f, 0.1f, colorSpace = ColorSpaces.AdobeRgb)
For Musist, where album art dominates the screen, this is a meaningful win. The most vibrant cover art assets already contain wide gamut data — what was missing was the runtime preserving it. Now it does. If your app is media- or photography-heavy, it's worth running the same UI on a Pixel 9 before and after the upgrade just to see the difference.
Variable Fonts — Zero-Config GMS Certificates
Variable fonts in Compose have always worked, but the setup has always had an invisible tax: you needed to supply the correct GMS certificate to verify downloadable Google Fonts at runtime, and getting that wrong produced a silent fallback to the system font with no obvious error.
In 1.12.0, ui-text-google-fonts handles the certificate automatically. You just request the font and set variation settings — the certificate management happens in the library:
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontVariation
import androidx.compose.ui.text.googlefonts.Font
import androidx.compose.ui.text.googlefonts.GoogleFont
val provider = GoogleFont.Provider(
providerAuthority = "com.google.android.gms.fonts",
providerPackage = "com.google.android.gms",
// No certificate list needed — 1.12.0 handles it
)
val flexFont = FontFamily(
Font(
googleFont = GoogleFont("Google Sans Flex"),
fontProvider = provider,
variationSettings = FontVariation.Settings(
FontVariation.weight(700),
FontVariation.slant(0f)
)
)
)
If you're on a pre-1.12 BOM and your downloadable font sometimes silently falls back to system default, this is probably why. The fix is a BOM bump, not a code change.
Foundation 1.12.0 — FlexBox and WindowInsets Are Now Stable
The alpha/experimental story for Compose has always been "usable in dev, questionable in production." Two Foundation APIs that were experimental for most of 1.12's pre-release cycle graduated to stable in the final build.
FlexBox is stable. No more @OptIn(ExperimentalFlexBoxApi::class). The stable release also adds maxItemsInEachLine, which limits how many items can wrap to a single line — useful for tag clouds and chip rows where you want a maximum density without the layout becoming unreadable:
FlexBox(
modifier = Modifier.fillMaxWidth(),
config = FlexBoxConfig(
direction = FlexDirection.Row,
wrap = FlexWrap.Wrap,
justifyContent = JustifyContent.Start
),
maxItemsInEachLine = 4
) {
skills.forEach { skill ->
Chip(label = skill, modifier = Modifier.flex(grow = 0f))
}
}
WindowInsets visibility and animation APIs are stable. This one matters for apps that animate the keyboard in and out — the WindowInsets APIs for querying visibility and hooking into the inset animation lifecycle are now safe to build on without the experimental opt-in.
DialogProperties Gets Blur and Shape Controls
A small but welcome addition that wasn't flagged in the pre-release posts: DialogProperties and PopupProperties now accept blur and shape parameters.
Dialog(
onDismissRequest = { showDialog = false },
properties = DialogProperties(
blurBehindRadius = 12.dp,
backgroundBlurRadius = 20.dp,
scrimAlpha = 0.6f,
windowShape = RoundedCornerShape(24.dp)
)
) {
// Dialog content
}
On devices that support window blur (API 31+ with the right hardware compositing path), blurBehindRadius blurs the content behind the dialog window — the frosted glass effect that every design team wants and that used to require a custom Window flag setup. backgroundBlurRadius blurs the dialog's own background. Both degrade gracefully on unsupported hardware, so you can ship this without guarding it.
The Espresso Bridge for Hybrid Apps
If you're migrating from Views to Compose incrementally — which most production apps are — this is a quality-of-life upgrade for your test suite. Compose 1.12.0 adds onRootWithViewInteraction to scope Compose test interactions to a specific View hierarchy:
import androidx.compose.ui.test.junit4.onRootWithViewInteraction
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.*
@Test
fun testComposableInsideRecyclerViewRow() {
val specificRow = onView(allOf(withId(R.id.row), hasDescendant(withText("Item #5"))))
// Scopes all Compose interactions to the Compose root inside that specific row
composeTestRule.onRootWithViewInteraction(specificRow)
.onNodeWithTag("fav_icon")
.performClick()
// Assertions...
}
Before this, testing Compose content inside a RecyclerView row was awkward — you'd either write a full Espresso test that couldn't verify Compose semantics, or you'd restructure your layout to make the Compose root more accessible. Now the two worlds talk to each other cleanly.
Worth knowing for Nodat: the app's main feed is still a hybrid RecyclerView/Compose setup during migration. This test API unlocks proper semantic testing for those rows without requiring a full Compose migration first.
Breaking Changes — Read This Before You Bump the BOM
1.12.0 has a few genuine breaking changes. None are catastrophic, but they'll cause compile errors or silent behavior changes if you don't catch them in code review.
TextObfuscationMode.Default is gone — use .System
Covered in the RC01 post, but it ships as a stable breaking change now. TextObfuscationMode.Default was renamed to TextObfuscationMode.System. If you use BasicSecureTextField, you'll get a compile error unless you update the reference. Find all usages before merging the BOM bump:
// Before 1.12.0
BasicSecureTextField(
textObfuscationMode = TextObfuscationMode.Default
)
// 1.12.0+
BasicSecureTextField(
textObfuscationMode = TextObfuscationMode.System
)
Modifier.onFocusedBoundsChanged is a no-op
If you were using Modifier.onFocusedBoundsChanged to react to focus movements for custom scroll-to-focus behavior, it now does nothing. The replacement is FocusTargetModifierNode.getFocusedRect(), which you query on demand from inside a custom modifier node rather than subscribing to a stream. This is a behavior change, not a compile error — your code will still build, but the callback will never fire.
ExposedDropdownMenu needs an updated import
ExposedDropdownMenu is now an extension function on ExposedDropdownMenuBoxScope rather than a top-level composable. This changes the import path. If you get "unresolved reference" on ExposedDropdownMenu after the upgrade, update the import to the new fully-qualified path from the Material3 package.
Test default input mode changed
Compose UI tests now default to InputMode.Touch instead of InputMode.Keyboard. Tests that relied on keyboard-mode behavior without explicitly setting it may start failing. If you have tests that do keyboard-specific interactions, set the input mode explicitly:
@get:Rule
val composeTestRule = createComposeRule(
ComposeUiTestConfig(inputMode = InputMode.Keyboard)
)
AGP requirement: Compose 1.12.0 requires AGP 9.2.0 or higher. If you're still on AGP 8.x or earlier 9.x builds, you need to upgrade AGP first. This also means AGP 9.2's R8 AtomicReferenceFieldUpdater optimization kicks in automatically — coroutine performance improves without any code changes on your end.
LazyGridState Cache Window Configuration Changed
A quieter breaking change in Foundation: the way you configure LazyLayoutCacheWindow for LazyVerticalGrid has moved. Providing it via LazyGridState is now deprecated — you pass it directly to the composable instead. The old API still compiles with a deprecation warning, so it won't block your build, but update it before the next major version removes the overload entirely.
The Rest of the August 12 Stable Wave
Six libraries stable on the same day is unusual. Here's the practical impact of the others:
Fragment 1.9.0 stable. RC01 was covered in detail two days ago — fragment-ktx is fully absorbed into the main artifact, AndroidFragment gets maxLifecycle to prevent lifecycle leaks in Compose pagers, and Fragment enters maintenance mode. If you're on RC01, the stable bump is a no-op. If you skipped RC01, read that post first.
Tracing 2.0.0 stable. The full in-process Perfetto engine — TraceDriver, Tracer, traceCoroutine() for suspend boundary propagation — is now stable and safe to build on. Benchmark 1.5.0-beta01 (which also landed today) already uses it to merge in-process trace events into Macrobenchmark output.
Navigation3 1.1.6 stable. Patch release over 1.1.5 — no new APIs. If you're already on Navigation3, this is a routine update.
Paging 3.5.1 stable. Bug-fix-only over 3.5.0, which introduced asItemSnapshotListFlow() and the manual load trigger APIs.
Appcompat 1.8.0 stable. Routine minor release for the Views compatibility layer — no standout changes relevant to pure Compose apps.
How to Upgrade — The Exact Steps
If you're using the Compose BOM, bump to the August 12 BOM version. If you're managing individual artifact versions, update to 1.12.0 for compose.ui, compose.foundation, compose.runtime, and compose.animation. Confirm AGP is at 9.2.0+, then run through this checklist before merging:
- Search for
TextObfuscationMode.Default— replace withTextObfuscationMode.System - Search for
onFocusedBoundsChanged— audit if you were using it for scroll-to-focus; migrate togetFocusedRect()if so - Search for
ExposedDropdownMenu— verify imports after upgrading if you hit "unresolved reference" - Run your Compose UI test suite — watch for failures from the
InputMode.Touchdefault flip - Check
LazyGridStateusages if you were passingLazyLayoutCacheWindowvia state — migrate to the composable parameter - Run the app on a wide-gamut display (Pixel 9 or similar) if your app renders custom colors — verify the new WCG behavior looks correct
The bottom line: the breaking changes in 1.12.0 are real but narrow — they'll show up as compile errors or one failing test, not as runtime crashes that slip past review. This is a stable release worth shipping to production. The WCG improvement, FlexBox graduation, and Dialog blur controls are all additive; the migration risk is low if you run the checklist above.
The bigger picture from this release: Compose's surface area for "things that are safe to build on in production" grew considerably in 1.12. FlexBox stable, WindowInsets animation stable, InterceptPlatformTextInput stable, the passkey semantics stable — these are all APIs that were technically available before but came with the experimental asterisk. That asterisk is gone now. Build on them.
No comments yet. Be the first to leave one!