跪拜 Guibai
← Back to the summary

Compose 1.12 Lands Mesh Gradients, WCG, and a SideEffect That's 90% Faster Than LaunchedEffect

compose-header.png

Jetpack Compose August 2026 release is now stable.

This time the core Compose modules all reach 1.12:

androidx.compose.runtime:runtime:1.12.0

This release adds many new capabilities: Mesh Gradient, wide color gamut (WCG) support, named areas in Grid, integration with Android Credential Manager, and improvements in testing and performance.

To upgrade to this version, simply update the compose-bom in your project to 2026.08.00:

implementation(platform("androidx.compose:compose-bom:2026.08.00"))

Breaking Changes

AGP and Compile SDK: Compose 1.12 raises compileSdk to API 37 and requires a minimum AGP of 9.1.1. Compose will always build targeting the latest compileSdk.

Even though this might matter less in the AI coding era, I still have to complain about this Android upgrade — there is zero compatibility. Raising to API 37 essentially means AGP, Kotlin, and Android Studio all need to be updated together; one change triggers everything. Sometimes I even feel that only seasoned Android developers can figure all this out.

Modifier.onFirstVisible() has been deprecated. You need to migrate to Modifier.onVisibilityChanged(), which can track visibility thresholds more precisely.

Also, if you use the Styles API, the breaking impact here is even bigger. What does that mean?

If you previously packaged a Lib A using Compose and used it in Project B; now you want to try the latest Compose and upgrade B, compilation might be fine, but if Lib A actually calls experimental APIs that changed this time, it could still crash at runtime. So be careful!

I've already discovered that the size function in the Styles API has changed.

The main reason is that the experimental Styles API does not guarantee binary compatibility; when encountering such changes, you should recompile and upgrade related libraries together.

Alright, without further ado, let's dive into the update interpretation.

SideEffect Overload with Key

SideEffect now supports passing a key. This allows a one-shot side effect to execute when a specific key changes. If you need neither a coroutine nor a dispose callback, it is more suitable than LaunchedEffect or DisposableEffect.

SideEffect performance can be up to 90% faster than LaunchedEffect and about 20% faster than DisposableEffect.

However, its execution timing is earlier than DisposableEffect and LaunchedEffect. If you are migrating existing side effects to this API, you need to pay attention to this, especially for LaunchedEffect which depends on being scheduled to start after the current frame completes.

@Composable
fun AnalyticsTracker(userId: String, screenName: String) {
    SideEffect(key1 = userId, key2 = screenName) {
        analytics.logScreenView(userId, screenName)
    }
}

As a developer, I didn't expect this feature to arrive so late.

Previously, if we wanted to write a side effect for a Composable that only takes effect on first composition, we could only write LaunchedEffect(Unit). But this Effect needs to launch a coroutine, thus incurring additional scheduling and management overhead.

Mesh Gradients

Compose 1.12 introduces MeshGradientPainter, used to create more natural color gradients composed of multiple control points.

mesh-gradient.gif

val rows = 1
val columns = 1

val gradientPainter = remember {
    MeshGradientPainter(rows, columns) {
        // Parameters: row, column, position, color
        setVertex(0, 0, Offset(0f, 0f), Color.Red)     // Top-left
        setVertex(0, 1, Offset(1f, 0f), Color.Blue)    // Top-right
        setVertex(1, 0, Offset(0f, 1f), Color.Green)   // Bottom-left
        setVertex(1, 1, Offset(1f, 1f), Color.Yellow)  // Bottom-right
    }
}

Box(
    modifier = modifier
        .aspectRatio(16 / 9f)
        .fillMaxWidth()
        .paint(gradientPainter)
)

What a cool update!!!

Wide Color Gamut and HDR Support

Modern display devices can provide richer colors and higher dynamic range. Compose 1.12 enables Wide Color Gamut (P3) and HDR rendering support across the entire chain of graphics, paint, and shader.

