用途
该技能用于设计或评审可复用的 Jetpack Compose 组件:当组件的视觉区域会因调用方不同而变化,或组件签名中已经出现原始内容参数(如 title: String、subtitle: String?、leadingIcon: ImageVector?)和用于切换内容形态的布尔标志(如 showSwitch、showChevron)时,指导改用 slot API 模式。
核心原则:可复用 Compose 组件的职责是布局,而不是枚举它布局的内容。一旦写出 title: String、subtitle: String?、leadingIcon: ImageVector?、trailingIcon: ImageVector?、trailingText: String?、showSwitch: Boolean、switchValue: Boolean、onSwitchChange: (Boolean) -> Unit?、badge: String? 这类参数,组件就不再描述布局,而是在枚举调用点,下一个调用点就会需要组件没有的参数。解决办法是通过 @Composable lambda 参数把内容委托给调用方:组件提供结构(leading、headline、supporting、trailing 各放哪里),调用方提供槽位中的一切内容。Material 3 的 ListItem 是典型示例,每个视觉部分都是槽位(headlineContent、supportingContent、leadingContent、trailingContent、overlineContent),而不是原始类型。
适用时机
正在设计或评审一个打算复用的 Compose 组件(现在或计划有多个调用点),其视觉内容因调用方而异,且满足以下任一条件:签名中有 title: String、icon: ImageVector、actionText: String? 等描述内容的原始类型;有多个随调用点变化的可选内容参数;有仅用于切换内容形态的布尔标志;接受 String 参数但不同调用方分别想要自定义样式 Text、带 Badge 的 Text 或一行图标;已经有一个槽位(通常是 trailing 或 content),其余参数仍是原始类型。
主要能力与规则
1. 用 @Composable 槽位替换原始内容参数
在组件需要调用方控制内容的地方,优先使用 @Composable () -> Unit 槽位。结构上必需的槽位保持非空且无默认值;可选的槽位设为可空并以 null 为默认值。
反例:SettingsRow(title: String, onClick, modifier, subtitle: String? = null, leadingIcon: ImageVector? = null, trailing: (@Composable () -> Unit)? = null)。这种写法看似可行,因为当前调用点都符合(title 总是单行文本,leadingIcon 总是 ImageVector),但下一个调用点若需要标题旁加 Badge、leading 是圆形头像而非 ImageVector、subtitle 是一行 chips,就会被迫新增参数、新增标志或绕过限制。
正例:SettingsRow(headlineContent: @Composable () -> Unit, onClick, modifier, supportingContent: (@Composable () -> Unit)? = null, leadingContent: (@Composable () -> Unit)? = null, trailingContent: (@Composable () -> Unit)? = null)。典型内容是一行代码,调用点仍然简短;原本需要新增原始参数的场景现在无需改动组件。
槽位命名:自由形式的 @Composable () -> Unit 槽位使用 xxxContent(headlineContent、supportingContent、trailingContent),与 Material 3 一致;当槽位语义受限且组件名可消歧时使用单数名词(title、icon、actions),如 Scaffold(topBar、bottomBar、floatingActionButton);不要在同一组件中混用 content 与其他 xxxContent 槽位,每个组件选定一种约定。
2. 槽位内容进入布局时使用 Scope 接收者
如果槽位内容将位于 Row/Column/Box 中,且其布局能力(Modifier.weight、BoxScope.matchParentSize、alignment)应对调用方可用,则把槽位声明为接收者 lambda:@Composable RowScope.() -> Unit。反例是 actions: @Composable () -> Unit = {} 渲染在 Row 内但调用方无法使用 RowScope.weight();正例是 actions: @Composable RowScope.() -> Unit = {}。这正是 TopAppBar(actions = { IconButton(…); IconButton(…) }) 能工作的原因——调用方隐式处于 RowScope 中。不要反射式地给每个槽位都加 scope 接收者,接收者应匹配槽位实际渲染所在的父布局:在 Box 内用 BoxScope,在 Column 内用 ColumnScope,父布局不是标准布局(或其 scope API 对槽位内容无用)则不加。
3. 可选槽位用可空加 null 默认值
对于可能缺席的槽位,优先使用 (@Composable () -> Unit)? = null 而非 @Composable () -> Unit = {}。原因:可空槽位让组件可以判断 leadingContent != null 并完全跳过该槽位的容器、间距和内边距;空默认值仍会分配槽位,有时会在实际为空的内容周围留下多余的内边距或间隔。可空形式让“缺席”在结构上明确区分。代价是:以前显式传空 {} 来关闭槽位的调用方现在需要传 null 或省略参数,而这本来就是正确做法。
4. 默认内容放在 XxxDefaults 中
当需要说明“trailing 槽位通常应是 chevron”或“默认背景传 MaterialTheme.colorScheme.surface”时,把辅助内容放在组件旁的 XxxDefaults 对象中。例如 SettingsRowDefaults 提供 Chevron() 和 TrailingValue(text: String)。常见场景调用点保持声明式,一次性场景槽位仍完全开放。这与 Material 3 的 ButtonDefaults、TopAppBarDefaults 等一致——本身是 composable 的默认内容应放在这里,而不是作为新组件参数并内联展开 MaterialTheme.x.y 默认值。
快速参考
症状与修复对应:可复用组件上出现 title: String、subtitle: String?、leadingIcon: ImageVector? → 原始内容参数,改为 xxxContent: (@Composable () -> Unit)? 槽位;多个布尔标志(showChevron、showSwitch)选择 trailing 形态 → 枚举形态,改为一个 trailingContent 槽位;mode: Mode.Sealed 参数列出变体 → 同上,改为槽位;Row 体内 actions: @Composable () -> Unit = {} → 缺少 scope 接收者,改为 @Composable RowScope.() -> Unit = {};可选区域用 slot: @Composable () -> Unit = {} → 空 lambda 默认值,改为可空加 null 并分支;组件参数 defaultColor: Color = MaterialTheme.colorScheme.surface → 默认值内联,移到 XxxDefaults.color;常见 trailing 内容在每个调用点重复 → 缺少默认辅助,添加 XxxDefaults.Chevron() 等。
不适用场景
单次使用的组件:只在一处使用且无复用计划的 composable 不会从槽位灵活性中受益,槽位间接层反而让唯一读者更难读,原始参数加内联内容即可;一旦出现第二个调用点就应改为槽位。设计系统原语且要求所有调用方外观一致:Heading2(text: String) 的存在就是为了每个 H2 看起来相同,改成 headlineContent 会诱使调用方破坏规则,保持原始类型;反之若 Heading2 需要内联 badge,则改为槽位。组件有意拥有的语义参数:如果组件拥有排版、图标、无障碍措辞或产品一致性,原始参数可能正是想要的约束。真正受限的类型参数:Switch(checked: Boolean, onCheckedChange: ...) 不需要把 checked 指示器做成槽位,带回调的布尔值不是“内容”。性能关键的快速路径(应用代码中少见,框架原语中常见):槽位是分配的 lambda,在最深的 LazyList item 层有时原始类型更优;如果不是在写框架,这不适用。
评审中的常见误区
“Title 总是 String,做成槽位是过度设计”——“今天总是”是陷阱,Material 的 ListItem.headlineContent 存在是因为明天有人想要 Text + Badge;槽位在每个调用点只多 8 个字符的包装,而事后加槽位要改每个现有调用点。“Lambda 比字符串重”——在典型 Compose UI 规模下不可测量,框架自身组件(Button、ListItem、TopAppBar、Scaffold)都使用槽位;若组件处于最热路径,见不适用场景。“以后有人要再加槽位”——加槽位会把一个参数变成两个参数并修改每个调用点,形态变化不是“以后”的事。“用 sealed Trailing 类型建模变体”——sealed 枚举有界,槽位无界;sealed 类型在遇到未预料的变体时仍要改组件,槽位避免这个循环。“leading 总是图标、trailing 才变化,只给 trailing 做槽位”——这是部分槽位陷阱,“总是图标”的假设在第一次需要头像、旗帜 emoji 或彩色形状时就会破灭,leading 也要做槽位。“今天只有一个调用点”——如果只有一个调用点,可能还没在设计可复用组件,见不适用场景,真正单次使用时原始类型没问题;一旦复制粘贴就应改为槽位。
相关
compose-modifier-and-layout-style:其中的 modifier 参数规则与 slot API 配套。可复用组件接受 modifier 参数并对内容做槽位化;调用方同时拥有放置位置和放置内容。