Compose 10 min read

Compose 1.13.0-alpha01: FlexBox Hits Stable, SelectionState API, and the Next Cycle Begins

Compose 1.12.0 went stable on August 12. And in classic Google fashion, the same day the release notes landed for stable, alpha01 of 1.13 also dropped — quietly, without fanfare, buried in the AndroidX all-channel release page.

That's actually a good sign for the project's health. The team isn't holding its breath waiting for one version to ship before thinking about the next. But it does mean there's a decision to make: what actually matters in this alpha, and what's noise you can filter out for the next six months while it stabilizes?

I went through every change in Compose UI and Foundation 1.13.0-alpha01 and pulled out the things that will actually affect how you write production Compose code when this graduates. There are a few that matter more than the rest.

Stable
FlexBox graduates from experimental
New
SelectionState API for programmatic text selection
Default
Click interaction sounds on Android

FlexBox Is Stable — and Gets One More Feature on the Way In

FlexBox launched as @ExperimentalFlexBoxApi in 1.11 and was still experimental through the 1.12 cycle. In 1.13.0-alpha01, it graduates to stable — no more @OptIn annotations needed on any of the FlexBox configuration APIs.

If you've been holding off using it in production because of the experimental gate, that gate is now gone. The mental model is still the same: you configure direction, wrap, justifyContent, and alignItems via FlexBoxConfig, and children declare their flex basis/grow/shrink via Modifier.flex(). What's new alongside the promotion is a maxItemsInEachLine parameter that caps how many items appear on a single row or column before wrapping kicks in, regardless of available space.

// No @OptIn needed as of 1.13.0-alpha01
@Composable
fun TagCloud(tags: List) {
    FlexBox(
        config = FlexBoxConfig(
            direction = FlexDirection.Row,
            wrap = FlexWrap.Wrap,
            justifyContent = JustifyContent.Start,
            maxItemsInEachLine = 4  // wrap after 4 items max
        )
    ) {
        tags.forEach { tag ->
            Chip(
                label = tag,
                modifier = Modifier.flex(grow = 0f, shrink = 1f)
            )
        }
    }
}

The maxItemsInEachLine addition is useful for tag clouds, chip groups, and horizontal pill rows where the visual density matters as much as the available width. In Musist's playlist detail screen, I've been faking this with explicit item count tracking — wrapping it myself before passing to a Row. With this, that logic lives inside FlexBox where it belongs.

Migration from experimental: If you wrote a @Suppress("OPT_IN_USAGE") or @OptIn(ExperimentalFlexBoxApi::class) at a site-wide level for FlexBox, those can be removed as of 1.13.0. The suppression itself is technically harmless if you leave it, but it'll generate a lint warning about unnecessary opt-ins once the annotation class is removed.

SelectionState — Finally, Programmatic Control Over Text Selection

Text selection in Compose has always been mostly fire-and-forget. You wrap content in a SelectionContainer, the user long-presses, the framework handles the handles (pun intended), and you get a copy action. What you've never been able to do is observe what text is currently selected, or set the selection programmatically from outside the container.

1.13.0-alpha01 introduces SelectionState to fix exactly that. You create a state object, pass it into SelectionContainer, and you can read the current selection as a TextRange and write it from anywhere that has the reference:

@Composable
fun SelectableCodeBlock(code: String) {
    val selectionState = rememberSelectionState()

    Column {
        Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            Button(onClick = {
                // Select all text programmatically
                selectionState.selection = TextRange(0, code.length)
            }) {
                Text("Select All")
            }
            Button(onClick = {
                selectionState.selection = TextRange.Zero
            }) {
                Text("Clear")
            }
        }

        // Observe what the user has selected
        val selectedText = code.substring(
            selectionState.selection.start,
            selectionState.selection.end
        )
        if (selectedText.isNotEmpty()) {
            Text(
                text = "${selectedText.length} chars selected",
                style = MaterialTheme.typography.labelSmall,
                color = MaterialTheme.colorScheme.primary
            )
        }

        SelectionContainer(selectionState = selectionState) {
            Text(text = code, fontFamily = FontFamily.Monospace)
        }
    }
}

This opens up patterns I've wanted for a while. A "copy code" button that programmatically selects a code block before copying — instead of copying directly from clipboard APIs, which requires permissions depending on API level. A search-within-page feature that highlights and scrolls to the matched range. Rich text editors where your toolbar drives selection state rather than just reading it.

It's still an alpha API, so the exact shape of SelectionState and rememberSelectionState may shift before stable. But the concept is sound and the use cases are well-established. Worth tracking.

Click Sounds Are Now Automatic on Android

This one caught me off guard when I read it: in Foundation 1.13.0-alpha01, clicks on Modifier.clickable elements now automatically play interaction sound effects on Android. Not a UI flash, not a haptic pulse — the system click sound, the same one Android's native controls have emitted for years.

The implementation hooks into the Android SoundEffectConstants system, which means it respects the user's system sound settings. If someone has sounds off, nothing plays. If they're on an accessibility profile that mutes UI sounds, nothing plays. It's consistent with what every native Android control already does — Compose was the outlier in not doing this, and that inconsistency showed up as a subtle "something feels slightly off" reaction from users who had system sounds enabled.

If you've already implemented click sounds yourself via a custom Indication or explicit LocalView.current.playSoundEffect(SoundEffectConstants.CLICK) calls, audit those sites after upgrading. You'll get double sounds otherwise. The isClickSoundEnabled parameter on Modifier.clickable (or a flag in ComposeFoundationFlags) will let you opt specific elements out if needed.