Colors defined in non-sRGB color spaces, such as Display P3, can now be preserved all the way to the platform rendering stage without being clamped to the sRGB gamut midway. If an unsupported color space is used, such as CieXyz, CieLab, Oklab; or a color space not supported by the current Android version, such as Bt2020Hlg on Android 13 and below; or the app runs on Android 9 (API 28) and below, the color will safely fall back to sRGB.

Other notable changes:

Styles

At Google I/O, Google already shared the early vision for the Compose Styles API: a unified and high-performance way to define styles for components.

Since then, Google has been continuously refining the underlying architecture, aiming for strict type safety and predictable correctness, while also supporting custom design systems.

This foundational capability still needs further polishing, so the API will remain in experimental status, and breaking changes may occur later.

Anyway, now that we're coding with AI, you can actually give it a try. The biggest change I've felt so far is: if you write a complex control, using this Styles API is much, much better than passing multiple Modifiers!

Modifier has a characteristic: order matters a lot. This leads to a problem when customizing styles — if your control already uses some Modifier parameters internally, it's hard to place the incoming Modifier in the right position. Also, if you want to support style customization at multiple positions, you need to pass in multiple Modifiers, which makes your control look unconventional — in Compose, controls usually have only one Modifier, and it's the first parameter.

Of course, the functionality of this Styles API actually overlaps a lot with Modifier. Even for controls like BasicText, style control can be done through Modifier, Styles, and TextStyle. Both Styles and TextStyle can be used to control the internal text style. I hope Google can converge these in the future; all roads leading to Rome is not a good thing for developers.

For an explanation of the Styles API, you can check out this article.

Interactive Two-Phase Transitions

First, DeferredTargetAnimation no longer requires experimental opt-in.

The new DeferredAnimatedContent and DeferredAnimatedVisibility can create two-phase transition animations, for example, following a predictive back gesture.

Manual animation control: During the deferred phase of the transition, animation properties like scale and offset can be manually controlled in real-time, for example, directly following a swipe gesture.

Smooth takeover: After the deferred phase ends, the transition engine takes over the subsequent animation and smoothly connects it, including velocity transfer.

Shared element support: SharedContentConfig adds a permitTransformDuringDeferredTransition flag to control whether shared elements undergo visual transformation along with the parent container during the deferred transition phase.

val state = remember { DeferredTransitionState(initialScreen) }
val transition = rememberDeferredTransition(state)

if (predictiveBackInProgress) {
    state.defer(targetScreen)
} else {
    state.animateTo(targetScreen)
}

transition.DeferredAnimatedContent(
    targetState = targetScreen,
    mutableTransformSpec = {
       MutableContentTransform {
           // Manually control properties during the deferred phase
           initialContentTransform { scale = swipeProgress }
       }
    }
) { screen ->
    ScreenContent(screen)
}

The two examples below show a scenario: after a gesture-driven animation ends, control is handed over to a triggered animation.

deferred-transition.gif

In fact, this is an animation tailor-made for predictive back.

Editable Text Formatting

The new API adds rich text formatting capabilities to BasicTextField's editable text. You can now programmatically apply and modify inline character formats and paragraph formats within the scope of TextFieldBuffer, using SpanStyle, ParagraphStyle, and the new addStyle() method. This scope can be textFieldState.edit { ... } or InputTransformation.

Additionally, TextFieldBuffer provides getSpanStyles() and getParagraphStyles(), which return TrackedRange objects, allowing you to read, update, or remove applied styles. TextFieldState adds a read-only property textStyles for querying currently active styles within different ranges; TextFieldBuffer provides originalTextStyles to view the formatting state before editing. Text formatting and custom annotations are preserved across configuration changes.

val state = rememberTextFieldState("Formatted text in Compose 1.12")

// Apply bold and color to a range of text
state.edit {
    addStyle(
        SpanStyle(fontWeight = FontWeight.Bold, color = Color.Blue),
        start = 0,
        end = 9
    )
}

// Query currently active styles from TextFieldState
val currentStyles = state.textStyles

Text Selection

