Compose 修饰符 - 样式 Style API(概念篇) 一、简介Modifier承载的设置太多臃肿的大量链式调用影响阅读不同类型的调用顺序还关系到重组性能把视觉效果类的设置给抽出来成了 Style API。简化了基于状态的样式设置以声明式的方式更简洁地来定义基于不同状态如悬停hover、聚焦focus、按压pressed而变化的样式大幅减少模板代码。改进了动画状态转换状态切换时的样式过渡动画开箱即用且避免了 animateColorAsState 带来的重组问题。简化了组件 API用单一的 Style 参数替代大量样式参数得到了大幅简化并提供了更高的灵活性。性能提升Style 在 Compose 的绘制和布局阶段运行跳过组合阶段减少重组次数从而提升修饰符的性能。更标准化的API提供一套统一的样式属性让任何组件都可接入 Style。Style用于定义界面元素的外观类似 CSS 样式可以在本地或通过主题进行自定义。相同的样式属性会覆盖前者如果将 background 属性设置两次则只会显示后者。StyleScopeStyle 中 applyStyle() 函数的接收器范围。它提供了一些函数来定义视觉属性contentPadding、background、border等并访问当前的 StyleState。StyleState提供可在样式中使用的状态例如 isEnabled、isPressed、isChecked、自定义状态以根据不同的条件应用样式。1.1 对比ModifierStyle主要目标定义行为、语义和复杂布局。修饰符可动态操控特定可组合函数中的各个元素不会从主题向下传递。定义视觉外观、各个商品的大小和可设置主题的属性。样式在主题级别运行可在组件级别覆盖。它们会向下传递并跨不同的可组合项应用样式。逻辑相加 - 修饰符组合在一起形成新的结果。可覆盖 - Style中设置的最后一个属性生效。样式充当单个属性层这些属性会根据定义的优先级层次结构相互替换。主题难以提升为主题通常单独使用。根据设计Style可设置主题可访问 CompositionLocal并且只需定义一次即可在多个组件中使用。性能更新通常需要 Compose 的所有三个阶段组合、布局和绘制。如需使修饰符实现良好的动画效果通常需要编写基于 lambda 的版本。跳过组合阶段仅在布局和绘制阶段处于活动状态从而减少重组。需要更少的对象分配。动画需要使用单独的动画原语例如 animate*AsState。内置 animate { } API可为您处理一些动画。1.1.1 Modifier 的限制在当前的 Compose 环境中Modifier 具有许多优势不过 Style 解决了 Modifier 的一些限制。修饰符通常在组合阶段创建。更新可能会强制完全重新运行 Composition、Layout 和 Draw即使是颜色等细微的视觉变化也是如此除非您创建基于 lambda 的修饰符。条件修饰符需要在流畅链中实现破坏性的 if-else 逻辑。 为它们添加动画效果需要手动设置状态样板并且缺少高性能的“自动动画”机制。修饰符会叠加而不是替换。您无法替换组件的默认边框只能在上面绘制第二个边框。修饰符很难抽象为全局主题。因此主题通常存储原始值而不是可重复使用的修饰符配置。1.1.2 Style 的限制虽然 Style 可以弥补 Modifier 的一些不足但它们也存在一些限制这表明它无法完全取代 Modifier。Style 是专门的 Modifier。虽然 Modifier 可以执行 Style 能执行的任何操作但反之却不行。因此 Style 可以补充 Modifier 但不能取代 Modifier。Style 仅限于视觉配置背景、内边距、边框 它们无法处理点击逻辑、手势检测或无障碍语义等行为。将 Style 解析为最终状态比应用单个 Modifier更耗费资源。系统必须生成一个包含所有可能属性值的数据结构而继承属性的查找会进一步增加复杂性。1.2 何时使用虽然是否使用 Style 很大程度上取决于您的应用和使用场景但以下指南有助于确定何时优先选择 Style 而不是 Modifier。为了实现主题范围内的一致性Style 旨在“提升”到全局主题中。您可以在主题中定义单个Style以便在整个应用中创建统一的外观而无需向每个组件传递重复的 Modifier。在执行频繁动画时Style 会在布局和绘制阶段进行评估从而允许颜色或缩放等属性在完全绕过组合阶段的情况下实现动画效果。这可显著减少性能开销。在执行视觉属性动画时请使用 Style 而不是Modifier。替换与堆叠如果您需要替换默认属性请使用Style。Modifier 是累加的添加一个边框会堆叠第二个边框而 Style 使用“后写胜出”逻辑因此可以更轻松地替换背景或内边距而不会造成视觉混乱。自定义 Material 组件如果某个 Material 组件提供 Style 参数建议使用此方法进行自定义。借助这些Style您可以访问和修改可组合函数内部结构中可能无法访问的特定属性。1.3 性能优势Style 在 Compose 的布局和绘制阶段运行。这样一来就不需要创建基于 Lambda 的修饰符因为样式始终会跳过组合阶段。阶段转换Style 通常以绘制阶段为目标。当值发生更改时 Compose 只会使受影响的阶段失效例如绘制而不会触发完整的重组或重新布局。延迟分配Style 会延迟动画资源分配直到动画实际开始。这样可以减少初始撰写期间所需的工作量。减少了对象开销链式调用 Modifier 会为每个属性例如padding、border分配一个对象。Style 使用单个 Lambda 来应用多个属性从而显著减少内存分配。如果某个 Style 是在主题中定义的则该 Lambda 会在所有使用相应主题的组件之间共享。下表显示了 Compose 1.11.0-alpha06 的 Style 内部性能基准测试的示例结果并与不使用 Style 的 Compose 实现进行了比较。basic_box_border_change 测试突显了 Style 系统在避免属性更新期间分配多个 Modifier 对象方面的优势从而使分配量大幅减少了约 77%时间减少了约 59%。测试方法说明时间变更分配变更basic_box_border_change切换 Box 的边框颜色以衡量更新性能。-59.91%-77.22%input_state_basic_box比较基于样式的悬停/焦点/按下状态与手动互动状态收集。-5.24%-14.72%basic_box测量具有 5 个链式修饰符的 Box 的初始组合和布局。-4.78%-6.60%basic_text使用硬编码的字符串渲染五个 BasicText 组件。0.62%2.41%basic_text_provided_color比较了通过样式设置文字颜色与使用 CompositionLocalProvider 设置文字颜色。5.86%9.82%1.4 样式覆盖styleable{ } 代码块中的属性重复定义会被覆盖会采用代码块中的最后一次指定值。若是多次链式调用 Modifier.styleable() 非继承属性是累加性的其行为类似于 Modifier 多次定义相同属性的情况。对于继承属性这些属性会被覆盖链中最后一个 styleable 修饰符将设置其值。1.5 样式继承排版和颜色相关的属性会传播到子组件为子组件设置的样式会覆盖掉继承的父样式。覆盖优先级方式效果最低父级样式对于可继承的属性排版/颜色从父级传递下来。↓修饰符链Modifier.styleable{ contentColor(Color.Red)↓样式参数本地样式覆盖 Text(style Style { contentColor(Color.Red)}最高组件上的直接实参替换所有内容例如Text(color Color.Red)val styleState remember { MutableStyleState(null) } Column( modifier Modifier.styleable(styleState) { background(Color.LightGray) val blue Color(0xFF4285F4) val purple Color(0xFFA250EA) val colors listOf(blue, purple) contentBrush(Brush.linearGradient(colors)) }, ) { // 这里进行了覆盖 BaseText( text Children can , style { contentBrush(Brush.linearGradient(listOf(Color.Red, Color.Blue))) } ) // 这里会进行继承 BaseText(certain properties) BaseText(from their parents) }二、简单使用.styleable()fun Modifier.styleable(styleState: StyleState? null, style: Style): Modifierfun Modifier.styleable(styleState: StyleState?, vararg styles: Style): Modifier2.1 通过参数传递给已支持的组件BaseButton( onClick { }, style { background(Color.Blue) } ) { Text(Click me) }2.2 通过 Modifier.styleable() 应用样式Box( modifier Modifier.styleable { background(Color.Blue) } ) {...}2.3 提取 Style 重复使用val style Style { background(Color.Blue) } // 通过参数传递使用 BaseButton( onClick { }, style style ) { BaseText(Button) } // 通过 Modifier 应用 Box( modifier Modifier.styleable(null, style) ) {...}2.4 自定义 Style 属性对 StyleScope 使用扩展属性来创建自定义效果。暂时只支持整合 Style 已有的属性 20260511。// 定义 fun StyleScope.outlinedBackground(color: Color) { border(1.dp, color) background(color) } // 使用 val customExtensionStyle Style { outlinedBackground(Color.Blue) }2.5 覆盖方式Composable fun Demo( modifier: Modifier Modifier, style: Style Style ) { // 外部覆盖内部 Box(modifier modifier.styleable(styleState, {...} then style)) // 内部覆盖外部 Box(modifier modifier.styleable(styleState, style then {...})) }三、最佳实践3.1 正确做法3.1.1 使用Style实现视觉效果使用Modifier实现行为使用 Styles API 进行视觉配置背景、内边距、边框并为点击逻辑、手势检测或无障碍功能等行为保留修饰符。3.1.2 自定义组件公开 Style 参数供外部使用Composable fun Demo( modifier: Modifier Modifier, style: Style Style ) {...}3.1.3 将组件的视觉效果参数替换为 Style// 之前 Composable fun OldButton( background: Color, fontColor: Color ) {} // 现在 Composable fun NewButton(style: Style Style) {}3.1.4 优先使用 Style 实现动画使用内置 animate 块进行基于状态的 Style 设置并使用动画来提高性能而不是使用Modifier。3.1.5 利用“后写优先”原则利用 style 属性会覆盖而非堆叠这一事实。使用此属性可替换默认组件边框或背景而无需使用多个参数。3.1.6 为子系统值更改创建一个 Style如果要在深色模式和浅色模式之间切换请查询现有主题值通过 CompositionLocal以动态更改 Style。// 使用 CompositionLocals 或主题化的值来创建单一的样式。 val buttonStyle Style { background(colors.brandSecondary) shape(shapes.small) }3.1.7 当组件在主题定义之间存在根本差异时替换整个Style如果 Style 对象在主题级别上存在根本差异可以替换整个 Style 对象。例如如果要创建一个应用该应用针对每个产品/页面或产品/服务提供不同的主题并且 Style 的许多属性都不同那么在主题级别上替换整套 Style 是可以接受的。// 推荐做法当多个属性不同时整体切换不同的样式 - 例如产品 A 和产品 B 是两个提供不同主题的白标应用。 val productBThemedButton Style { shape(shapes.small) background(colors.brandSecondary) // 其他属性本质上是不同的。 } val productAThemedButton Style { shape(shapes.large) background(colors.brand) // 其他属性本质上是不同的。 }3.2 错误做法3.2.1 使用 Style 实现互动逻辑请勿尝试在 Style 中处理 onClick() 或手势检测。Style 仅限于基于状态的视觉配置因此不应处理业务逻辑相反它们只应根据状态具有不同的视觉效果。3.2.2 将自定义 Style 用作默认参数提供应始终使用 style: Style Style 进行声明。如需添加自定义的默认效果将传入的 Style 与自定义的 Style 合并。Composable fun BadButton( modifier: Modifier Modifier, // 不要将自定义 Style 用作默认参数 style: Style Style { background(Color.Red) } ) {}Composable fun GoodButton( modifier: Modifier Modifier, style: Style Style ) { val defaultStyle Style { background(Color.Red) } Box( // 应该在内部将默认值和传入的进行合并 modifier modifier.styleable(styleState, defaultStyle, style) ) {} }3.2.3 向基于布局的可组合项提供 Style 参数虽然可以向任何组件提供Style但基于布局的组件或屏幕级组件不应接受Style因为从消费者的角度来看不清楚 Style 在此级别上的作用。 Style 是为控件设计的不一定是为布局设计的。3.2.4 在组件内创建 Style对 CompositionLocals 不要在定义 Style 时读取而是在使用 Style 时读取。否则当实际使用 Style 时 CompositionLocal 的状态可能已更改从而导致 Style 未被更新。// 不要像这样在组件内部通过局部变量来创建 Style // 当值变化后Style不会得到更新 Composable fun containerStyle(): Style { val background MaterialTheme.colorScheme.background val onBackground MaterialTheme.colorScheme.onBackground return Style { background(background) contentColor(onBackground) } }val StyleScope.colors: JetsnackColors get() JetsnackTheme.LocalJetsnackTheme.currentValue.colors val StyleScope.typography: androidx.compose.material3.Typography get() JetsnackTheme.LocalJetsnackTheme.currentValue.typography val StyleScope.shapes: Shapes get() JetsnackTheme.LocalJetsnackTheme.currentValue.shapes val button Style { background(colors.brandSecondary) shape(shapes.small) }