mcpField
C
compose-slot-api-pattern

compose-slot-api-pattern

github.com/chrisbanes开发1.1k970 次安装1 天前更新
代码仓库

关于 compose-slot-api-pattern

面向 Compose 开发者的组件 API 设计技能,帮助可复用组件通过可组合插槽接收变化内容,并按需提供布局作用域和默认值。

主要功能

  1. 01用可组合插槽替代内容型参数
  2. 02为插槽选择合适的作用域接收器
  3. 03用 null 表示可选插槽
  4. 04将常用默认内容集中到 Defaults
  5. 05说明不适用插槽模式的情形

适用场景

  1. 01审查可复用组件的参数设计
  2. 02让调用方定制标题、图标或操作区
  3. 03避免缺省内容仍占用布局空间

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/chrisbanes/skills --skill compose-slot-api-pattern

安装后在 Claude Code 中输入/compose-slot-api-pattern确认已加载。

详细介绍

Compose:插槽 API 模式

核心原则

可复用的 Compose 组件描述布局结构。调用方通过插槽提供可变的视觉内容。

API 审查流程

  1. 确认组件可复用。对于真正只使用一次的可组合项,不要添加插槽仪式。
  2. 标出哪些区域会因调用方而异:标题、辅助文本、前置视觉元素、后置视觉元素、操作项、正文。
  3. 将由调用方控制的基本内容和形状标志替换为插槽。
  4. 仅当插槽在调用方应使用其作用域 API 的布局中发出时,才添加接收器作用域。
  5. 将缺失时可省略的区域设为可空(null),以便组件可以省略其容器和间距。
  6. 将重复的默认内容或令牌放入 XxxDefaults。
  7. 同时遵循 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 参数,也要将其内容设为插槽;由调用方负责放置组件以及决定放置什么内容。

评论

支持 Markdown