The new SelectionState API allows programmatic control and observation of text selection state within SelectionContainer. By creating and hoisting SelectionState via rememberSelectionState() and passing it to SelectionContainer, you get a reactive selectedTexts, which is a list of AnnotatedString; you can also call methods like selectAll(), clear(), select(TextRange), extendSelectionByWord(), etc.

Also, you can get all selectable text items in layout order via getSelectableTexts(), and select text across Composables within multiple SelectionContainers in a global scope.

@Composable
fun ProgrammaticSelectionExample() {
    val selectionState = rememberSelectionState()

    Column {
        Button(
            onClick = { selectionState.selectAll() },
            modifier = Modifier.disableSelectionClearOnTap()
        ) {
            Text("Select All")
        }

        SelectionContainer(state = selectionState) {
            Text("Text content that needs to be selected programmatically.")
        }
    }
}

Credential Manager Integration

Compose text fields can now natively integrate with Android's Credential Manager (API 34 and above) through the Autofill framework. Below API 34, it is handled by the androidx.credential library. By attaching the new credentialRequest semantic property to a text field and passing in CredentialRequestData, you can directly prompt for Passkeys, saved credentials, or sign-in requests during user input.

@Composable
fun LoginField(textFieldState: TextFieldState) {
    val credentialData = remember {
        CredentialRequestData(
            // Specify Credential Manager request options
        )
    }

    BasicTextField(
        state = textFieldState,
        modifier = Modifier.semantics {
            credentialRequest = credentialData
        }
    )
}

Of course, regarding password Autofill, you can check out my article.

Other notable changes:

Named Areas in Grid Layout

The experimental Grid component now supports named areas, making complex two-dimensional layouts easier to maintain.

Previously, you had to maintain numeric row and column indices on each item; now you can define meaningful areas within GridConfigurationScope and place Composables by area name.

@OptIn(ExperimentalGridApi::class)
@Composable
fun DashboardLayout() {
    Grid(
        config = {
            area("header", row = 0, column = 0, rowSpan = 1, columnSpan = 2)
            area("sidebar", row = 1, column = 0)
            area("content", row = 1, column = 1)
            gap(16.dp)
        }
    ) {
        HeaderSection(modifier = Modifier.gridItem(areaId = "header"))
        NavigationSidebar(modifier = Modifier.gridItem(areaId = "sidebar"))
        MainContentView(modifier = Modifier.gridItem(areaId = "content"))
    }
}

I previously wrote an article explaining Grid, where I called Grid "the last piece of the responsive layout puzzle". If you want to review it, feel free to check it out.

Performance

Every release continuously invests in Compose performance, hoping the framework can help developers build apps that are both beautiful and smooth enough.

This time the main optimization is startup performance. In benchmark tests, Compose's Time to Initial Display (the time required for the app to produce the first frame) is now comparable to View.

time-to-initial-display.png

Thumbs up!!! Let's see who still says Compose startup is slower than View now.

Test Synchronization

Compose 1.12 adds several new test APIs to shorten test execution time and reduce flakiness during state sampling:

@Test
fun testAnimationStateFast() {
    composeTestRule.mainClock.autoAdvance = false

    while (composeTestRule.hasPendingWork()) {
        composeTestRule.mainClock.advanceTimeByFrame()
        composeTestRule.waitForIdle()

        composeTestRule.runOnUiThread {
            composeTestRule.runWithoutImplicitWait {
                // Most effective when querying multiple nodes in the same frame.
                // It avoids the repeated synchronization overhead that occurs with each individual query.
                val box1 = composeTestRule.onNodeWithTag("Box1").fetchSemanticsNode()
                val box2 = composeTestRule.onNodeWithTag("Box2").fetchSemanticsNode()

                assertThat(box1.boundsInRoot.right).isAtMost(box2.boundsInRoot.left)
            }
        }
    }
}

Other notable changes:

Happy Composing

Compose 1.12 gives app development more room for expression: Mesh Gradients, wide color gamut support, downloadable variable fonts, Credential Manager integration, and faster testing tools all arrive in this release.

Come on, comrades, embrace Compose!