Compose 10 min read

Compose 1.12 RC01: Sound Effects, Passkey Semantics, and the APIs Before Stable

Compose 1.12.0-rc01 dropped July 29, and if you track these releases at all, you know what RC means: stable is probably two to four weeks away. My previous post on 1.12 covered MeshGradient, SwipeToReveal with programmatic control, and Grid Named Areas — the visually demonstrable stuff that showed up in beta02. But there's a parallel track of changes that landed across the beta01 and rc01 windows that barely got a mention anywhere, and some of them have a bigger day-to-day impact on production apps than a mesh gradient you might never actually reach for.

This post covers that second track: sound effects for interactive components, Credential Manager integration wired directly into the Compose semantics tree, a breaking rename in BasicSecureTextField's obfuscation mode, and the stable graduation of platform text input interception. None of these are glamorous. All of them will show up in your code before stable ships.

RC01
July 29 — stable imminent, likely within weeks
New
SoundEffectOnInteraction composable
Breaking
TextObfuscationMode.Default renamed to .System

1. What RC01 Actually Means for Your Upgrade Schedule

Release candidates in the Compose world don't usually ship big new features — they're the stability window where the team fixes regressions surfaced during beta testing and makes any last API renames before the API is locked. RC01 itself carries one notable API change (the TextObfuscationMode rename, covered below) and a round of bug fixes, including a text selection toolbar positioning fix and a correction for mouse selection starting in empty areas within SelectionContainer.

What this means practically: the safe play is to update to the 1.12.0-rc01 BOM now and run it on your internal test builds. You want to catch any issues before stable forces the upgrade path on you. The BOM coordinates are already published, the Kotlin compatibility requirements haven't changed, and 1.12 is additive — the TextObfuscationMode rename aside, there are no API removals that would break compilation on most apps.

BOM to use: androidx.compose:compose-bom:2026.07.01-rc01 (or check the latest published BOM that pins 1.12.0-rc01 across compose artifacts). Start with internal testing. Ship to production when stable is published.

2. SoundEffectOnInteraction — Compose Finally Plays Click Sounds

This one is small but genuinely overdue. In the View system, View.playSoundEffect() has been part of the accessibility contract since API 1. When a user taps a button, the system plays a subtle click sound. When focus moves to a new element, there's a navigation sound. These cues matter most for users who rely on audio feedback — TalkBack users, low-vision users navigating by ear — but they're also part of the system-wide audio contract that sighted users have absorbed without noticing.

Compose never played these sounds automatically. A Button in Compose uses Modifier.clickable under the hood, and for a long time clickable simply didn't invoke the platform sound effect system. You could work around it by calling LocalView.current.playSoundEffect(SoundEffectConstants.CLICK) manually from an onClick lambda, but that's the kind of boilerplate you forget to add and then wonder why your Compose screens feel slightly less polished than the View-based ones next to them.

Compose 1.12 adds SoundEffectOnInteraction — a new composable that automatically attaches click and focus-navigation sound effects to any interactive component it wraps:

@Composable
fun ActionButton(
    label: String,
    onClick: () -> Unit
) {
    SoundEffectOnInteraction {
        Button(onClick = onClick) {
            Text(label)
        }
    }
}

Wrap any focusable, clickable composable with it and the correct system sound plays at the right moment — CLICK on tap, NAVIGATION_UP/DOWN/LEFT/RIGHT as focus moves. The composable respects the system's sound settings, so it won't play if the user has disabled interaction sounds in accessibility settings. You don't manage that state yourself.

When you actually need this vs. when you don't

If you're building on top of Material3 components — Button, Chip, IconButton, Switch — the M3 library will likely apply this itself once it catches up to the 1.12 runtime. Where you need to think about it is custom interactive components: a card with Modifier.clickable that you styled from scratch, a custom bottom sheet handle, a tap target in a canvas-drawn UI. Anything where you own the interaction layer directly rather than delegating it to a Material3 widget is a candidate.

