Material3 组件选择、状态管理与避坑指南
与官方文档的关系:官方列API 签名与默认样式;本文写「为什么这样配」、易混点、和 View/XML 时代思维差异。请始终以 developer.android.com/jetpack/compose 为准核对版本差异。
配套示例
运行 App →系列示例→09|基础控件与 Material3,对照ControlsLabScreen.kt(纵向滚动一屏内包含Text / TextField / Button / Switch / Chip / Radio / Checkbox / Slider / AlertDialog等区块)。
屏内纵向分区(与代码顺序一致)
自上而下与ControlsLabScreen.kt对应关系,方便「一文 + 一屏」对照:
| 顺序 | 区块 | 要点 |
|---|---|---|
| 1 | 长Text | maxLines+TextOverflow.Ellipsis,大字模式/窄屏防撑破 |
| 2 | OutlinedTextField(昵称) | value+onValueChange受控;isError+supportingText;trailingIcon清空 |
| 3 | OutlinedTextField(密码) | PasswordVisualTransformation/None切换;KeyboardOptions |
| 4 | 提交Button | enabled与name、agree、loading绑定;rememberCoroutineScope().launch { delay }仅演示加载态,生产请ViewModel + 可取消任务 |
| 5 | Switch+ 协议文案 | 与主按钮enabled联动 |
| 6 | AssistChip+FilterChip | 主行动 vs 筛选语义;FilterChip的leadingIcon勾选 |
| 7 | RadioButton组 | Modifier.selectableGroup()+ 行selectable(role = RadioButton),RadioButton(onClick = null) |
| 8 | Checkbox | 独立附加选项示例 |
| 9 | Slider | steps = 9、valueRange = 0f..1f,展示百分比文案 |
| 10 | AlertDialog | showDialog布尔;onDismissRequest与双按钮一致关窗 |
受控输入:单一真相(与 01 篇对齐)
value与onValueChange必须同源;若value来自 ViewModel、onValueChange只写本地remember而不同步上去,会出现光标跳、删字回弹等「输入法与 UI 打架」。
1.Text:别只盯stringResource
技巧
- 行数与溢出:列表/卡片标题务必显式
maxLines+overflow = TextOverflow.Ellipsis,否则在大字模式或窄屏下会把父布局撑破或裁切不可预期。 style:优先MaterialTheme.typography.*,让字阶随 Theme 变;局部再用merge覆盖fontSize时,注意行高lineHeight是否仍匹配(否则中文易「行间距发飘」)。- 富文本链接:新代码优先看
Text+LinkAnnotation等 API;旧代码里可能见到AnnotatedString+ClickableText。无论哪种写法,都要把「可点链接」与「整段 onClick」分清,避免命中区域重叠。
避坑
- 在
Text里拼超长调试字符串 → 重组时分配大量String/CharSequence,热点路径里改用截断或日志走 Timber。 - 把业务格式化(金额、日期)放在 Composable 每次重组都算:应
remember(currency, amount)或下沉ViewModel(与 07 篇derivedStateOf择一或组合)。
2.OutlinedTextField:状态必须「单一真相」
心智:value + onValueChange是受控组件;value来源不清就会输入法与 UI 打架(光标跳、删字回弹)。
技巧
isError+supportingText:错误提示别只用Toast;字段旁错误文案更可访问(TalkBack 也能读到)。- 密码:
PasswordVisualTransformation()+ 自管showPassword布尔;若是复杂的自定义VisualTransformation,建议用remember { … }包住,避免重组时反复创建无意义对象。简单内置转换通常问题不大。 singleLine = true与maxLines:搜索框常用单行;多行备注用maxLines+ 滚动,别混用语义。
避坑
onValueChange里做同步重计算(正则很强、每次全量校验大文本)会卡输入线程感;应防抖或把重校验放LaunchedEffect/ ViewModel(见 03 篇)。readOnly = true仍可获得焦点:若只想展示,考虑Text+Modifier.clickable或disabled语义,别假装是输入框。
3.Button/TextButton/IconButton
技巧
- 防连点:提交动作应用
enabled = !loading或统一在 ViewModel 里串行化提交;本屏用delay+LinearProgressIndicator只演示形态,生产请把异步放进ViewModel并处理取消与重复点击。 TextButton放在OutlinedTextFieldtrailing:很常见;注意最小触摸目标 48dp(Material 默认会处理一部分,自定义缩得过小会被无障碍打回)。
避坑
- 在
Button的onClick里直接launch(Dispatchers.IO)且无宿主:泄漏/崩溃难查;用rememberCoroutineScope+ 可取消 Job或ViewModel。
4.Switch/Checkbox/RadioButton
技巧
- 文案绑定:
Switch右侧Text与 switch同一语义时,注意读屏是否出现「两个控件」;必要时用toggleable或文案semantics合并。 - 三态 Checkbox(若仍用 XML 习惯):Compose 默认二态;三态要自定义状态机并在设计稿标明。
避坑
- 用
AssistChip当「主要提交按钮」:语义不对,点按区域与键盘导航顺序也不友好;主行动用Button,筛选用FilterChip。
4.1FilterChip:selected与leadingIcon
- 语义:表示「过滤条件开/关」;
selected = true时建议加勾选图标(本仓库用Icons.Default.Check),读屏用户能感知「已应用筛选」。 - 避坑:
onClick里同时改多个筛选条件时,注意状态原子性(一次重组内一致),避免半开半关。
4.2RadioButton:别只给圆圈加点按区域
- 技巧:用
Modifier.selectableGroup()包一组,单行Row(Modifier.selectable(role = Role.RadioButton))让整行可点;RadioButton的onClick可置null,由外层selectable统一处理,避免双重点击逻辑。 - 避坑:三个
RadioButton各写独立mutableStateOf<Boolean>易出现多选为真;用单个selectedIndex: Int或enum才是单选真相源。
4.3Checkbox:与Switch分工
- Checkbox:多选列表、条款勾选;Switch:即时生效的设置项。混用会让用户形成错误心理模型。
- 三态:默认二态;树形「部分选中」要自定义
ToggleableState与图标,别硬套默认 Checkbox。
4.4Slider:steps与重组频率
steps = 9+valueRange 0f..1f:端点之间有 9 个中间刻度,合计11 个可选值、10 个区间;适合「音量、缩放百分比」等可解释档位。- 避坑:拖动时
onValueChange极高频;不要把重计算/日志放在 lambda 里。展示字符串可用remember(sliderValue) { … }或derivedStateOf(见第 07 篇)。
4.5AlertDialog:onDismissRequest是唯一真相吗?
- 必须决定:蒙层点击、返回键、
cancel按钮是否等价于同一业务取消;否则会出现「用户以为取消了,其实后台任务仍在跑」。 - 避坑:在
text = { }里塞大列表 / WebView:对话框不是容器万能胶;复杂内容用全屏路由或ModalBottomSheet。
5. 图片与图标(本屏未接网络图,只给策略)
- 图标:
Icon(imageVector = Icons.Default.xxx, contentDescription = …),contentDescription为null仅当装饰性;可点击必须描述。 - 位图:优先Coil / Glide Compose集成,
model+contentScale+约束尺寸;不要在LazyColumnitem 里按原图尺寸 decode。
6.Card/Surface/Divider:层次别用错
Surface:更底层,tonal elevation、形状、点击波纹常从这里搭;适合作为「一块可交互表面」的根。Card:Material3 的 Card 更偏内容分组;别把整张页面套一层 Card 再套一层 Surface无意义叠高程。HorizontalDivider:分隔列表块;在滚动列表里 Divider 过密会显得「碎」;考虑用间距 + 背景对比代替。
7.RangeSlider与连续值(补充)
- 连续值绑定
FloatState或mutableFloatStateOf;拖动中会高频重组,派生文案(如「约等于 12 GB」)用remember(value) { … }。 - 范围选择:
RangeSlider适合价格区间、时间区间等「起点 + 终点」场景;两端值都要与业务最小粒度对齐,避免用户拖出无法提交的中间值。
8.AlertDialog/ModalBottomSheet(思路)
- 对话框状态应与导航或单一 boolean明确绑定;
dismissRequest里要处理返回键与蒙层点击是否等价于取消。 - BottomSheet与全屏键盘同屏:Insets + sheet 高度一起验收,否则出现「键盘盖住 sheet 按钮」(见 12 篇)。
9. 和「精通段」衔接
- 下一篇自定义 Layout:当你发现
Row/Column表达不了测量策略时,再下沉到Layout。 - 动画篇:控件显隐用
AnimatedVisibility比手写alpha更符合可访问性与默认过渡。
10. 自检清单
- 受控字段的
value是否单一来源(本地remember或 ViewModelStateFlow,勿混)? - 提交路径是否在主线程重活 / 无取消?应下沉ViewModel并暴露
loading/error(见 01 篇)。 Slider/TextField的高频回调里是否只做O(1) 更新,重逻辑已remember/ 防抖 / 派生状态?- Radio / Switch / Checkbox的读屏语义是否与视觉一致(整行可点、单选真相源)?
AlertDialog关闭路径是否与业务取消对齐,无「窗关了任务还在跑」?
参考答案(复习用)
- 是。
OutlinedTextField(value = x, onValueChange = { x = it })的x只来自一处;若 VM 下发state.name,onValueChange必须onEvent写回 VM,不能只在本地改remember。 - 否才算合格(对生产而言)。本仓库
ControlsLabScreen的scope.launch { delay }仅演示;生产应在 ViewModel 用viewModelScope+MutableStateFlow/UiState暴露loading/error,并可在离开屏时取消。 - 应如此。
onValueChange只赋值;校验用LaunchedEffect(name)防抖或 VM 内;Slider的展示文案用remember(sliderValue)或derivedStateOf,避免在onValueChange里打日志或重算大对象。 - 应对齐。单选用一个
selectedIndex+selectableGroup+ 行selectable,RadioButton(onClick = null);Switch 与文案不要两个独立焦点抢同一业务布尔。 - 应对齐。
onDismissRequest、取消按钮、确认按钮各自应调用同一套「关窗 + 是否取消请求」逻辑;若仅showDialog = false而后台仍launch,属于未对齐。
源码仓库:ComposeDemo(分支
main)
相关推荐
《重组与参数稳定性:跳过规则、derivedStateOf 与调试》
《测试分层:JVM 单测、ViewModel 测试与 Compose UI Test》
