| 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.kt into 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,
onClick: () -> Unit,
modifier: Modifier = Modifier,
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(),
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.
@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.
@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.
Surface(modifier = Modifier.fillMaxWidth().then(modifier)) { ... }
-
Swallowed modifier. Param accepted but never applied — usually leftover from a refactor.
@Composable
internal fun Row(text: String, modifier: Modifier = Modifier) {
Surface(modifier = Modifier.fillMaxWidth()) { Text(text) }
}
Not subject to this rule
LazyListScope / LazyGridScope / BoxScope extension 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.
@Preview composables — they don't take parameters from external callers, so a modifier param adds no value. (They should still be private or internal.)
When to apply
- Authoring any new
@Composable function — drop in modifier: Modifier = Modifier from 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 = Modifier and apply it. Don't propagate the legacy shape when copy-pasting.
Auditing the codebase
Find non-conforming composables:
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.