跪拜 Guibai
← Back to the summary

Compose 1.12 Lands: Mesh Gradients, WCG/HDR Pipeline, and Deferred Animation for Predictive Back

Jetpack Compose August Edition Officially Released, Core Module 1.12

This release doesn't have any particularly major architectural changes. It's basically a version where "Compose starts to fill in UI Framework capabilities," such as: graphics pipeline, rich text editing, programmatic text selection, gesture-driven animation, Grid, Credential Manager, startup performance, and more.

Mesh Gradient and Complete WCG/HDR Pipeline

Previously, the built-in Gradients in Compose were mainly common types like Linear, Radial, and Sweep, which follow "a single axis or a center point." The newly added Mesh Gradient can now define a two-dimensional grid, specifying positions and colors at different vertices, and the GPU interpolates between these control points. This allows for the kind of multi-color, organic, fluid-sense backgrounds that are very common in modern design drafts.

The corresponding API roughly looks like this:

val gradientPainter = remember {
    MeshGradientPainter(1, 1) {
        setVertex(0, 0, Offset(0f, 0f), Color.Red)
        setVertex(0, 1, Offset(1f, 0f), Color.Blue)
        setVertex(1, 0, Offset(0f, 1f), Color.Green)
        setVertex(1, 1, Offset(1f, 1f), Color.Yellow)
    }
}

Box(
    Modifier.paint(gradientPainter)
)

An interesting point here is that during its alpha phase, it was initially called Modifier.meshGradient(). When it finally stabilized, Google changed the API to MeshGradientPainter + Modifier.paint().

This part of the API uses hardware-accelerated Canvas.drawMesh under the hood, meaning Google ultimately positioned Mesh Gradient as a Painter/drawing capability, rather than treating it as a Modifier with its own special rendering mechanism.

In fact, this is also more reasonable for animating Mesh Gradients in the future: you can modify the mesh vertices and colors, and then they enter the Compose drawing system normally.

More importantly is Wide Color Gamut + HDR. Previously, although Compose's Color itself could support ColorSpaces like Display P3 and Adobe RGB, the path into Paint, Shader, and the Android Canvas would cause parts of the pipeline to be converted/clamped back to sRGB. Now, 1.12 has truly completed this chain:

Compose Color → Paint / Shader → Canvas → Android platform rendering

That is, on API 29+, non-sRGB colors like Display P3 and Adobe RGB can be preserved and will no longer be clamped in Compose's intermediate stages. Only in cases where the platform doesn't support the ColorSpace or the system version is too low will it safely fall back to sRGB.

BasicTextField

Although Compose could previously display AnnotatedString and style TextField display results via OutputTransformation, that approach was essentially like the data was still plain text, just dressing certain characters in different clothes during the rendering output.

For example, a phone number 12345678900 in its state is still this string of characters, but on screen it can be displayed as 123 4567 8900, and some colors can be added during display. Compose 1.12 adds another layer of capability:

state.edit {
    addStyle(
        SpanStyle(
            fontWeight = FontWeight.Bold,
            color = Color.Blue
        ),
        start = 0,
        end = 9
    )
}

That is, TextFieldBuffer itself can now addStyle(), getSpanStyles(), getParagraphStyles(). At the same time, TextFieldState also has textStyles. Styles can exist alongside the editing state, be queried, modified, deleted, and can be saved across configuration changes.

This means Compose's own TextField architecture is closer to a WYSIWYG editor for things like a Markdown Editor or rich text notes. For instance, previously when writing these, you'd often get stuck in:

Raw String
      ↓
AnnotatedString
      ↓
selection offset mapping
      ↓
IME offset
      ↓
Transformation offset
      ↓
Undo/Redo

Then, with a slight text modification, all span ranges had to be maintained manually. Now, the Style itself enters the TextField Editing Model, which is essentially an enhancement at the state model level, greatly improving overall usability:

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

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

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

SelectionState

Starting from 1.12, SelectionState supports turning "selected text" into a controllable State, for example:

val selectionState = rememberSelectionState()

SelectionContainer(
    state = selectionState
) {
    Text(...)
}

Afterwards, the program can directly operate through these APIs:

selectionState.selectAll()
selectionState.clear()
selectionState.select(...)
selectionState.extendSelectionByWord()

It can also observe:

selectionState.selectedTexts

And even obtain all selectable text within a SelectionContainer via getSelectableTexts(), then select content across multiple Composables using a global range.

That is, previously SelectionContainer was basically: "I allow the user to long-press, and then Compose helps me display selection handles."

Now, selection is becoming operable data. For example, an AI Chat:

Answer Paragraph One
Answer Paragraph Two
Code Block
Answer Paragraph Three

The program can directly implement:

"Select All Answers", "Select a specific paragraph", "Extend one word from the current position", "Get all currently selected AnnotatedStrings".

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

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

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

Additionally, 1.12 supports automatic scrolling when dragging a selection beyond the viewport. These kinds of details are very practical for long-text readers and editors.

