name: compose-modifier
description: Enforce the Compose guideline that every @Composable function must declare modifier: Modifier = Modifier as its first optional parameter and apply it to the root layout. Trigger when authoring or reviewing any @Composable function — including private / internal helpers extracted from a UI file.
Rule
Every @Composable function (public, internal, OR private) MUST declare a modifier: Modifier = Modifier parameter and pipe it into the outermost layout element.
Why
- Lets callers customise sizing, padding, click behavior, alignment, semantics, and test tags without forking the composable.
- Matches the official AndroidX Compose API guidelines so this codebase reads consistently with the rest of the ecosystem.
- Removes a footgun where you decompose a
*Ui.ktinto helpers and the helpers silently swallow caller intent.
Position
modifier is the first optional parameter: it follows all required ones and precedes the rest of the defaults.
@Composable
internal fun PodcastRow(
podcast: PodcastSearchResult, // required
onClick: () -> Unit, // required
modifier: Modifier = Modifier, // ← first default
shape: Shape = MaterialTheme.shapes.medium,
)
Apply to root, caller-first
Pipe modifier into the outermost layout. Chain local modifiers onto it (caller's modifications run first), don't .then(modifier) after locals.
@Composable
internal fun Section(
text: String,
modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier.fillMaxWidth(), // caller first, ours after
shape = MaterialTheme.shapes.medium,
) { Text(text) }
}
Anti-patterns
No modifier parameter at all. Most common offender — internal helpers like row composables, section headers, dialog bodies extracted from a screen UI.
// ❌ DON'T @Composable private fun ExplicitRow(enabled: Boolean, onToggle: (Boolean) -> Unit) { ... }Modifier not first among the defaults. Pushes future API additions to insert other defaults before it and breaks call-site stability.
// ❌ DON'T @Composable internal fun Row( text: String, shape: Shape = MaterialTheme.shapes.medium, modifier: Modifier = Modifier, )Local-first then
.then(modifier). Caller's intent is overridden by the composable's hardcoded modifiers.// ❌ DON'T Surface(modifier = Modifier.fillMaxWidth().then(modifier)) { ... }Swallowed modifier. Param accepted but never applied — usually leftover from a refactor.
// ❌ DON'T @Composable internal fun Row(text: String, modifier: Modifier = Modifier) { Surface(modifier = Modifier.fillMaxWidth()) { Text(text) } // <-- ignored modifier }
Not subject to this rule
LazyListScope/LazyGridScope/BoxScopeextension functions (e.g.LazyListScope.episodesSection(...),BoxScope.CenteredMessage(...)) — these aren't standalone composables; they emit items into an existing scope and have no single root layout to attach a modifier to. If you need per-item modification, plumb it explicitly through the children.- Slot / content lambdas typed
@Composable () -> Unit— those are caller-supplied content, not functions you declare. @Previewcomposables — they don't take parameters from external callers, so a modifier param adds no value. (They should still beprivateorinternal.)
When to apply
- Authoring any new
@Composablefunction — drop inmodifier: Modifier = Modifierfrom the start. - Reviewing a PR that introduces a Compose file or adds a new private helper composable inside one.
- Extracting a private composable out of a screen UI file. Tempting to skip the modifier param because "no one calls it except the file it came from" — don't. Tomorrow someone moves the file, or the screen needs to position the helper differently, or someone wants
Modifier.testTag(...)on it. Future-you spends 30 seconds adding the parameter back; future-you will not remember why. - Touching an existing helper that doesn't comply: opportunistically add
modifier: Modifier = Modifierand apply it. Don't propagate the legacy shape when copy-pasting.
Auditing the codebase
Find non-conforming composables:
# Composables with no parameter list mentioning Modifier
rg -P --multiline-dotall \
'@Composable\s*\n(?:internal |private )?fun [A-Z]\w+\s*\([^)]*\)' \
--type kt \
-g '!*/build/*' \
-g '!*Preview*' \
| rg -v 'modifier:\s*Modifier'
The add/composables/ and builder/composables/ directories under features/podcasts/ui/ carry the most violations as of the recent screen extractions — many helper rows, dialog bodies, and section composables were extracted without modifier params. Migrate as you touch them.