compose-modifier

star 115

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.

r0adkll By r0adkll schedule Updated 5/18/2026

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,             // 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 / 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:

# 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.

Install via CLI
npx skills add https://github.com/r0adkll/Campfire --skill compose-modifier
Repository Details
star Stars 115
call_split Forks 4
navigation Branch main
article Path SKILL.md
More from Creator