破坏性更新 - 解读 Jetpack Compose 1.12

51 阅读10分钟

compose-header.png

Jetpack Compose 2026 年 8 月版本现已稳定发布。

这次核心 Compose 模块都来到了 1.12:

androidx.compose.runtime:runtime:1.12.0

这次新增了不少能力:Mesh Gradient、广色域(WCG)支持、Grid 的命名区域、与 Android Credential Manager 的集成,以及测试和性能方面的改进。

如果要升级到这个版本,只需要把项目里的 compose-bom 更新为 2026.08.00

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

破坏性变更

AGP 与 Compile SDK: Compose 1.12 将 compileSdk 提升到了 API 37,最低需要 AGP 9.1.1。Compose 会始终以最新的 compileSdk 为目标版本构建。

虽然 AI Coding 的时代这些可能没有那么重要了,但是我依然要吐槽一下 Android 这个升级——一点兼容性没有,像这次提升到 API 37,基本上等同于 AGP,Kotlin,Android Studio 都要一并更新,牵一发而动全身。有时候我甚至觉得只有搞 Android 的老手才能搞明白这些。

Modifier.onFirstVisible() 已被弃用。需要迁移到 Modifier.onVisibilityChanged(),后者可以更精确地跟踪可见性阈值。

并且,如果你使用到了 Styles API,那么这次的破坏性还要大一点。什么意思?

如果你以前用 Compose 打包了一个 Lib A,并在项目 B 中使用;现在想尝鲜,给 B 升级最新的 Compose,编译虽然可能没问题,但如果 Lib A 实际调用了这次变更的实验性 API,运行时仍可能崩溃。所以要注意了!

我目前发现了,Styles API 中 size 这个函数,就发生了变化。

主要原因是实验性的 Styles API 不承诺二进制兼容性;遇到这类变更时,应一并重新编译并升级相关库。

好,话不多说,马上进入更新解读。

带 key 的 SideEffect 重载

SideEffect 现在支持传入 key。这样一来,特定 key 变化时,可以执行一次性的副作用。如果既不需要协程,也不需要 dispose 回调,它会比 LaunchedEffectDisposableEffect 更合适。

SideEffect 的性能最高可比 LaunchedEffect 快 90%,比 DisposableEffect 快约 20%。

不过,它的执行时机早于 DisposableEffectLaunchedEffect。如果要把现有副作用迁移到这个 API,需要注意这一点,特别是那些依赖于“当前帧完成后才被调度启动”的 LaunchedEffect

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

作为开发者,我没想到这个功能来的这么迟。

以前如果我们想针对一个 Composable 编写仅在首次进入组合时生效的副作用,只能写 LaunchedEffect(Unit)。不过这个 Effect 需要启动协程,因此会有额外的调度和管理开销。

Mesh Gradients

Compose 1.12 引入了 MeshGradientPainter,用于创建由多个控制点组成、更自然的颜色渐变。

mesh-gradient.gif

val rows = 1
val columns = 1