In Samachar's news feed, the story cards are custom composables with a Modifier.combinedClickable for tap and long-press. Those never played a click sound. Wrapping them in SoundEffectOnInteraction is a one-line fix that brings them into parity with a standard Button from an accessibility audit perspective.

3. credentialRequest Semantics — Passkeys Finally Work Correctly in Compose Forms

This is the change I'm most glad to see land, because it fixes a real integration gap that's affected every Compose login screen since Credential Manager launched.

When Android's Credential Manager wants to offer passkeys or saved passwords for a login form, it needs to know what type of credential a given input field is requesting. In the View system, this happens through autofill hints — AUTOFILL_HINT_USERNAME, AUTOFILL_HINT_PASSWORD, AUTOFILL_HINT_CREDENTIAL. Credential Manager reads these from the accessibility/autofill tree to decide when and what to surface.

In Compose, TextField and BasicTextField expose their autofill hints through the Semantics tree — but Credential Manager's passkey prompts need richer context than a basic autofill hint can provide. Prior to 1.12, getting Credential Manager to correctly trigger a passkey prompt on a Compose login form required a bunch of manual plumbing: wiring up a CredentialManager instance, launching the request flow from onClick or onFocus callbacks, and handling the result yourself. The declarative "the OS sees the form and offers the right thing" flow that works automatically in XML layouts didn't fully work in Compose.

Compose 1.12 introduces credentialRequest as a first-class Semantics property, backed by a CredentialRequestData helper that encodes what kind of credential your field is expecting. The autofill framework and Credential Manager read this from the semantics tree and can now trigger the correct passkey or password prompt automatically — the same behavior View-based forms get for free:

@Composable
fun LoginForm() {
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }

    Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
        OutlinedTextField(
            value = email,
            onValueChange = { email = it },
            label = { Text("Email") },
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
            modifier = Modifier.semantics {
                contentType = ContentType.Username
                credentialRequest = CredentialRequestData(
                    isPasswordField = false
                )
            }
        )

        OutlinedTextField(
            value = password,
            onValueChange = { password = it },
            label = { Text("Password") },
            visualTransformation = PasswordVisualTransformation(),
            modifier = Modifier.semantics {
                contentType = ContentType.Password
                credentialRequest = CredentialRequestData(
                    isPasswordField = true
                )
            }
        )
    }
}

The credentialRequest property is available on API 34+, matching the minimum API level for the full Credential Manager passkey experience. On older API levels, the property is a no-op — the autofill framework falls back to its existing behavior, so you're not adding a runtime check yourself.

Why this matters more than it sounds

I've shipped a login screen in Nodat that used Credential Manager for passkey sign-in. Getting the UI to trigger the passkey prompt correctly in the Compose form required a FocusRequester, a LaunchedEffect, and a manually fired getCredential() call — all because the declarative flow didn't understand what the field was for. With credentialRequest Semantics, the OS can read that context directly from the composable tree and drive the prompt on its own schedule, the same way it does with a View-based form. That's less code in my screen, fewer edge cases around when the launch effect fires, and a more reliable UX for the user.

Note: The credentialRequest Semantics API is marked as requiring API 34+ (@RequiresApi(34)). Wrap the semantics block in an API check if your minSdk is below 34, or rely on the CredentialRequestData companion's guard utilities — the behavior on older APIs is silent no-op.

4. TextObfuscationMode.Default Is Gone — Check Your Secure Text Fields

This is the rc01-era change most likely to cause a compile error when you update your BOM, so it gets its own section even though the fix is mechanical.

BasicSecureTextField has a textObfuscationMode parameter that controls how the field renders typed characters. Before 1.12, the options were:

In 1.12.0-rc01, TextObfuscationMode.Default is renamed to TextObfuscationMode.System. The semantics are identical — it still defers to the platform convention — but the name is now accurate. "Default" was ambiguous (default value? default system behavior?). "System" is explicit.

The migration is a straight rename:

// Before
BasicSecureTextField(
    state = passwordState,
    textObfuscationMode = TextObfuscationMode.Default
)

