组件不是控件,是一次决定
你建一个组件,不是为了少写几行代码,是为了把一个决定固定下来。这一章讲这个决定该有多大——以及它什么时候开始失控。
组件封的是决定,不是代码
先摆正一件事。你写 OrderCard 的时候,复用代码只是副产品。真正发生的是:
这正是第 1 章那条主线在组件层面的样子。一个组件的价值和它关掉了多少个临场决定成正比。
推论:一个把所有决定都变成参数的组件,等于什么都没关掉。它只是把 110 个随手定的数搬了个家。
2ⁿ 的诅咒
组件失控的过程永远是一样的:一个布尔值一个布尔值地加,每次都觉得「就一个布尔值而已」。
把开关一个个打开,看那两个数:
红色的格子是永远不会有人打开的组合——但它们能被构造出来。也就是说,它们是 IllegalStateException 和「诶这个界面怎么这样」的产地。
这些组合不会在你加参数的那天出问题。它们会在半年后,某个人为了赶需求传了 isLoading = true, hasIcon = true, isCompact = true,然后图标和转圈重叠在一起的时候出问题。
而那时候没人记得这三个参数为什么会互斥。
那条线:样子还是身份
怎么判断该做参数还是该拆成变体?有一条很实用的线:
- 改的是样子(大小、有没有图标、单行还是两行)→ 参数
- 改的是身份(这是个危险操作、这是个次要动作)→ 变体
理由:身份不同的东西,将来会朝不同方向演化。
今天「危险按钮」只是颜色不同;明天它要加二次确认;后天它要在长按时才生效。如果它只是 Button(isDanger = true),这些需求会全部挤进那一个组件,然后你就有了第七个布尔参数。
Button( text: String, isLoading: Boolean = false, isDanger: Boolean = false, isCompact: Boolean = false, hasIcon: Boolean = false, icon: ImageVector? = null, onClick: () -> Unit )
32 种组合。hasIcon = true 但 icon = null 会怎样?没人知道。
// 身份 → 三个变体 PrimaryButton(text, onClick) SecondaryButton(text, onClick) DangerButton(text, onClick) // 样子 → 参数,且用可空 // 类型消灭掉不合法的组合 PrimaryButton( text: String, icon: ImageVector? = null, // 有就画 loading: Boolean = false, onClick: () -> Unit )
icon: ImageVector? 一个参数干掉了两个——不可能出现「说有图标但没给图标」。
用插槽,不用参数
Compose 里有一个比参数更好的东西:插槽(把 @Composable lambda 当参数)。
// ✗ 参数式:每来一个新需求就加一个参数
fun ListItem(
title: String,
subtitle: String? = null,
iconRes: Int? = null,
badgeCount: Int? = null,
showChevron: Boolean = false,
trailingSwitch: Boolean = false,
switchChecked: Boolean = false, // 又一个耦合参数…
onSwitchChange: ((Boolean) -> Unit)? = null
)
// ✓ 插槽式:结构固定,内容开放
@Composable
fun ListItem(
headline: @Composable () -> Unit,
modifier: Modifier = Modifier,
supporting: (@Composable () -> Unit)? = null,
leading: (@Composable () -> Unit)? = null,
trailing: (@Composable () -> Unit)? = null
)
// 用的时候,尾部想放什么放什么 —— 组件不需要知道
ListItem(
headline = { Text("推送通知") },
supporting = { Text("接收订单状态变化") },
leading = { Icon(Icons.Default.Notifications, null) },
trailing = { Switch(checked, onCheckedChange) }
)
插槽的关键好处:组件固定的是「结构」(哪儿放什么、间距多少、对齐怎么算),开放的是「内容」。
而结构正是你想统一的东西——第 5 到 12 章那些间距、对齐、字号规则,全部封在这一层里。至于尾部到底是个开关还是个箭头,那本来就不该由组件决定。
这也是 Material 3 自己的 API 风格:Scaffold、TopAppBar、ListItem、Card 全是插槽式的。
什么时候不该建组件
反方向也要说,因为过早抽象的代价一样大:
- 只用过一次。「以后会用到」不算理由。等到第二次用到时再抽,那时你才知道哪些该变、哪些不该变。
- 两处用法看起来像但意图不同。「订单卡片」和「商品卡片」现在长得一样,不代表它们该是同一个组件——它们会各自演化,而合并会让每次演化都变成加参数。
- 为了避免重复三行代码。三行重复的代价,远小于一个抽象错的组件。
如果你无法用一句话说清「这个组件是什么」,它就不该存在。
「订单卡片」——清楚。
「通用信息展示卡片」——不清楚,它会长成怪物。
说不清的时候,通常意味着你在合并两个不同的东西。拆开,各自命名,让它们独立演化。等它们真的长得一样了(而且稳定了半年),再考虑合并。
- 「参数还是变体」——改样子做参数,改身份做变体。身份不同的东西会朝不同方向演化。
- 「怎么加新需求」——优先加插槽,不加布尔参数。用可空类型消灭不合法组合。
- 「什么时候抽组件」——第二次用到时。说不清它是什么就不建。