val gradientPainter = remember {
    MeshGradientPainter(rows, columns) {
        // 参数:行、列、位置、颜色
        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 = modifier
        .aspectRatio(16 / 9f)
        .fillMaxWidth()
        .paint(gradientPainter)
)

好酷炫的更新!!!

广色域与 HDR 支持

现在的显示设备可以提供更丰富的色彩和更高的动态范围。Compose 1.12 在 graphics、paint 和 shader 的完整链路中启用了 广色域(P3)HDR 渲染 支持。

非 sRGB 色彩空间定义的颜色,例如 Display P3,现在可以一直保留到平台渲染阶段,不会中途被限制到 sRGB 色域中。如果使用了不支持的色彩空间,例如 CieXyz、CieLab、Oklab;依赖当前 Android 版本不支持的色彩空间,例如 Android 13 及以下的 Bt2020Hlg;或者应用运行在 Android 9(API 28)及以下,颜色会安全回退到 sRGB。

其他值得注意的改动:

  • GraphicsLayerModifier.graphicsLayer 新增了 LayerOutsets。它可以让图层的视觉边界超出自身测量尺寸。当图层被提升为离屏缓冲区时,可以通过 LayerOutsets 避免隐式的 clipToBounds 行为。

Styles

在 Google I/O 上,Google 已经分享过 Compose Styles API 的早期设想:用一套统一且高性能的方式为组件定义样式。

从那之后,Google 一直持续在完善底层架构,希望它具备严格的类型安全与可预测的正确性,同时也能支持自定义设计系统。

这层基础能力还需要继续打磨,因此 API 仍会保持实验性状态,后续也可能出现破坏性变更。

反正现在已经用 AI 编码了,实际上各位可以体验一下。我目前感受最大的变化就是:如果你写了一个复杂的控件,那么使用这个 Styles API 比传递多个 Modifier 好用很多很多!

Modifier 有个特点,就是顺序很重要,这导致了在自定义样式的时候,如果你的控件已经自己用了一部分 Modifier 参数了,那么传入的 Modifier 你很难放在那个位置。同时,如果你想支持多个位置的样式自定义,你就需要传入多个 Modifier,这就会让你的控件看起来很另类——Compose 中的控件多数情况下只有一个 Modifier,而且是第一个参数。

当然,这个 Styles API 的功能实际上和 Modifier 很多功能都是重复的,甚至针对 BasicText 这种控件,样式控制可以通过 ModifierStylesTextStyle 控制,StylesTextStyle 都可以用来控制内部文本的样式。希望后续 Google 能够收敛,条条大路通罗马对于开发者并不是一件好事儿。

关于 Styles API 的讲解,大家可以移步这篇文章

可交互的两阶段过渡

首先,DeferredTargetAnimation 已不再需要 experimental opt-in。

新的 DeferredAnimatedContentDeferredAnimatedVisibility 可以创建两阶段的过渡动画,例如跟随预测返回手势。

手动控制动画: 在过渡的 deferred 阶段,缩放、偏移等动画属性可以实时手动控制,例如直接跟随滑动手势。

平滑接管: deferred 阶段结束后,过渡引擎会接手后续动画,并平滑衔接,其中也包括速度的传递。

共享元素支持: SharedContentConfig 新增 permitTransformDuringDeferredTransition 标志,用于控制共享元素在 deferred 过渡阶段是否随父容器一起进行视觉变换。

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

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

transition.DeferredAnimatedContent(
    targetState = targetScreen,
    mutableTransformSpec = {
       MutableContentTransform {
           // 在 deferred 阶段手动控制属性
           initialContentTransform { scale = swipeProgress }
       }
    }
) { screen ->
    ScreenContent(screen)
}

下面两个示例展示了这样的场景:手势驱动的动画结束后,再把控制权交给触发式动画。

deferred-transition.gif

实际上,这是一个为预测性返回量身定做的动画。

可编辑文本格式

新的 API 为 BasicTextField 的可编辑文本加入了富文本格式能力。现在可以在 TextFieldBuffer 的作用域中,以编程方式应用和修改行内字符格式、段落格式,使用的是 SpanStyleParagraphStyle 和新增的 addStyle() 方法。这个作用域可以是 textFieldState.edit { ... },也可以是 InputTransformation

另外,TextFieldBuffer 提供了 getSpanStyles()getParagraphStyles(),返回的是 TrackedRange 对象,可以读取、更新或移除已经应用的样式。TextFieldState 新增只读属性 textStyles,用于查询不同范围内当前生效的样式;TextFieldBuffer 则提供 originalTextStyles,用于查看编辑前的格式状态。文本格式和自定义 annotation 会在 configuration change 后保留下来。

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

// 对一段文本应用粗体和颜色
state.edit {
    addStyle(
        SpanStyle(fontWeight = FontWeight.Bold, color = Color.Blue),
        start = 0,
        end = 9
    )
}

// 从 TextFieldState 查询当前生效的样式
val currentStyles = state.textStyles

文本选择

新的 SelectionState API 可以通过代码控制和观察 SelectionContainer 中的文本选择状态。通过 rememberSelectionState() 创建并提升 SelectionState,再传给 SelectionContainer,就可以拿到响应式的 selectedTexts,它是由 AnnotatedString 组成的列表;同时还可以调用 selectAll()clear()select(TextRange)extendSelectionByWord() 等方法。

另外,可以通过 getSelectableTexts() 按布局顺序获取所有可选择的文本项,并通过全局范围跨多个 SelectionContainer 内的 Composable 选择文本。

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

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

        SelectionContainer(state = selectionState) {
            Text("需要通过代码选中的文本内容。")
        }
    }
}

