Loading...
Loading...
Compare original and translation side by side
title: String, subtitle: String?, leadingIcon: ImageVector?, trailingIcon: ImageVector?, trailingText: String?, showSwitch: Boolean, switchValue: Boolean, onSwitchChange: (Boolean) -> Unit?, badge: String?, …@ComposableListItemheadlineContentsupportingContentleadingContenttrailingContentoverlineContentListItemtitle: String, subtitle: String?, leadingIcon: ImageVector?, trailingIcon: ImageVector?, trailingText: String?, showSwitch: Boolean, switchValue: Boolean, onSwitchChange: (Boolean) -> Unit?, badge: String?, …@ComposableListItemheadlineContentsupportingContentleadingContenttrailingContentoverlineContentListItemtitle: Stringicon: ImageVectoractionText: String?subtitle: String?leadingIcon: ImageVector?trailingText: String?showChevron: BooleanshowSwitch: Booleanmode: Mode.Text | Mode.Switch | …StringTextTextBadgetrailingcontenttitle: Stringicon: ImageVectoractionText: String?subtitle: String?leadingIcon: ImageVector?trailingText: String?showChevron: BooleanshowSwitch: Booleanmode: Mode.Text | Mode.Switch | …StringTextBadgeTexttrailingcontent@Composable@Composable@Composable () -> Unitnull// ❌ 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,
) { … }titleleadingIconImageVectorBadgeImageVector// ✅ 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 () -> Unitnull// ❌ 不佳——原始类型参数;仅后置区域是插槽;其余内容均被固定
@Composable
fun SettingsRow(
title: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
subtitle: String? = null,
leadingIcon: ImageVector? = null,
trailing: (@Composable () -> Unit)? = null,
) { … }titleleadingIconImageVectorBadgeImageVector// ✅ 良好——每个视觉区域都是插槽;组件仅描述结构,不限制内容
@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 = { … },
)xxxContent@Composable () -> UnitheadlineContentsupportingContenttrailingContenttitleiconactionsScaffold(topBar = { … }, bottomBar = { … }, floatingActionButton = { … })contentxxxContent@Composable () -> UnitxxxContentheadlineContentsupportingContenttrailingContentScaffold(topBar = { … }, bottomBar = { … }, floatingActionButton = { … })topBarcontentxxxContentRowColumnBoxModifier.weightBoxScope.matchParentSize@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(…) })RowScopeBoxBoxScopeColumnColumnScopeRowColumnBoxModifier.weightBoxScope.matchParentSize@Composable RowScope.() -> Unit// ❌ 不佳——操作项在Row中渲染,但调用者无法使用RowScope.weight()
@Composable
fun MyTopBar(
title: @Composable () -> Unit,
actions: @Composable () -> Unit = {}, // ← 调用者无Row作用域
)// ✅ 良好——调用者拥有RowScope;可在内部使用.weight()和对齐方式
@Composable
fun MyTopBar(
title: @Composable () -> Unit,
actions: @Composable RowScope.() -> Unit = {},
)TopAppBar(actions = { IconButton(…); IconButton(…) })RowScopeBoxBoxScopeColumnColumnScopenullnull(@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)? = nullleadingContent != null{}null{}(@Composable () -> Unit)? = null@Composable () -> Unit = {}// ❌ 不佳——空默认值;“无前置内容”对应空lambda
leadingContent: @Composable () -> Unit = {}
// ✅ 良好——null表示“无插槽”;组件可在插槽为空时省略其容器、间距和内边距
leadingContent: (@Composable () -> Unit)? = nullleadingContent != null{}null{}XxxDefaultsXxxDefaultsMaterialTheme.colorScheme.surfaceXxxDefaultsobject 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 = { … },
)ButtonDefaultsTopAppBarDefaultsMaterialTheme.x.yMaterialTheme.colorScheme.surfaceXxxDefaultsobject 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 = { … },
)ButtonDefaultsTopAppBarDefaultsMaterialTheme.x.y| Symptom | Diagnosis | Fix |
|---|---|---|
| Primitive content params (§1) | Convert to |
Multiple boolean flags ( | Enumerating shapes (§1) | One |
A | Same as flag soup (§1) | Slot it |
| Missing scope receiver (§2) | |
| Empty-lambda default (§3) | |
Component param | Defaults inlined (§4) | Move to |
| Common trailing content repeats at every call site | Missing default helper (§4) | Add |
| 症状 | 诊断 | 修复方案 |
|---|---|---|
可重用组件包含 | 使用了原始内容参数(§1) | 转换为 |
多个布尔标志( | 枚举样式(§1) | 使用一个 |
包含 | 与标志混乱问题相同(§1) | 使用插槽 |
| 缺少作用域接收器(§2) | 修改为 |
可选区域使用 | 使用了空lambda默认值(§3) | 修改为 |
组件参数 | 默认值内联(§4) | 移至 |
| 常见后置内容在每个调用场景重复出现 | 缺少默认辅助方法(§4) | 添加 |
Heading2(text: String)headlineContent: @Composable () -> UnitHeading2Switch(checked: Boolean, onCheckedChange: ...)Heading2(text: String)headlineContent: @Composable () -> UnitHeading2Switch(checked: Boolean, onCheckedChange: ...)| Thought | Reality |
|---|---|
| "Title is always a String — making it a slot is over-engineering" | "Always today" is the trap. Material's |
| "Lambdas are heavier than strings" | At the scale of typical Compose UI, this isn't measurable — and the framework's own components ( |
| "I'll add a slot later if someone asks" | The slot turns one parameter into two parameters (the slot itself + maybe an internal flag) and edits every call site. The shape change isn't a "later" change. |
"I'll model the variants with a sealed | Sealed enumeration is bounded; slots are unbounded. A sealed type works until the day someone needs a variant you didn't anticipate — at which point you're back to editing the component. The slot avoids the cycle. |
| "The leading area is always an icon, the trailing area varies — I'll slot only the trailing" | This is the partial-slot trap. The "always-an-icon" assumption breaks the first time a row needs an avatar or a flag emoji or a coloured shape. Slot leading too. |
| "There's only one call site today" | If there's only one call site, you're probably not designing a reusable component yet. See "When NOT to apply" — primitives are fine for a true single-use. The moment you copy-paste it, slot it. |
| 想法 | 实际情况 |
|---|---|
| "标题永远是字符串——做成插槽是过度设计" | "现在永远"是陷阱。Material的 |
| "lambda比字符串更重" | 在典型的Compose UI规模下,这一差异无法被测量——且框架自身的组件( |
| "有人需要时我再添加插槽" | 添加插槽会将一个参数变为两个参数(插槽本身+可能的内部标志),并需要修改所有调用场景。这种结构变更不能留到“以后”。 |
"我用密封的 | 密封枚举是有界的;插槽是无界的。密封类型在你未预料到的变体出现前有效——此时你又要回到修改组件的循环中。插槽可以避免这种循环。 |
| "前置区域永远是图标,后置区域变化——我只给后置区域做插槽" | 这是部分插槽陷阱。“永远是图标”的假设会在第一次需要头像、国旗表情或彩色形状时被打破。前置区域也应做成插槽。 |
| "现在只有一个调用场景" | 如果只有一个调用场景,你可能还没在设计可重用组件。参考“何时不适用”——原始类型适用于真正的一次性组件。一旦你复制粘贴该组件,就改为插槽形式。 |
compose-modifier-and-layout-stylemodifiercompose-modifier-and-layout-stylemodifier