Loading...
Loading...
Use when writing or reviewing Jetpack Compose layout APIs, modifier parameters, modifier chain construction, hardcoded root layout decisions, or layout wrappers around a single conditional.
npx skill4agent add chrisbanes/skills compose-modifier-and-layout-stylemodifier.fillMaxWidth()ifif@Composable funBoxColumnRowLazyColumnTextImageSurfaceCardLayout { … }compose.foundation.layoutcompose.material*modifier.fillMaxWidth().padding(...)var m = Modifierm = m.padding(…)m = m.background(…)modifier = …Layout { if (cond) Content() }modifiermodifierModifiermodifiermodmwrapperModifier// ❌ BAD — no modifier param; caller can't position, size, or constrain this
@Composable
fun HomeScreenHeader(title: String, subtitle: String) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(title, style = MaterialTheme.typography.headlineLarge)
Text(subtitle, style = MaterialTheme.typography.bodyMedium)
}
}// ✅ GOOD — parent decides width and padding; the composable describes structure only
@Composable
fun HomeScreenHeader(
title: String,
subtitle: String,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(title, style = MaterialTheme.typography.headlineLarge)
Text(subtitle, style = MaterialTheme.typography.bodyMedium)
}
}HomeScreenHeader(title, subtitle, Modifier.fillMaxWidth().padding(horizontal = 16.dp))modifier// ❌ BAD — modifier accepted but never applied
@Composable
fun Avatar(url: String, modifier: Modifier = Modifier) {
Image(painter = rememberAsyncImagePainter(url), contentDescription = null)
}
// ❌ BAD — applied to a child, not the root; caller's size/position changes don't take
@Composable
fun Avatar(url: String, modifier: Modifier = Modifier) {
Box {
Image(
painter = rememberAsyncImagePainter(url),
contentDescription = null,
modifier = modifier,
)
}
}
// ❌ BAD — caller's modifier ends up last, so the composable's own size wins
@Composable
fun Avatar(url: String, modifier: Modifier = Modifier) {
Image(
painter = rememberAsyncImagePainter(url),
contentDescription = null,
modifier = Modifier
.clip(CircleShape)
.size(48.dp)
.then(modifier),
)
}// ✅ GOOD — caller's modifier first, then the composable's intrinsic chain
@Composable
fun Avatar(url: String, modifier: Modifier = Modifier) {
Image(
painter = rememberAsyncImagePainter(url),
contentDescription = null,
modifier = modifier
.clip(CircleShape)
.size(48.dp),
)
}.size(...).padding(...).fillMaxWidth().padding(horizontal = 16.dp).height(56.dp)// ❌ BAD — every caller now fills max width whether they want to or not
@Composable
fun PrimaryButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
Button(
onClick = onClick,
modifier = modifier.fillMaxWidth(), // ← hardcoded
) { Text(text) }
}
// ✅ GOOD — caller adds .fillMaxWidth() if (and only if) they want it
@Composable
fun PrimaryButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
Button(onClick = onClick, modifier = modifier) { Text(text) }
}Avatar.clip(CircleShape).size(48.dp)clip(CircleShape)var modifier =// ❌ BAD — visual flow broken into reassignments; `var` invites more mutation
@Composable
fun Demo() {
var m = Modifier
m = m.padding(16.dp)
m = m.fillMaxSize()
Box(m) { }
}
// ❌ ALSO BAD — same shape, dressed up with .then()
@Composable
fun Demo() {
var m = Modifier
m = m.padding(16.dp)
m = m.then(Modifier.fillMaxSize())
Box(m) { }
}// ✅ GOOD
@Composable
fun Demo() {
val m = Modifier
.padding(16.dp)
.fillMaxSize()
Box(m) { }
}valvarvarval// ✅ GOOD — short chain inline
Box(modifier = Modifier.fillMaxWidth()) { … }
Box(modifier = Modifier.padding(8.dp).background(Color.Red)) { … }var// ✅ GOOD — conditional inside the chain, still one expression
Box(
modifier = Modifier
.fillMaxWidth()
.then(if (selected) Modifier.background(Color.Red) else Modifier),
)Modifier.thenmodifier// ❌ BAD — three+ calls on one line; hard to scan
Box(
modifier = modifier.fillMaxSize().padding(16.dp).weight(1f),
)
// ✅ GOOD
Box(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
.weight(1f),
)valmodifierifif// ❌ BAD — Column always emitted; only its inner content is conditional
@Composable
fun A() {
Column {
if (showHeader) {
Text("Title")
Text("Subtitle")
}
}
}
// ✅ GOOD — Column only exists when it has content
@Composable
fun A() {
if (showHeader) {
Column {
Text("Title")
Text("Subtitle")
}
}
}modifiercontentAlignmenthorizontalArrangementverticalAlignment// ✅ KEEP AS-IS — modifier on the container is doing visible work
@Composable
fun A(modifier: Modifier = Modifier) {
Box(modifier = modifier) {
if (something) {
Text("Bleh1")
Text("Bleh2")
}
}
}ififif … else …// ✅ KEEP AS-IS — both branches contribute to the layout
Box {
if (something) Text("Hint") else innerTextField()
}| Symptom | Diagnosis | Fix |
|---|---|---|
| No | Add |
| Param ignored (§2) | Apply to root layout's |
| Wrong target (§2) | Move to the outermost layout's |
| Caller's modifier last (§2) | Reorder: |
| Layout hardcoded (§3) | Remove the hardcoded calls; let callers add them |
Sibling composables in the file don't have | Spreading anti-pattern | Fix this one; fix siblings opportunistically |
| Wrong name (§1) | Rename to exactly |
| Stepwise modifier construction (§4) | One fluent chain on a |
| Same shape via | Collapse |
| Modifier branch needs a condition | Reaching for | |
| Long chain not formatted (§5) | One call per line, indented under the value |
| Hoist (§6) | Move the |
| Layout carries semantics — leave (§6 carve-out) | Keep as-is |
| Both branches contribute — leave (§6 carve-out) | Keep as-is |
@Composable fun computeColor(): Color@Composable @ReadOnlyComposablemodifier@ReadOnlyComposablecompose-state-authoring@Previewmodifier*TestcomposeTestRule.setContent { … }modifiermodifierAnimatable| Thought | Reality |
|---|---|
"This composable is internal-only — adding | The parameter is eight characters and a default. It's not over-engineering; it's the convention. Skipping it is the over-engineering — it's a custom decision against the grain of every Compose API. |
| "It's only used in one place, so I know the layout requirements" | "Only used in one place" describes today. The cost of the parameter is paid once; the cost of refactoring callers when the second use site appears is paid per caller. |
"The sibling composables in this file don't have | Spreading an anti-pattern isn't matching style. Fix this one. Fix the siblings opportunistically. |
"The parent always wants | Then the parent passes |
| "I'll add it when someone needs it" | You're someone. You need it now (for the convention). The next caller won't add it either — they'll work around its absence. |
| "It's a tiny composable — the modifier param is noise" | The param is eight characters at the declaration and zero characters at any call site that doesn't need it. The "noise" is imagined. |
"I added | Then the not-home-screen caller can't unset it. Move the |
"I need | A conditional segment is |
| "Three lines is too few to make multiline" | Three chained calls is the threshold. Below three, one line. At or above three, multiline. |
| "The Column adds nothing but I'll keep it for symmetry" | Then hoist the conditional and keep the Column inside the consequent — symmetry preserved, no always-on container. |
"I'll put the | "Already exists" is the bug. The layout shouldn't exist when the condition is false. |
compose-slot-api-pattern@Composable () -> Unitmodifier