第5课完成了布局系统的学习。本课进入 UI 组件与设计系统的核心:Text、Button、Card、Icon、Image 等常用组件的使用,Material 3 主题体系(colorScheme、typography、shapes、motionScheme)的配置,动态主题与自定义设计 Token 的实现,以及组件状态(enabled、loading、error)的统一处理模式。本课内容基于 CMP 1.12.1 与 Material 3 1.5.0-alpha22。 文末附有15道配套习题与本课知识点总结。
目录
- [一、Material 3 组件体系概览](#一、Material 3 组件体系概览)
- 二、基础组件使用
- [2.1 Text:文本显示](#2.1 Text:文本显示)
- [2.2 Button:按钮](#2.2 Button:按钮)
- [2.3 Card:卡片](#2.3 Card:卡片)
- [2.4 Icon 与 Image](#2.4 Icon 与 Image)
- [三、Material 3 主题体系](#三、Material 3 主题体系)
- [3.1 MaterialTheme 的四个核心维度](#3.1 MaterialTheme 的四个核心维度)
- [3.2 ColorScheme:语义颜色体系](#3.2 ColorScheme:语义颜色体系)
- [3.3 Typography:排版体系](#3.3 Typography:排版体系)
- [3.4 Shapes:形状体系](#3.4 Shapes:形状体系)
- [3.5 MotionScheme:动效体系](#3.5 MotionScheme:动效体系)
- [四、动态主题与自定义设计 Token](#四、动态主题与自定义设计 Token)
- [4.1 动态颜色:Android 平台特有能力](#4.1 动态颜色:Android 平台特有能力)
- [4.2 从种子色生成完整色板](#4.2 从种子色生成完整色板)
- [4.3 自定义设计 Token:间距与布局参数](#4.3 自定义设计 Token:间距与布局参数)
- 五、组件状态表达
- [5.1 密封接口建模状态](#5.1 密封接口建模状态)
- [5.2 统一的 LoadingUi 组件](#5.2 统一的 LoadingUi 组件)
- [5.3 组件内置状态:enabled 与 disabled](#5.3 组件内置状态:enabled 与 disabled)
- [六、Material 3 Expressive 主题](#六、Material 3 Expressive 主题)
- [6.1 Expressive 主题的状态](#6.1 Expressive 主题的状态)
- [6.2 使用 Expressive 主题](#6.2 使用 Expressive 主题)
- [6.3 1.5.0-alpha22 中的组件更新](#6.3 1.5.0-alpha22 中的组件更新)
- 七、跨平台注意事项
- 配套习题(15题)
- 本课知识点总结
一、Material 3 组件体系概览
Material 3 是 Google Material Design 的最新演进版本。Compose Multiplatform 通过 org.jetbrains.compose.material3:material3 库提供完整的 Material 3 支持。在 CMP 1.12.1 中,Material 3 库的版本为 1.12.0-alpha03,基于 Jetpack Material3 1.5.0-alpha22。稳定版 androidx.compose.material3:material3 为 1.4.0,不包含 Expressive 特性。
组件的强调层级(Emphasis Level) 是理解 Material 3 组件选择的核心框架。Material 3 为同一功能提供了多种视觉变体,对应不同的强调程度。以按钮为例:
| 组件 | 描述 | 强调层级 |
|---|---|---|
Button(Filled Button) |
高强调的填充按钮 | 高 |
ElevatedButton |
带阴影的填充按钮 | 中 |
FilledTonalButton |
带色调背景的填充按钮 | 中 |
OutlinedButton |
带边框的中等强调按钮 | 中 |
TextButton |
无边框的低强调按钮 | 低 |
IconButton |
紧凑的图标按钮 | 低 |
这一分类体系同样适用于卡片(Card、ElevatedCard、OutlinedCard)和芯片(AssistChip、FilterChip、InputChip)等组件族。组件选择的核心原则是:根据操作在界面中的重要性选择对应的强调层级,而非依赖颜色或尺寸的临时调整。
下面是 Material 3 组件强调层级的选择流程:
#mermaid-svg-kqq6aXIjqPmP1ulp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kqq6aXIjqPmP1ulp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kqq6aXIjqPmP1ulp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kqq6aXIjqPmP1ulp .error-icon{fill:#552222;}#mermaid-svg-kqq6aXIjqPmP1ulp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kqq6aXIjqPmP1ulp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kqq6aXIjqPmP1ulp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kqq6aXIjqPmP1ulp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kqq6aXIjqPmP1ulp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kqq6aXIjqPmP1ulp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kqq6aXIjqPmP1ulp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kqq6aXIjqPmP1ulp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kqq6aXIjqPmP1ulp .marker.cross{stroke:#333333;}#mermaid-svg-kqq6aXIjqPmP1ulp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kqq6aXIjqPmP1ulp p{margin:0;}#mermaid-svg-kqq6aXIjqPmP1ulp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kqq6aXIjqPmP1ulp .cluster-label text{fill:#333;}#mermaid-svg-kqq6aXIjqPmP1ulp .cluster-label span{color:#333;}#mermaid-svg-kqq6aXIjqPmP1ulp .cluster-label span p{background-color:transparent;}#mermaid-svg-kqq6aXIjqPmP1ulp .label text,#mermaid-svg-kqq6aXIjqPmP1ulp span{fill:#333;color:#333;}#mermaid-svg-kqq6aXIjqPmP1ulp .node rect,#mermaid-svg-kqq6aXIjqPmP1ulp .node circle,#mermaid-svg-kqq6aXIjqPmP1ulp .node ellipse,#mermaid-svg-kqq6aXIjqPmP1ulp .node polygon,#mermaid-svg-kqq6aXIjqPmP1ulp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kqq6aXIjqPmP1ulp .rough-node .label text,#mermaid-svg-kqq6aXIjqPmP1ulp .node .label text,#mermaid-svg-kqq6aXIjqPmP1ulp .image-shape .label,#mermaid-svg-kqq6aXIjqPmP1ulp .icon-shape .label{text-anchor:middle;}#mermaid-svg-kqq6aXIjqPmP1ulp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kqq6aXIjqPmP1ulp .rough-node .label,#mermaid-svg-kqq6aXIjqPmP1ulp .node .label,#mermaid-svg-kqq6aXIjqPmP1ulp .image-shape .label,#mermaid-svg-kqq6aXIjqPmP1ulp .icon-shape .label{text-align:center;}#mermaid-svg-kqq6aXIjqPmP1ulp .node.clickable{cursor:pointer;}#mermaid-svg-kqq6aXIjqPmP1ulp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kqq6aXIjqPmP1ulp .arrowheadPath{fill:#333333;}#mermaid-svg-kqq6aXIjqPmP1ulp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kqq6aXIjqPmP1ulp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kqq6aXIjqPmP1ulp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kqq6aXIjqPmP1ulp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kqq6aXIjqPmP1ulp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kqq6aXIjqPmP1ulp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kqq6aXIjqPmP1ulp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kqq6aXIjqPmP1ulp .cluster text{fill:#333;}#mermaid-svg-kqq6aXIjqPmP1ulp .cluster span{color:#333;}#mermaid-svg-kqq6aXIjqPmP1ulp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kqq6aXIjqPmP1ulp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kqq6aXIjqPmP1ulp rect.text{fill:none;stroke-width:0;}#mermaid-svg-kqq6aXIjqPmP1ulp .icon-shape,#mermaid-svg-kqq6aXIjqPmP1ulp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kqq6aXIjqPmP1ulp .icon-shape p,#mermaid-svg-kqq6aXIjqPmP1ulp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kqq6aXIjqPmP1ulp .icon-shape .label rect,#mermaid-svg-kqq6aXIjqPmP1ulp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kqq6aXIjqPmP1ulp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kqq6aXIjqPmP1ulp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kqq6aXIjqPmP1ulp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 高
中
是
否
是
否
低
是
否
需要选择组件
操作的重要性?
Button(Filled Button)
是否需要边框?
OutlinedButton
是否需要阴影?
ElevatedButton
FilledTonalButton
是否需要图标?
IconButton
TextButton
二、基础组件使用
2.1 Text:文本显示
Text 是使用频率最高的组件,接受 text 字符串参数,并通过 style 参数引用 MaterialTheme.typography 中的文本样式:
kotlin
Text(
text = "标题文本",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
核心参数 :style 引用主题排版,color 使用 colorScheme 中的语义颜色,maxLines 与 overflow 控制文本截断行为。
2.2 Button:按钮
Material 3 提供六种按钮变体。基础用法如下:
kotlin
// 高强调操作
Button(onClick = { /* 处理点击 */ }) {
Text("确认提交")
}
// 中等强调操作
FilledTonalButton(onClick = { /* 处理点击 */ }) {
Text("保存草稿")
}
// 低强调操作
TextButton(onClick = { /* 处理点击 */ }) {
Text("取消")
}
// 图标按钮
IconButton(onClick = { /* 处理点击 */ }) {
Icon(Icons.Default.Share, contentDescription = "分享")
}
所有按钮变体共享 onClick、enabled、content 等核心参数。content 是 @Composable RowScope.() -> Unit 类型的 Slot,允许在按钮内放置图标和文本的组合。
2.3 Card:卡片
卡片用于将相关内容组织在一个视觉容器中。Material 3 提供三种卡片变体:
kotlin
// 填充卡片(默认)
Card(
modifier = Modifier.fillMaxWidth(),
onClick = { /* 可选:可点击卡片 */ }
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("卡片标题", style = MaterialTheme.typography.titleMedium)
Text("卡片内容描述", style = MaterialTheme.typography.bodyMedium)
}
}
// 带阴影的卡片
ElevatedCard(onClick = { /* ... */ }) { /* ... */ }
// 带边框的卡片
OutlinedCard(onClick = { /* ... */ }) { /* ... */ }
卡片通过 CardDefaults 暴露颜色和高度接口,可在不改动组件内部实现的情况下进行定制。
2.4 Icon 与 Image
Icon 用于显示矢量图标,Image 用于显示位图资源:
kotlin
// 矢量图标
Icon(
imageVector = Icons.Default.Home,
contentDescription = "首页",
tint = MaterialTheme.colorScheme.primary
)
// 位图图片
Image(
painter = painterResource("photo.png"),
contentDescription = "照片描述",
contentScale = ContentScale.Crop,
modifier = Modifier.size(120.dp).clip(MaterialTheme.shapes.medium)
)
Icon 的 tint 参数使其自动适配当前主题色,Image 的 contentScale 控制裁剪和缩放行为。contentDescription 是辅助功能的必要参数,纯装饰性图片可设为 null。
三、Material 3 主题体系
3.1 MaterialTheme 的四个核心维度
MaterialTheme 是 Material 3 主题的根可组合项,接受 colorScheme、typography、shapes、motionScheme 四个参数,为子树中的所有组件提供默认样式:
kotlin
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
MaterialTheme(
colorScheme = if (darkTheme) darkColorScheme() else lightColorScheme(),
typography = Typography(),
shapes = Shapes(),
motionScheme = MotionScheme.standard(),
content = content
)
}
这四个维度的值通过 CompositionLocalProvider 传播,分别对应 LocalColorScheme、LocalTypography、LocalShapes 和 LocalMotionScheme。
3.2 ColorScheme:语义颜色体系
Material 3 的 ColorScheme 采用语义角色而非具体色值来定义颜色。核心颜色角色包括:
| 角色 | 用途 |
|---|---|
primary / onPrimary |
主品牌色及其上的文字颜色 |
secondary / onSecondary |
辅助色 |
tertiary / onTertiary |
强调色 |
surface / onSurface |
卡片、面板等表面色 |
background / onBackground |
页面背景色 |
error / onError |
错误状态色 |
outline |
边框和分隔线 |
使用 lightColorScheme() 和 darkColorScheme() 创建浅色和深色方案,或通过 MaterialTheme.colorScheme.primary 访问当前方案中的颜色。语义角色的优势在于:组件不直接依赖具体色值,而是依赖颜色角色。当品牌色变更或切换到暗色模式时,所有组件自动适配。
3.3 Typography:排版体系
Material 3 定义了 15 种文本样式,按尺寸层级组织:
| 类别 | 样式示例 | 典型用途 |
|---|---|---|
| Display | displayLarge、displayMedium、displaySmall |
超大标题 |
| Headline | headlineLarge、headlineMedium、headlineSmall |
页面标题 |
| Title | titleLarge、titleMedium、titleSmall |
区块标题 |
| Body | bodyLarge、bodyMedium、bodySmall |
正文内容 |
| Label | labelLarge、labelMedium、labelSmall |
按钮、标签 |
自定义排版通过修改 Typography() 的对应样式实现。自定义字体文件存放在 composeResources/font 目录中,支持 TTF、OTF、TTC 和可变字体格式:
kotlin
val AppTypography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily(Font(Res.font.inter_regular)),
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
)
3.4 Shapes:形状体系
Shapes 定义了五种尺寸类别的圆角形状:
| 类别 | 默认圆角 | 典型用途 |
|---|---|---|
extraSmall |
4.dp | Chip、小型 IconButton |
small |
8.dp | SegmentedButton、SnackbarHost |
medium |
12.dp | Card、Dialog、Menu |
large |
16.dp | NavigationDrawer、BottomSheet |
extraLarge |
28.dp | FloatingActionButton |
通过 MaterialTheme.shapes.medium 引用形状,或自定义 Shapes 对象覆盖默认值。需要注意的是,ShapeKeyTokens.CornerFull 使用 RoundedCornerShape(50%)(完全圆角)而非引用 Shapes,因此按钮始终保持药丸形状,不受主题影响。
3.5 MotionScheme:动效体系
MotionScheme 是 Material 3 主题的第四个维度,用于定义动画的时序和缓动曲线。Material 3 提供两种内置动效方案:
MotionScheme.standard():基础动效方案,适用于实用型 UI 元素和重复性交互,提供线性运动感。MotionScheme.expressive():表达性动效方案,用于更个性化的体验,动效更活泼、更具弹性。
动效方案通过 MaterialTheme.motionScheme 访问,组件自动使用当前方案中定义的动画规格。
下面是 MaterialTheme 四个核心维度的传播机制:
#mermaid-svg-BcR5yQam8eMti1T5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BcR5yQam8eMti1T5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BcR5yQam8eMti1T5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BcR5yQam8eMti1T5 .error-icon{fill:#552222;}#mermaid-svg-BcR5yQam8eMti1T5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BcR5yQam8eMti1T5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BcR5yQam8eMti1T5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BcR5yQam8eMti1T5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BcR5yQam8eMti1T5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BcR5yQam8eMti1T5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BcR5yQam8eMti1T5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BcR5yQam8eMti1T5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BcR5yQam8eMti1T5 .marker.cross{stroke:#333333;}#mermaid-svg-BcR5yQam8eMti1T5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BcR5yQam8eMti1T5 p{margin:0;}#mermaid-svg-BcR5yQam8eMti1T5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BcR5yQam8eMti1T5 .cluster-label text{fill:#333;}#mermaid-svg-BcR5yQam8eMti1T5 .cluster-label span{color:#333;}#mermaid-svg-BcR5yQam8eMti1T5 .cluster-label span p{background-color:transparent;}#mermaid-svg-BcR5yQam8eMti1T5 .label text,#mermaid-svg-BcR5yQam8eMti1T5 span{fill:#333;color:#333;}#mermaid-svg-BcR5yQam8eMti1T5 .node rect,#mermaid-svg-BcR5yQam8eMti1T5 .node circle,#mermaid-svg-BcR5yQam8eMti1T5 .node ellipse,#mermaid-svg-BcR5yQam8eMti1T5 .node polygon,#mermaid-svg-BcR5yQam8eMti1T5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BcR5yQam8eMti1T5 .rough-node .label text,#mermaid-svg-BcR5yQam8eMti1T5 .node .label text,#mermaid-svg-BcR5yQam8eMti1T5 .image-shape .label,#mermaid-svg-BcR5yQam8eMti1T5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-BcR5yQam8eMti1T5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BcR5yQam8eMti1T5 .rough-node .label,#mermaid-svg-BcR5yQam8eMti1T5 .node .label,#mermaid-svg-BcR5yQam8eMti1T5 .image-shape .label,#mermaid-svg-BcR5yQam8eMti1T5 .icon-shape .label{text-align:center;}#mermaid-svg-BcR5yQam8eMti1T5 .node.clickable{cursor:pointer;}#mermaid-svg-BcR5yQam8eMti1T5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BcR5yQam8eMti1T5 .arrowheadPath{fill:#333333;}#mermaid-svg-BcR5yQam8eMti1T5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BcR5yQam8eMti1T5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BcR5yQam8eMti1T5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BcR5yQam8eMti1T5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BcR5yQam8eMti1T5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BcR5yQam8eMti1T5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BcR5yQam8eMti1T5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BcR5yQam8eMti1T5 .cluster text{fill:#333;}#mermaid-svg-BcR5yQam8eMti1T5 .cluster span{color:#333;}#mermaid-svg-BcR5yQam8eMti1T5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BcR5yQam8eMti1T5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BcR5yQam8eMti1T5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-BcR5yQam8eMti1T5 .icon-shape,#mermaid-svg-BcR5yQam8eMti1T5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BcR5yQam8eMti1T5 .icon-shape p,#mermaid-svg-BcR5yQam8eMti1T5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BcR5yQam8eMti1T5 .icon-shape .label rect,#mermaid-svg-BcR5yQam8eMti1T5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BcR5yQam8eMti1T5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BcR5yQam8eMti1T5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BcR5yQam8eMti1T5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MaterialTheme 根节点
colorScheme
typography
shapes
motionScheme
LocalColorScheme
LocalTypography
LocalShapes
LocalMotionScheme
子树中的所有组件
四、动态主题与自定义设计 Token
4.1 动态颜色:Android 平台特有能力
Android 12 及以上支持从用户壁纸提取颜色生成动态主题。Compose Multiplatform 中,动态颜色仅在 Android 平台可用 ,因此必须放在 androidMain 源集中:
kotlin
// androidMain 中的 expect 声明
@Composable
expect fun platformDynamicColorScheme(darkTheme: Boolean): ColorScheme?
// androidMain 中的 actual 实现
@Composable
actual fun platformDynamicColorScheme(darkTheme: Boolean): ColorScheme? {
val context = LocalContext.current
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (darkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
} else null
}
在 commonMain 中通过 expect/actual 机制调用,非 Android 平台返回 null 并回退到静态色板。
4.2 从种子色生成完整色板
对于非 Android 平台或需要品牌一致性控制的场景,可使用 MaterialKolor 等社区库从单个种子色生成完整的 Material 3 色板。MaterialKolor 5.0.0 支持 Compose Multiplatform 1.11.0,并通过 DynamicMaterialExpressiveTheme 提供 Material 3 Expressive 的动态主题能力:
kotlin
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun MyExpressiveTheme(
seedColor: Color,
isDark: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
DynamicMaterialExpressiveTheme(
seedColor = seedColor,
motionScheme = MotionScheme.expressive(),
isDark = isDark,
animate = true,
content = content
)
}
4.3 自定义设计 Token:间距与布局参数
Material 3 的 colorScheme、typography、shapes、motionScheme 覆盖了颜色、文字、形状、动效四个维度,但间距(Spacing) 不在 Material 3 主题的默认参数中。生产级项目需要通过 CompositionLocal 实现自定义间距 Token:
kotlin
// 定义间距 Token
data class Spacing(
val none: Dp = 0.dp,
val extraSmall: Dp = 4.dp,
val small: Dp = 8.dp,
val medium: Dp = 16.dp,
val large: Dp = 24.dp,
val extraLarge: Dp = 32.dp
)
val LocalSpacing = staticCompositionLocalOf { Spacing() }
@Composable
fun AppTheme(content: @Composable () -> Unit) {
CompositionLocalProvider(LocalSpacing provides Spacing()) {
MaterialTheme(content = content)
}
}
// 使用
val spacing = LocalSpacing.current
Column(modifier = Modifier.padding(spacing.medium)) { /* ... */ }
staticCompositionLocalOf 用于不频繁变化的 Token(如间距),读取时不追踪变化,性能更优;compositionLocalOf 用于需要追踪读取的频繁变化值。通过 CompositionLocalProvider 将 Token 注入组合树,实现设计系统与组件实现的解耦。
下面是动态主题与自定义设计 Token 的整体架构:
#mermaid-svg-801XrmzNuMdb9Oku{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-801XrmzNuMdb9Oku .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-801XrmzNuMdb9Oku .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-801XrmzNuMdb9Oku .error-icon{fill:#552222;}#mermaid-svg-801XrmzNuMdb9Oku .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-801XrmzNuMdb9Oku .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-801XrmzNuMdb9Oku .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-801XrmzNuMdb9Oku .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-801XrmzNuMdb9Oku .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-801XrmzNuMdb9Oku .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-801XrmzNuMdb9Oku .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-801XrmzNuMdb9Oku .marker{fill:#333333;stroke:#333333;}#mermaid-svg-801XrmzNuMdb9Oku .marker.cross{stroke:#333333;}#mermaid-svg-801XrmzNuMdb9Oku svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-801XrmzNuMdb9Oku p{margin:0;}#mermaid-svg-801XrmzNuMdb9Oku .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-801XrmzNuMdb9Oku .cluster-label text{fill:#333;}#mermaid-svg-801XrmzNuMdb9Oku .cluster-label span{color:#333;}#mermaid-svg-801XrmzNuMdb9Oku .cluster-label span p{background-color:transparent;}#mermaid-svg-801XrmzNuMdb9Oku .label text,#mermaid-svg-801XrmzNuMdb9Oku span{fill:#333;color:#333;}#mermaid-svg-801XrmzNuMdb9Oku .node rect,#mermaid-svg-801XrmzNuMdb9Oku .node circle,#mermaid-svg-801XrmzNuMdb9Oku .node ellipse,#mermaid-svg-801XrmzNuMdb9Oku .node polygon,#mermaid-svg-801XrmzNuMdb9Oku .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-801XrmzNuMdb9Oku .rough-node .label text,#mermaid-svg-801XrmzNuMdb9Oku .node .label text,#mermaid-svg-801XrmzNuMdb9Oku .image-shape .label,#mermaid-svg-801XrmzNuMdb9Oku .icon-shape .label{text-anchor:middle;}#mermaid-svg-801XrmzNuMdb9Oku .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-801XrmzNuMdb9Oku .rough-node .label,#mermaid-svg-801XrmzNuMdb9Oku .node .label,#mermaid-svg-801XrmzNuMdb9Oku .image-shape .label,#mermaid-svg-801XrmzNuMdb9Oku .icon-shape .label{text-align:center;}#mermaid-svg-801XrmzNuMdb9Oku .node.clickable{cursor:pointer;}#mermaid-svg-801XrmzNuMdb9Oku .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-801XrmzNuMdb9Oku .arrowheadPath{fill:#333333;}#mermaid-svg-801XrmzNuMdb9Oku .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-801XrmzNuMdb9Oku .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-801XrmzNuMdb9Oku .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-801XrmzNuMdb9Oku .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-801XrmzNuMdb9Oku .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-801XrmzNuMdb9Oku .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-801XrmzNuMdb9Oku .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-801XrmzNuMdb9Oku .cluster text{fill:#333;}#mermaid-svg-801XrmzNuMdb9Oku .cluster span{color:#333;}#mermaid-svg-801XrmzNuMdb9Oku div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-801XrmzNuMdb9Oku .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-801XrmzNuMdb9Oku rect.text{fill:none;stroke-width:0;}#mermaid-svg-801XrmzNuMdb9Oku .icon-shape,#mermaid-svg-801XrmzNuMdb9Oku .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-801XrmzNuMdb9Oku .icon-shape p,#mermaid-svg-801XrmzNuMdb9Oku .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-801XrmzNuMdb9Oku .icon-shape .label rect,#mermaid-svg-801XrmzNuMdb9Oku .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-801XrmzNuMdb9Oku .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-801XrmzNuMdb9Oku .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-801XrmzNuMdb9Oku :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 自定义设计 Token
非 Android 平台
Android 12+
从壁纸提取颜色
dynamicLightColorScheme
dynamicDarkColorScheme
种子色(seedColor)
MaterialKolor 生成色板
Spacing 数据类
staticCompositionLocalOf
CompositionLocalProvider 注入
ColorScheme
MaterialTheme
五、组件状态表达
5.1 密封接口建模状态
在跨平台应用中,数据加载通常涉及加载中、成功、错误、重试等多种状态。推荐使用 密封接口(sealed interface) 对状态建模,而非多个布尔标志:
kotlin
@Immutable
sealed interface UiState<out T> {
data object Loading : UiState<Nothing>
data class Success<T>(val data: T) : UiState<T>
data class Error(val message: String, val cause: Throwable? = null) : UiState<Nothing>
data class Retrying(val attempt: Int) : UiState<Nothing>
}
密封接口的优势:穷尽性检查 ------when 表达式必须覆盖所有子类型,编译器在添加新状态时强制开发者处理;类型安全 ------Success 携带数据,Error 携带错误信息,不会出现"加载中却有数据"的非法状态组合。
5.2 统一的 LoadingUi 组件
将状态渲染逻辑封装为可复用的泛型组件:
kotlin
@Composable
fun <T> LoadingUi(
state: UiState<T>,
loading: @Composable () -> Unit = { CircularProgressIndicator() },
error: @Composable (String) -> Unit = { message ->
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(message, color = MaterialTheme.colorScheme.error)
}
},
content: @Composable (T) -> Unit
) {
when (state) {
is UiState.Loading -> loading()
is UiState.Success -> content(state.data)
is UiState.Error -> error(state.message)
is UiState.Retrying -> {
LinearProgressIndicator()
Text("正在重试(第 ${state.attempt} 次)...")
}
}
}
5.3 组件内置状态:enabled 与 disabled
Material 3 组件内置了 enabled 参数,当设为 false 时组件自动应用禁用态的颜色和交互行为:
kotlin
Button(
onClick = { submit() },
enabled = formState.isValid && !formState.isSubmitting
) {
if (formState.isSubmitting) {
CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp)
Spacer(Modifier.width(8.dp))
}
Text(if (formState.isSubmitting) "提交中..." else "提交")
}
设计原则 :组件的 enabled 状态应基于业务逻辑(如表单校验是否通过、是否正在提交)自动计算,而非手动管理。加载中的按钮应同时显示进度指示器并禁用点击,防止重复提交。
下面是组件状态流转与 LoadingUi 的处理流程:
#mermaid-svg-Gj2ZUXcdgH3OnMrf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Gj2ZUXcdgH3OnMrf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .error-icon{fill:#552222;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .marker.cross{stroke:#333333;}#mermaid-svg-Gj2ZUXcdgH3OnMrf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Gj2ZUXcdgH3OnMrf p{margin:0;}#mermaid-svg-Gj2ZUXcdgH3OnMrf defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-Gj2ZUXcdgH3OnMrf g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-Gj2ZUXcdgH3OnMrf g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-Gj2ZUXcdgH3OnMrf g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-Gj2ZUXcdgH3OnMrf g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-Gj2ZUXcdgH3OnMrf g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Gj2ZUXcdgH3OnMrf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Gj2ZUXcdgH3OnMrf .edgeLabel .label text{fill:#333;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .label div .edgeLabel{color:#333;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Gj2ZUXcdgH3OnMrf #statediagram-barbEnd{fill:#333333;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .cluster-label,#mermaid-svg-Gj2ZUXcdgH3OnMrf .nodeLabel{color:#131300;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .note-edge{stroke-dasharray:5;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .statediagram-note text{fill:black;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .statediagram-note .nodeLabel{color:black;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .statediagram .edgeLabel{color:red;}#mermaid-svg-Gj2ZUXcdgH3OnMrf #dependencyStart,#mermaid-svg-Gj2ZUXcdgH3OnMrf #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-Gj2ZUXcdgH3OnMrf .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Gj2ZUXcdgH3OnMrf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 数据加载成功
加载失败
用户点击重试
重新发起请求
重试仍失败
Loading
Success
Error
Retrying
六、Material 3 Expressive 主题
6.1 Expressive 主题的状态
Compose Multiplatform 支持 Material 3 库中实验性的 MaterialExpressiveTheme。但需要注意:在 Material 3 1.4.0-beta01 中,所有标记为 ExperimentalMaterial3ExpressiveApi 和 ExperimentalMaterial3ComponentOverrideApi 的公共 API 已被移除。要使用 Expressive 主题,必须显式引入 Alpha 版本的 Material 3 依赖。
Material 3 Expressive 已在 MaterialKolor 5.0.0 中正式稳定,表明 Expressive 方向正在从实验性走向生产就绪。
6.2 使用 Expressive 主题
引入 Alpha 版本依赖:
kotlin
implementation("org.jetbrains.compose.material3:material3:1.9.0-alpha04")
配置 Expressive 主题:
kotlin
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AppTheme(content: @Composable () -> Unit) {
MaterialExpressiveTheme(
colorScheme = lightColorScheme(),
motionScheme = MotionScheme.expressive(),
shapes = Shapes(),
typography = Typography(),
content = content
)
}
MaterialExpressiveTheme 在标准主题的四个维度(colorScheme、typography、shapes、motionScheme)上提供完整的配置能力。注意事项:生产项目应谨慎评估是否依赖实验性 API,并关注 Material 3 稳定版对 Expressive 特性的整合进度。
6.3 1.5.0-alpha22 中的组件更新
Material 3 1.5.0-alpha22(2026年6月17日发布)带来了若干值得关注的组件更新:
- TopAppBarScrollBehavior 及相关 API 提升为稳定状态 ,不再需要
@ExperimentalMaterial3ExpressiveApi标注。 - Expressive FloatingToolbar API 转为非实验性。
TopAppBarDefaults新增公开的snapAnimationSpecgetter,暴露默认的吸附动画规格。pinnedScrollBehavior和enterAlwaysScrollBehavior更新为接受ScrollableState,自动处理反转布局和前置滚动内容等边界情况。ExpandedDockedSearchBarWithGap和ExpandedFullScreenContainedSearchBar转为非实验性。
这些更新表明 Material 3 Expressive 组件正在逐步稳定化,生产项目可以开始评估采用。
下面是 Material 3 Expressive 主题的演进路径:
#mermaid-svg-F4jXHDy20WCbdOzy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-F4jXHDy20WCbdOzy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-F4jXHDy20WCbdOzy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-F4jXHDy20WCbdOzy .error-icon{fill:#552222;}#mermaid-svg-F4jXHDy20WCbdOzy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-F4jXHDy20WCbdOzy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-F4jXHDy20WCbdOzy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-F4jXHDy20WCbdOzy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-F4jXHDy20WCbdOzy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-F4jXHDy20WCbdOzy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-F4jXHDy20WCbdOzy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-F4jXHDy20WCbdOzy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-F4jXHDy20WCbdOzy .marker.cross{stroke:#333333;}#mermaid-svg-F4jXHDy20WCbdOzy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-F4jXHDy20WCbdOzy p{margin:0;}#mermaid-svg-F4jXHDy20WCbdOzy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-F4jXHDy20WCbdOzy .cluster-label text{fill:#333;}#mermaid-svg-F4jXHDy20WCbdOzy .cluster-label span{color:#333;}#mermaid-svg-F4jXHDy20WCbdOzy .cluster-label span p{background-color:transparent;}#mermaid-svg-F4jXHDy20WCbdOzy .label text,#mermaid-svg-F4jXHDy20WCbdOzy span{fill:#333;color:#333;}#mermaid-svg-F4jXHDy20WCbdOzy .node rect,#mermaid-svg-F4jXHDy20WCbdOzy .node circle,#mermaid-svg-F4jXHDy20WCbdOzy .node ellipse,#mermaid-svg-F4jXHDy20WCbdOzy .node polygon,#mermaid-svg-F4jXHDy20WCbdOzy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-F4jXHDy20WCbdOzy .rough-node .label text,#mermaid-svg-F4jXHDy20WCbdOzy .node .label text,#mermaid-svg-F4jXHDy20WCbdOzy .image-shape .label,#mermaid-svg-F4jXHDy20WCbdOzy .icon-shape .label{text-anchor:middle;}#mermaid-svg-F4jXHDy20WCbdOzy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-F4jXHDy20WCbdOzy .rough-node .label,#mermaid-svg-F4jXHDy20WCbdOzy .node .label,#mermaid-svg-F4jXHDy20WCbdOzy .image-shape .label,#mermaid-svg-F4jXHDy20WCbdOzy .icon-shape .label{text-align:center;}#mermaid-svg-F4jXHDy20WCbdOzy .node.clickable{cursor:pointer;}#mermaid-svg-F4jXHDy20WCbdOzy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-F4jXHDy20WCbdOzy .arrowheadPath{fill:#333333;}#mermaid-svg-F4jXHDy20WCbdOzy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-F4jXHDy20WCbdOzy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-F4jXHDy20WCbdOzy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-F4jXHDy20WCbdOzy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-F4jXHDy20WCbdOzy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-F4jXHDy20WCbdOzy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-F4jXHDy20WCbdOzy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-F4jXHDy20WCbdOzy .cluster text{fill:#333;}#mermaid-svg-F4jXHDy20WCbdOzy .cluster span{color:#333;}#mermaid-svg-F4jXHDy20WCbdOzy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-F4jXHDy20WCbdOzy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-F4jXHDy20WCbdOzy rect.text{fill:none;stroke-width:0;}#mermaid-svg-F4jXHDy20WCbdOzy .icon-shape,#mermaid-svg-F4jXHDy20WCbdOzy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-F4jXHDy20WCbdOzy .icon-shape p,#mermaid-svg-F4jXHDy20WCbdOzy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-F4jXHDy20WCbdOzy .icon-shape .label rect,#mermaid-svg-F4jXHDy20WCbdOzy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-F4jXHDy20WCbdOzy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-F4jXHDy20WCbdOzy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-F4jXHDy20WCbdOzy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Material 3 1.4.0-beta01
实验性 API 被移除
需要 Alpha 版本
1.9.0-alpha04
MaterialKolor 5.0.0
Expressive 稳定
1.5.0-alpha22
组件逐步稳定化
生产项目评估采用
七、跨平台注意事项
组件 API 一致性 :CMP 的 Material 3 组件与 Jetpack Compose 共享同一套 API,组件定义、参数、行为在所有平台上保持一致。在 commonMain 中编写的组件代码无需修改即可运行于 Android、iOS、Desktop 和 Web。
平台差异:动态颜色是 Android 12+ 独占能力,其他平台需回退到静态色板或通过 MaterialKolor 等库从种子色生成。软件键盘的定位、滚动物理效果等在 iOS 上由 CMP 适配,但复杂交互仍需在真机验证。
commonMain 约束 :主题定义中不能直接引用 androidx.compose.material3.dynamicLightColorScheme(Android 专属 API),必须通过 expect/actual 机制下沉到 androidMain 源集。
配套习题(15题)
第一组:Material 3 组件体系(4题)
第1题 Material 3 中,强调层级最高的按钮组件是( )
A. TextButton
B. OutlinedButton
C. Button(Filled Button)
D. IconButton
答案:C
解析 :Button(Filled Button)是高强调的填充按钮。TextButton 为低强调,OutlinedButton 和 FilledTonalButton 为中等强调,IconButton 为低强调的紧凑按钮。
第2题 Material 3 中卡片组件的三种变体不包括( )
A. Card
B. ElevatedCard
C. OutlinedCard
D. FloatingCard
答案:D
解析 :Material 3 提供 Card(填充)、ElevatedCard(带阴影)、OutlinedCard(带边框)三种卡片变体。不存在 FloatingCard 组件。
第3题 Icon 组件的 tint 参数的作用是( )
A. 设置图标的背景色
B. 使图标自动适配当前主题色
C. 设置图标的阴影颜色
D. 控制图标的透明度
答案:B
解析 :Icon 的 tint 参数用于设置图标的着色,通常设置为 MaterialTheme.colorScheme.primary 等语义颜色,使图标自动适配当前主题。
第4题 Image 组件的 contentDescription 参数设为 null 的适用场景是( )
A. 所有图片都必须设置非 null 的描述
B. 纯装饰性图片,不承载信息
C. 图片加载失败时
D. 图片尺寸小于 48dp 时
答案:B
解析 :contentDescription 是辅助功能的必要参数。对于纯装饰性图片(不承载任何信息),可以设为 null,屏幕阅读器会跳过该元素。
第二组:Material 3 主题体系(4题)
第5题 MaterialTheme 的四个核心参数是( )
A. colorScheme、typography、shapes、motionScheme
B. colors、fonts、radii、animations
C. primary、secondary、tertiary、error
D. light、dark、system、dynamic
答案:A
解析 :MaterialTheme 接受 colorScheme(颜色方案)、typography(排版体系)、shapes(形状体系)、motionScheme(动效体系)四个参数,为子树中的所有组件提供默认样式。这四个维度的值通过 CompositionLocalProvider 传播。
第6题 Material 3 中 colorScheme.primary 的正确用途是( )
A. 设置页面的背景色
B. 作为主品牌色,用于高强调操作
C. 设置错误状态的提示颜色
D. 作为所有文本的默认颜色
答案:B
解析 :primary 是主品牌色,用于高强调操作和关键 UI 元素。页面背景使用 background,错误提示使用 error,文本颜色通常使用 onSurface 或 onBackground。
第7题 关于 Material 3 的 Typography,以下说法正确的是( )
A. Typography 只有一种文本样式
B. Typography 定义了从 Display 到 Label 的 15 种文本样式层级
C. Typography 只能使用系统默认字体
D. Typography 不需要自定义,使用默认值即可
答案:B
解析:Material 3 定义了 15 种文本样式,按尺寸层级组织为 Display、Headline、Title、Body、Label 五类,每类各有 Large/Medium/Small 三个尺寸。
第8题 Material 3 的 Shapes 体系中,ShapeKeyTokens.CornerFull 的特殊之处是( )
A. 使用主题中定义的 extraLarge 圆角
B. 使用 RoundedCornerShape(50%) 完全圆角,不受主题影响
C. 只在 Android 平台生效
D. 必须手动指定圆角值
答案:B
解析 :ShapeKeyTokens.CornerFull 使用 RoundedCornerShape(50%)(完全圆角)而非引用 Shapes,因此按钮始终保持药丸形状,无论主题中如何定义形状。
第三组:动态主题与设计 Token(4题)
第9题 在 Compose Multiplatform 中,动态颜色(从壁纸提取主题色)支持哪个平台?( )
A. 所有平台
B. 仅 iOS
C. 仅 Android 12 及以上
D. 仅 Desktop
答案:C
解析 :动态颜色主题仅支持 Android 12 及以上版本。在 CMP 中,动态颜色的实现必须放在 androidMain 源集中,其他平台通过 expect/actual 返回 null 并回退到静态色板。
第10题 在 commonMain 中调用 Android 专属的动态颜色 API 的正确方式是( )
A. 直接导入 androidx.compose.material3.dynamicLightColorScheme 并调用
B. 通过 expect/actual 机制在 commonMain 声明 expect 函数,在 androidMain 提供 actual 实现
C. 在 commonMain 中使用反射调用
D. 动态颜色 API 本身支持所有平台,无需特殊处理
答案:B
解析 :Android 专属 API 不能在 commonMain 中直接使用。正确的做法是在 commonMain 中声明 expect 函数(如 platformDynamicColorScheme),在 androidMain 中提供 actual 实现,其他平台返回 null 作为回退。
第11题 关于 staticCompositionLocalOf 与 compositionLocalOf 的选择,以下说法正确的是( )
A. 两者完全相同,可以互换使用
B. staticCompositionLocalOf 适用于不频繁变化的 Token,读取时不追踪变化
C. compositionLocalOf 适用于不频繁变化的 Token
D. 所有 Token 都应使用 compositionLocalOf
答案:B
解析 :staticCompositionLocalOf 用于不频繁变化的 Token(如间距),读取时不追踪变化,性能更优。compositionLocalOf 用于需要追踪读取的频繁变化值(如主题颜色)。
第12题 MaterialKolor 5.0.0 的核心新增特性是( )
A. 仅支持 Android 平台
B. 支持 Material 3 Expressive,提供 DynamicMaterialExpressiveTheme
C. 移除了对 iOS 的支持
D. 替代了 Material 3 的 MaterialTheme
答案:B
解析 :MaterialKolor 5.0.0 新增了对 Material 3 Expressive 的支持,通过 DynamicMaterialExpressiveTheme 提供从种子色生成 Expressive 动态主题的能力,支持 Compose Multiplatform 1.11.0。
第四组:组件状态与 Expressive 主题(3题)
第13题 使用密封接口建模 UI 状态的核心优势是( )
A. 减少代码量
B. 穷尽性检查和类型安全,避免非法状态组合
C. 提高渲染性能
D. 自动生成状态转换逻辑
答案:B
解析 :密封接口的 when 表达式必须覆盖所有子类型,编译器在添加新状态时强制开发者处理(穷尽性检查)。Success 携带数据、Error 携带错误信息,避免了"加载中却有数据"的非法状态组合。
第14题 Material 3 Expressive 主题新增的核心参数是( )
A. colorScheme
B. motionScheme
C. shapes
D. typography
答案:B
解析 :MaterialExpressiveTheme 在 colorScheme、shapes、typography 之外新增了 motionScheme 参数,用于控制动效方案(如 MotionScheme.expressive())。
第15题 关于 Material 3 Expressive 主题的使用,以下说法正确的是( )
A. Expressive 主题是稳定 API,可直接在生产项目中使用
B. Expressive 主题的实验性 API 在 Material 3 1.4.0-beta01 中已被移除,需要使用 Alpha 版本
C. Expressive 主题只支持 Android 平台
D. Expressive 主题替代了标准的 Material 3 主题,不可同时使用
答案:B
解析 :标记为 ExperimentalMaterial3ExpressiveApi 的公共 API 在 Material 3 1.4.0-beta01 中已被移除,需要使用 Alpha 版本(如 1.9.0-alpha04)才能继续使用。生产项目应谨慎评估是否依赖实验性 API。
本课知识点总结
1. Material 3 组件体系与强调层级
Material 3 为同一功能提供多种视觉变体,按强调层级组织。按钮族从高到低依次为:Button(填充)→ ElevatedButton/FilledTonalButton/OutlinedButton(中等)→ TextButton/IconButton(低)。卡片提供 Card、ElevatedCard、OutlinedCard 三种变体。CMP 1.12.1 使用 Material 3 1.12.0-alpha03(基于 Material3 1.5.0-alpha22),稳定版为 1.4.0。
2. Material 3 主题体系(四个维度)
MaterialTheme 通过 colorScheme、typography、shapes、motionScheme 四个维度定义应用的设计语言。ColorScheme 使用语义角色(primary、surface、error 等)而非具体色值。Typography 定义 15 种文本样式层级。Shapes 定义五种圆角尺寸类别,ShapeKeyTokens.CornerFull 始终使用完全圆角。MotionScheme 提供 standard() 和 expressive() 两种动效方案。
3. 动态主题与自定义设计 Token
动态颜色是 Android 12+ 独占能力,必须放在 androidMain 源集中,通过 expect/actual 机制在 commonMain 中安全调用。非 Android 平台可使用 MaterialKolor 等库从种子色生成完整色板,MaterialKolor 5.0.0 已支持 Material 3 Expressive。Material 3 主题不包含间距参数,生产项目应通过 CompositionLocal(staticCompositionLocalOf 用于稳定 Token)实现间距设计 Token。
4. 组件状态表达
使用密封接口(sealed interface)建模 Loading、Success、Error、Retrying 等状态,获得穷尽性检查和类型安全。将状态渲染封装为泛型 LoadingUi 组件,统一加载、错误、重试的 UI 呈现。组件的 enabled 状态应基于业务逻辑自动计算,加载中的按钮应同时禁用点击并显示进度指示器。
5. Material 3 Expressive 主题与 1.5.0-alpha22 更新
CMP 支持实验性的 MaterialExpressiveTheme,新增 motionScheme 参数。Expressive 主题的实验性 API 在 Material 3 1.4.0-beta01 中已被移除,需使用 Alpha 版本。1.5.0-alpha22 中,TopAppBarScrollBehavior 提升为稳定状态,ExpressiveFloatingToolbar 转为非实验性,TopAppBarDefaults 新增 snapAnimationSpec getter,pinnedScrollBehavior 和 enterAlwaysScrollBehavior 更新为接受 ScrollableState。Expressive 组件正在逐步稳定化。
下一课预告 :第7课将深入列表与惰性布局,包括 LazyColumn、LazyRow、LazyVerticalGrid 的基本用法,key() 的正确使用与列表性能优化,列表项的重组控制与 @Stable/@Immutable 标注,iOS 端惰性布局的滚动性能改进,以及分页加载与无限滚动的实现模式。