// After
BasicSecureTextField(
    state = passwordState,
    textObfuscationMode = TextObfuscationMode.System
)

If you were relying on the default argument — i.e., not passing textObfuscationMode at all — you're not affected. The BasicSecureTextField's default parameter value has been updated to reference System internally, so calls without an explicit mode continue to compile and behave identically.

RevealLastTyped now always reveals — and that's a behavioral change

Alongside the rename, TextObfuscationMode.RevealLastTyped got a behavior tweak that's worth knowing about separately. Previously, whether the "last typed" character was actually revealed depended on the IME and the system's reveal-password preference. Some IMEs respected it, some didn't, and the result was inconsistent across devices.

In 1.12, RevealLastTyped is defined as an absolute override — it always reveals the last typed character, regardless of the IME or system setting. If a user has disabled "show password" at the system level, RevealLastTyped still shows the character. Use TextObfuscationMode.System if you want to respect the user's system preference; use RevealLastTyped only when your UX explicitly requires it regardless of system settings.

Action needed: Search your codebase for TextObfuscationMode.Default — replace with TextObfuscationMode.System. If you're using TextObfuscationMode.RevealLastTyped, verify the new absolute-reveal behavior is what you actually want before upgrading.

5. InterceptPlatformTextInput and PlatformTextInputInterceptor — Now Stable

These two APIs have been experimental since they landed in early 1.11 alphas, and they've been gated behind @OptIn(ExperimentalComposeUiApi::class) the whole time. As of 1.12 rc01, they're stable — no opt-in annotation needed.

The short explanation: InterceptPlatformTextInput is a composable that lets you intercept the channel between Compose's text input system and the platform IME. You slot in a handler that receives the active PlatformTextInputMethodRequest before it reaches the actual keyboard, letting you modify it, redirect it, or replace the IME session entirely with your own input mechanism.

The canonical use case is custom input experiences — an emoji picker that behaves like an IME, a formula editor that intercepts character input and replaces it with LaTeX tokens, a PIN entry field that uses a custom numpad instead of the system keyboard. Without this API, those scenarios required either reflection hacks into the IME protocol or a custom InputConnection implementation that bypassed Compose's text handling entirely.

@Composable
fun CustomNumpadField(state: TextFieldState) {
    InterceptPlatformTextInput(
        interceptor = { request, nextHandler ->
            if (shouldUseCustomNumpad(request)) {
                // Route to our custom input instead of platform IME
                handleWithCustomNumpad(request)
            } else {
                nextHandler(request)
            }
        }
    ) {
        BasicTextField(state = state)
    }
}

Most apps don't touch this. If your text fields use the system keyboard and you're not building custom input widgets, the stable graduation is invisible to you. But if you've been waiting on a stable API to ship a custom input experience without @OptIn, the gate is open now.

Should You Upgrade to RC01 Now?

Yes, but deliberately. Bumping the BOM to rc01 on a feature branch and running it through your CI and internal test suite is the right move today. What you're looking for: any compile errors from the TextObfuscationMode.Default rename, any behavioral regressions in scroll-heavy screens from the Foundation bug fixes, and any new lint warnings from API changes that landed across the beta window.

The visual stuff — MeshGradient, SwipeToReveal, Grid Named Areas — is all still gated behind @OptIn annotations, so if you weren't using those experimentals before, they won't affect you. The stable-track changes (SoundEffectOnInteraction, credentialRequest Semantics, InterceptPlatformTextInput stable) are either additive or breaking-by-rename, not behavioral surprises in existing code.

One thing I've appreciated about the 1.12 release is how it fills in the operational gaps that beta testers spent 1.11 complaining about. Passkey Semantics, click sounds, and stable platform text interception aren't headline features for a marketing slide, but they're the difference between Compose feeling like a first-class production toolkit and Compose feeling like something you work around. The fact that rc01 is out and the API set is locked in is a signal that the team is satisfied with those foundations. I'll take it.

Comments 0

No comments yet. Be the first to leave one!

Leave a comment