The 1.12.0-RC01 post I wrote last week covered SoundEffectOnInteraction as a separate opt-in composable for accessibility-aware sound effects. That was in Compose UI. The Foundation change in 1.13.0-alpha01 is different — it's built directly into Modifier.clickable at the foundation layer, making sounds the automatic default rather than something you opt into. Two different APIs, one direction: Compose finally sounds like the platform it runs on.

Testing Infrastructure Gets Some Real Upgrades

The testing surface in Compose UI 1.13.0-alpha01 expanded in a few meaningful directions. These matter most if you have a non-trivial Compose test suite, which at this point you really should have if you're shipping anything serious.

TestFailurePolicy and TestFailureHandler

The new TestFailurePolicy and TestFailureHandler in ComposeTestConfig let you intercept test failures before they propagate. The main use case is failure artifact collection — capturing screenshots, recording semantic tree state, or writing a custom log entry — at the moment a test assertion fails, before the test runner tears down the composition:

@get:Rule
val composeTestRule = createComposeRule(
    testConfig = ComposeUiTestConfig(
        failureHandler = { failure, description ->
            // Capture screenshot on failure
            composeTestRule.onRoot().captureToImage().asAndroidBitmap()
                .compress(Bitmap.CompressFormat.PNG, 100, screenshotStream)
            // Re-throw so the test still fails
            throw failure
        }
    )
)

Previously, getting a screenshot on failure required a custom TestWatcher rule that fired after the composition was already torn down — meaning you'd often capture an empty or partially-cleaned-up screen. The new hook fires while the composition is still live.

onDescendants() and better assertions

The new onDescendants() selector on SemanticsNodeInteraction lets you query all semantics nodes within a subtree without knowing their exact depth. Useful for containers that might have variable nesting, like a recycled view host or a dynamically-composed card:

composeTestRule
    .onNodeWithTag("ArticleCard")
    .onDescendants()
    .filterToOne(hasText("Read more"))
    .performClick()

Also notable: captureToImage() now accepts a timeoutMillis parameter (default 2000ms) to control how long it waits for a redraw pass before failing. On slow emulators with complex compositions, the previous fixed timeout caused intermittent failures. This alone will make some flaky screenshot tests deterministic.

Accessibility: hintText Semantics and SemanticsProperties.alpha

Two accessibility additions that round out long-standing gaps.

SemanticsProperties.alpha exposes the resolved composite alpha of a semantics node — the product of all the alpha modifiers applied to it and its parents. This is primarily useful in tests where you need to verify that an element is visually hidden via alpha rather than gone from the tree. Previously you had to walk the modifier chain yourself or check via screenshot diffs; now you can assert it directly:

composeTestRule
    .onNodeWithTag("LoadingOverlay")
    .assert(SemanticsMatcher("alpha is 0") { node ->
        node.config.getOrNull(SemanticsProperties.alpha) == 0f
    })

The hintText semantic property maps to AccessibilityNodeInfo.hintText on Android — the grayed-out placeholder text in text fields that screen readers read as context for what to type. Compose's BasicTextField didn't surface this properly before; the new property closes that gap and makes Compose text fields behave correctly with TalkBack and other assistive services for users who rely on hint text to navigate forms.

LazyLayout Cache Window Changes — One Migration Note

The LazyLayout prefetch configuration story is changing shape in 1.13.0-alpha01. Previously, you configured prefetch strategy and cache window via LazyGridState:

// Old pattern (deprecated in 1.13.0-alpha01)
LazyVerticalGrid(
    columns = GridCells.Fixed(3),
    state = rememberLazyGridState().also {
        it.prefetchStrategy = MyCustomStrategy()
    }
)

Going forward, LazyLayoutCacheWindow is passed directly to the composable function instead. The state object stops being the configuration surface for prefetch behavior. The old APIs are deprecated in alpha01 — not removed yet, but expect them gone by the time 1.13 reaches stable. If you've customized prefetch for performance-sensitive list screens, note the migration direction now even if you're not upgrading yet.

Also new: isMultiLaneCacheWindowEnabled and cache window support lands in lazy staggered grid layouts, which previously had no cache window configuration at all. For apps like Samachar where staggered grids are load-bearing (the explore tab uses a staggered layout for article cards), this means the same prefetch tuning you've been applying to LazyVerticalGrid can now apply to staggered layouts too.

What Else Landed

A few smaller changes worth knowing exist without dedicating full sections to them:

Should You Touch Alpha01 in Production?

No. That's not what alphas are for. The API shape for SelectionState, the sound behavior configuration surface, and the LazyLayout prefetch migration target are all subject to change before stable. Adopting any of these in a production screen today means tracking alpha changelogs every release cycle until they stabilize — and Compose alphas aren't known for being boring.

What you should do: if FlexBox is already in your codebase behind @OptIn, you can remove those annotations after you upgrade to 1.13.0-alpha01 and verify nothing breaks. That's a mechanical migration and the risk is low since the API itself isn't changing, just the experimental annotation class is being removed.

For everything else, the pattern I've used across every new Jetpack alpha cycle applies here: read the release notes now, understand the direction, prototype the new APIs in a non-critical screen or a dedicated sample module, and be ready to adopt them for real when they reach RC. The six months between alpha01 and stable for a Compose release cycle is enough time to develop informed opinions about what works and what still feels rough — which puts you ahead of the developer who reads the stable release notes cold and wonders why the API is shaped the way it is.

The bigger story here is that the Compose team is shipping meaningful API additions to 1.13 while the 1.12 ink is still wet. FlexBox stable is a real production unlock. SelectionState fills a gap that's been frustrating since 1.0. The testing improvements make screenshot testing less brittle in ways that translate directly into CI reliability. Not every alpha cycle delivers changes this clearly motivated — this one does.

Comments 0

No comments yet. Be the first to leave one!

Leave a comment