关于 compose-slot-api-pattern
面向 Compose 开发者的组件 API 设计技能,帮助可复用组件通过可组合插槽接收变化内容,并按需提供布局作用域和默认值。
主要功能
- 01用可组合插槽替代内容型参数
- 02为插槽选择合适的作用域接收器
- 03用 null 表示可选插槽
- 04将常用默认内容集中到 Defaults
- 05说明不适用插槽模式的情形
适用场景
- 01审查可复用组件的参数设计
- 02让调用方定制标题、图标或操作区
- 03避免缺省内容仍占用布局空间
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ chrisbanes/ skills --skill compose-slot-api-pattern安装后在 Claude Code 中输入/compose-slot-api-pattern确认已加载。
详细介绍
Compose:插槽 API 模式
核心原则
可复用的 Compose 组件描述布局结构。调用方通过插槽提供可变的视觉内容。
API 审查流程
- 确认组件可复用。对于真正只使用一次的可组合项,不要添加插槽仪式。
- 标出哪些区域会因调用方而异:标题、辅助文本、前置视觉元素、后置视觉元素、操作项、正文。
- 将由调用方控制的基本内容和形状标志替换为插槽。
- 仅当插槽在调用方应使用其作用域 API 的布局中发出时,才添加接收器作用域。
- 将缺失时可省略的区域设为可空(
null),以便组件可以省略其容器和间距。 - 将重复的默认内容或令牌放入
XxxDefaults。 - 同时遵循
compose-modifier-and-layout-style中的modifier规则。
1. 将基本内容替换为 @Composable 插槽
当组件需要由调用方控制的内容时,优先使用 @Composable () -> Unit 插槽。结构上必需的插槽应保持非空,不提供默认值。可选插槽则应设为可空,默认值为 null。
// ❌ BAD — primitive parameters; trailing area is the only slot; everything else is locked
@Composable
fun SettingsRow(
title: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
subtitle: String? = null,
leadingIcon: ImageVector? = null,
trailing: (@Composable () -> Unit)? = null,
) { … }
// ✅ GOOD — every visual region is a slot; the row describes structure, not content
@Composable
fun SettingsRow(
headlineContent: @Composable () -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
supportingContent: (@Composable () -> Unit)? = null,
leadingContent: (@Composable () -> Unit)? = null,
trailingContent: (@Composable () -> Unit)? = null,
) { … }
对于典型的一行内容,调用处可以保持简洁:
SettingsRow(
headlineContent = { Text("Account") },
leadingContent = { Icon(Icons.Default.Person, contentDescription = null) },
trailingContent = { SettingsRowDefaults.Chevron() },
onClick = { … },
)
特殊情况不再需要新增组件参数:
SettingsRow(
headlineContent = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Inbox")
Spacer(Modifier.width(8.dp))
Badge { Text("3") }
}
},
onClick = { … },
)
插槽命名
- 对于自由形式的
@Composable () -> Unit插槽,使用xxxContent(headlineContent、supportingContent、trailingContent)——与 Material 3 保持一致。 - 当插槽在语义上受到约束,且组件名称足以明确其含义时,使用单数名词(
title、icon、actions),例如Scaffold(topBar = { … }, bottomBar = { … }, floatingActionButton = { … })。 - 不要同时使用
content和其他xxxContent插槽——每个组件选择一种命名约定。
2. 当插槽内容发出到布局中时使用作用域接收器
如果插槽内容将放在一个 Row/Column/Box 中,并且调用方应能够使用其布局功能(Modifier.weight、BoxScope.matchParentSize、对齐方式),请将插槽声明为接收器 lambda:@Composable RowScope.() -> Unit。
// ❌ BAD — actions render inside a Row, but callers can't use RowScope.weight()
@Composable
fun MyTopBar(
title: @Composable () -> Unit,
actions: @Composable () -> Unit = {}, // ← caller has no Row scope
)
// ✅ GOOD — caller gets RowScope; .weight() and alignment-by works inside
@Composable
fun MyTopBar(
title: @Composable () -> Unit,
actions: @Composable RowScope.() -> Unit = {},
)
正因如此,TopAppBar(actions = { IconButton(…); IconButton(…) }) 才能正常工作——调用方隐式处于 RowScope 中。
不要不加判断地给每个插槽都添加作用域接收器。接收器应与插槽实际发出到的父布局相匹配。如果插槽在 Box 中渲染,就使用 BoxScope。如果在 Column 中,就使用 ColumnScope。如果父布局不是标准布局(或者其作用域 API 对插槽内容没有用处),则不需要接收器。
3. 可选插槽——设为可空,默认值为 null
对于可能缺失的插槽,优先使用 (@Composable () -> Unit)? = null,而不是 @Composable () -> Unit = {}:
// ❌ BAD — empty default; "no leading content" is the empty lambda
leadingContent: @Composable () -> Unit = {}
// ✅ GOOD — null means "no slot"; the component can omit space/padding when absent
leadingContent: (@Composable () -> Unit)? = null
使用可空插槽时,组件可以根据 leadingContent != null 进行分支,并完全跳过该插槽的容器、间距和内边距。若默认值为空,布局通常仍会为缺失的内容分配空间。
4. 默认值放在 XxxDefaults 中
如果你发现自己需要说明“后置插槽通常应为一个箭头”,或“默认背景请传入 MaterialTheme.colorScheme.surface”,就应将这些辅助项与组件放在一起,放入 XxxDefaults 对象中:
object SettingsRowDefaults {
@Composable
fun Chevron() = Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = null,
)
@Composable
fun TrailingValue(text: String) = Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
常见情况的调用处仍保持声明式,而插槽也仍可完全用于特殊情况:
SettingsRow(
headlineContent = { Text("Notifications") },
trailingContent = { SettingsRowDefaults.Chevron() },
onClick = { … },
)
这与 Material 3 的 ButtonDefaults、TopAppBarDefaults 等保持一致——默认值本身是可组合项时,应放在这里,而不是将带有 MaterialTheme.x.y 默认值的内容直接展开为新的组件参数。
快速参考
| 症状 | 诊断 | 修复 |
|---|---|---|
可复用组件上出现 title: String, subtitle: String?, leadingIcon: ImageVector? | 原始内容参数(§1) | 将其转换为 xxxContent: (@Composable () -> Unit)? 插槽 |
多个布尔标志(showChevron、showSwitch)用于选择尾部形状 | 枚举形状(§1) | 使用一个 trailingContent: (@Composable () -> Unit)? 插槽 |
mode: Mode.Sealed 参数列出多个变体 | 与标志混杂相同(§1) | 将其设为插槽 |
Row 主体中有 actions: @Composable () -> Unit = {} | 缺少作用域接收者(§2) | 使用 actions: @Composable RowScope.() -> Unit = {} |
可选区域使用 slot: @Composable () -> Unit = {} | 默认空 Lambda(§3) | 使用 slot: (@Composable () -> Unit)? = null,并据此分支处理 |
组件参数 defaultColor: Color = MaterialTheme.colorScheme.surface | 默认值被内联(§4) | 将其移至 XxxDefaults.color 并引用它 |
| 每个调用点都重复使用常见的尾部内容 | 缺少默认辅助函数(§4) | 添加 XxxDefaults.Chevron() 等 |
不适用的情况
- 仅使用一次的组件。 一个只在一处使用且没有复用计划的可组合项,不会从插槽灵活性中受益;插槽间接层反而会让唯一的读者更难读懂代码。使用原始参数和内联内容即可。(一旦出现第二个调用点,就改用插槽。)
- 要求所有调用方外观一致的设计系统基础组件。
Heading2(text: String)的存在正是因为你希望所有 H2 的外观一致;将其改为headlineContent: @Composable () -> Unit会让调用方有机会违反这一规则。保留原始参数。(反过来说:如果Heading2以后需要内联徽标,就改用插槽。) - 组件有意负责管理的语义参数。 如果组件负责管理排版、图标、无障碍文案或产品一致性,那么原始参数可能正是你需要的约束。
- 确实需要受限的类型参数。
Switch(checked: Boolean, onCheckedChange: ...)不需要将其选中指示器设为插槽。带回调的布尔值不是“内容”。 - 性能关键的快速路径(在应用代码中罕见,在框架基础组件中常见)。插槽会分配一个 Lambda。在最深层的 LazyList 项目层中,有时原始参数更有优势。如果你不是在编写框架,则不适用此项。
评审中的警示信号
| 想法 | 事实 |
|---|---|
| “标题始终是 String;把它做成插槽属于过度设计” | “如今始终如此”正是陷阱。Material 的 ListItem.headlineContent 存在的原因是,明天就可能有人想要一个 Text + Badge。插槽会在每个调用位置多出 8 个字符的包装代码({ Text(…) });以后再添加插槽时,重构就得修改每个现有调用位置。 |
| “Lambda 比字符串开销更大” | 对于典型 Compose UI 的规模,这种差异无法测量——而且框架自身的组件(Button、ListItem、TopAppBar、Scaffold)都使用插槽。如果你的组件位于最关键的热路径中,请参阅“何时不应用”。 |
| “有人提出要求后,我再添加插槽” | 添加插槽会将一个参数变成两个参数(插槽本身 + 可能需要的内部标志),并且要修改每个调用位置。形状变更不是可以“以后再做”的改动。 |
“我会改用密封的 Trailing 类型来建模这些变体” | 密封枚举的范围有限;插槽则没有限制。密封类型可以奏效,直到有人需要一种你未曾预料的变体——届时你就得回去修改组件。插槽可以避免这个循环。 |
| “前导区域始终是图标,尾随区域则不固定——我只给尾随区域设置插槽” | 这是部分插槽陷阱。“始终是图标”的假设,第一次遇到需要头像、旗帜表情符号或彩色形状的行时就会失效。前导区域也要设为插槽。 |
| “现在只有一个调用位置” | 如果现在只有一个调用位置,你可能还没有在设计可复用组件。请参阅“何时不应用”——对于真正只使用一次的情况,原语就足够了。一旦复制粘贴,就改用插槽。 |
相关内容
compose-modifier-and-layout-style— 修饰符参数规则(其中的 §1–§3)与插槽 API 相辅相成。可复用组件既要接受modifier参数,也要将其内容设为插槽;由调用方负责放置组件以及决定放置什么内容。