Loading...
Loading...
Use when designing or reviewing a reusable Jetpack Compose component whose visual regions vary by caller, or when primitive content parameters and boolean shape flags are accumulating.
npx skill4agent add chrisbanes/skills compose-slot-api-patterntitle: 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 | …StringTextTextBadgetrailingcontent@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 = { … },
)xxxContent@Composable () -> UnitheadlineContentsupportingContenttrailingContenttitleiconactionsScaffold(topBar = { … }, bottomBar = { … }, floatingActionButton = { … })contentxxxContentRowColumnBoxModifier.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(…) })RowScopeBoxBoxScopeColumnColumnScopenull(@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{}XxxDefaultsMaterialTheme.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 |
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. |
compose-modifier-and-layout-stylemodifier