Credential Manager 集成

Compose 文本输入框现在可以通过 Autofill 框架原生集成 Android 的 Credential Manager(API 34 及以上)。API 34 以下则由 androidx.credential 库处理。给输入框附加新的 credentialRequest 语义属性,并传入 CredentialRequestData 后,就可以在用户输入过程中直接弹出 Passkey、已保存凭据或登录请求。

@Composable
fun LoginField(textFieldState: TextFieldState) {
    val credentialData = remember {
        CredentialRequestData(
            // 指定 Credential Manager 的请求选项
        )
    }

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

当然,关于密码的 Autofill,可以看我的这篇文章

其他值得注意的改动:

  • 下载字体支持字体 variation settings;
  • SelectionContainer 中把选区拖出视口时,现在会自动滚动;
  • Compose 组件新增自动交互音效支持,包括点击和焦点导航音效;可以使用 SoundEffectOnInteraction 关闭。需要注意,这项改动也意味着 semantics 的 click listener 现在必须在主线程调用,少量测试用例可能会受影响;
  • KeyboardType 新增 DateTimeDateTimeSignedDecimal
  • BasicSecureTextField 默认使用 TextObfuscationMode.System,而 RevealLastTyped 则是始终显示最后输入字符的强制覆盖选项。

Grid 布局中的命名区域

实验性 Grid 组件现在支持命名区域,复杂的二维布局会更容易维护。

以前需要在每个 item 上维护数字形式的行、列下标;现在可以在 GridConfigurationScope 中定义有实际含义的区域,再通过区域名称放置 Composable。

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

之前写过一篇文章讲解 Grid,当时我将 Grid 称之为“响应式布局的最后一块拼图”,大家如果想温习一下,可以去看看。

性能

每个版本都会持续投入 Compose 性能,希望框架能够帮助开发者构建好看、同时也足够流畅的应用。

这次主要优化了启动性能。在基准测试中,Compose 的首次显示时间(Time to Initial Display,即应用产出第一帧所需的时间)现在已经可以和 View 相当。

time-to-initial-display.png

好评!!!我看看以后谁还要说 Compose 启动比 View 慢了

测试同步

Compose 1.12 新增了几个测试 API,用于缩短测试执行时间,并减少状态采样时的不稳定情况:

  • hasPendingWork:被动检查 UI 是否仍有待处理工作,不会推进时钟。它适合手动驱动动画循环;
  • runWithoutImplicitWait:手动推进时钟帧时,临时关闭隐式同步,例如动画测试。
@Test
fun testAnimationStateFast() {
    composeTestRule.mainClock.autoAdvance = false

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

        composeTestRule.runOnUiThread {
            composeTestRule.runWithoutImplicitWait {
                // 同一帧中查询多个节点时效果最明显。
                // 它会避免每次单独查询时重复发生的同步开销。
                val box1 = composeTestRule.onNodeWithTag("Box1").fetchSemanticsNode()
                val box2 = composeTestRule.onNodeWithTag("Box2").fetchSemanticsNode()

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

其他值得注意的改动:

  • captureToImage 现在可以把 popupdialog 与它的锚点一起截到同一张 bitmap 中;
  • 新增 onRootWithViewInteraction,可以将 Compose 语义树的搜索范围限制到指定 Android View。对于 RecyclerView 这类混合 UI,它能简化测试,也不再需要为了测试专门给线上代码加入唯一 tag;
  • @PreviewWrapper 注解现在可以用于自定义 @MultiPreview 类,多个组件就可以复用同一套 Preview 配置,例如自定义主题。

Happy Composing

Compose 1.12 让应用开发有了更多表达空间:Mesh Gradients、广色域支持、可下载的可变字体、Credential Manager 集成,以及更快的测试工具都在这次版本中出现。

来吧,同志们,拥抱 Compose 吧!