Deferred Animation

This feels like the most interesting update this time. DeferredTargetAnimation has officially left Experimental, and simultaneously adds:

DeferredAnimatedContent
DeferredAnimatedVisibility

The core scenario is Predictive Back. Suppose the user swipes from the left edge of the screen to the right. The first half of the animation can be controlled by the finger:

Finger 20%
Page scale / offset 20%

Finger 50%
Page scale / offset 50%

But when the user lifts their finger, the animation needs to:

Current gesture position + current velocity
            ↓
Compose Transition takes over
            ↓
Continue animation to the final state

So, Compose 1.12's Deferred Transition is specifically designed to connect these two phases:

Gesture controlled phase
          ↓
     defer()
          ↓
Manually modify scale / offset
          ↓
User lifts finger
          ↓
Automatic Transition takes over
          ↓
Velocity continues to be passed
          ↓
Final Target State

Here, Google even specifically emphasized velocity transfer: When the automatic animation takes over, it inherits the velocity of the previous user gesture. Otherwise, a jarring UI feeling easily occurs:

User flicks quickly
      ↓
Lifts finger
      ↓
Animation suddenly slows down as if restarting

That is, the animation can now continue the motion trend from the moment the user lifts their finger. Then, permitTransformDuringDeferredTransition can further handle Shared Elements, allowing a shared element to transform along with its parent during the deferred phase. So, this entire set is actually establishing a unified animation infrastructure, for things like:

Predictive Back, drag-to-dismiss, interactive navigation, gesture-driven transition

Grid Named Areas

Similar to bringing the CSS Grid concept into Compose, 1.12's Experimental Grid adds Named Areas, for example:

@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"))
    }
}

There are even more advanced tricks, but that's not part of this content:

Previously, for this kind of Dashboard, you generally had to write row = 1, column = 0, span = 2 for each element.

+----------------------+
| Header               |
+--------+-------------+
| Sidebar| Content     |
|        |             |
+--------+-------------+

Now you can first declare:

area("header", ...)
area("sidebar", ...)
area("content", ...)

Components only need Modifier.gridItem(areaId = "header"), achieving a separation of Layout Structure and Component. For example, later for Compact:

header
content
sidebar

Expanded:

header header
sidebar content

You only need to change the Grid Configuration. The components inside still only know:

I am sidebar
I am content

This is clearly serving the adaptive layout push for Android's current heavy focus on Phone + Foldable + Tablet + Desktop Window + XR.

Performance

First is a newly supported optimization. Previously, if you wanted to execute a side effect when userId or screenName changed, you generally needed to write:

LaunchedEffect(userId, screenName) { ... }

But if there was no coroutine work inside at all, this actually performed relatively heavy coroutine-related operations for a very light side effect. Then, 1.12 adds keys to SideEffect:

SideEffect(userId, screenName) {
    analytics.log(...)
}

The microbenchmark given by Google is:

API New Keyed SideEffect
Compared to LaunchedEffect Up to about 90% faster
Compared to DisposableEffect 20% faster

That is, in specific Effect scheduling scenarios, the new API can achieve better performance, and the execution order is also different:

SideEffect executes earlier than DisposableEffect and LaunchedEffect, so you can't just mechanically replace them upon seeing the 90% figure. Google also specifically reminded about this point.

Additionally, there's Startup Performance. Google mentioned:

Compose 1.12's TTID has already become comparable to Views in their benchmarks.

That is, the time from cold start to the first frame appearing has finally basically caught up with traditional Views. In the Pokedex Hero Benchmark published by Google in May this year, under Compose 1.11, TTID was 2.5% slower than Views, and TTFD was 13.0% slower than Views, so this improvement is quite significant.

Furthermore, scrolling performance has already reached the same 0.21% jank rate since Compose 1.9.

Others

Compose TextField can now connect to Android Credential Manager via the new credentialRequest semantics. On API 34+, it integrates directly through the Autofill Framework, while lower versions continue to be handled through androidx.credentials. So, Passkeys, saved account passwords, and Sign-in credentials can more naturally enter the Compose TextField input flow.

KeyboardType has also added:

Date
Time
DateTime
SignedDecimal

Additionally, downloadable fonts now start to support variable font variation settings.

Then, Compose components have also added automatic click / focus navigation sounds, while providing SoundEffectOnInteraction to turn them off.

Graphics also has a very practical new API: LayerOutsets, which can expand the visual bounds of a GraphicsLayer. This solves the problem where content is easily clipped by the actual measured bounds after a layer is promoted to an offscreen buffer. This will be more useful for blur, shadow, and complex graphics layers.

Also, there's a catch. Compose 1.12's compileSdk has been updated to API 37 / Android 17, explicitly requiring a minimum AGP of 9.2.0. That means you can't escape the high version AGP. If you want to upgrade:

So, are you ready? Take the Compose 1.12 upgrade with AGP 9.2.