The checks that run without you asking are the ones that scale

Compose ships lint rules for the modifier convention, remember misuse, unstable collections and more. Adding your own is how a design-system convention becomes a build failure instead of a code-review comment.

6 min read
androidcomposekotlintooling

Day 94 — The checks that run without you asking

Day 94 of 100, closing the tools pillar. Ninety-three days of conventions, and one mechanism that makes any of them stick.

The symptom

A convention that exists only in review comments:

"Can you take a modifier parameter here?" "This should be the first optional parameter." "That remember is missing a key." "Don't pass the NavController down."

The same four comments, on every pull request, from whichever reviewer noticed. New joiners learn them slowly and inconsistently. The reviewer who cares most becomes a bottleneck, and when they're on holiday the conventions lapse.

Why the obvious approach fails

The obvious approach is documentation — a style guide, a wiki page, an onboarding doc.

Documentation is a reminder for someone who already remembers to look. It doesn't run, so it can't fail a build, and its adherence decays quietly. Six months on, half the codebase follows it and nobody can say which half.

The other obvious approach — more careful review — makes a person the enforcement mechanism, which doesn't scale and doesn't survive turnover.

A convention in a document is advice; a convention in lint is a build failure

The actual mechanism

Compose ships lint checks that encode its own conventions. They run in the IDE as you type and in ./gradlew lint on CI:

ComposableNaming@Composable functions that return Unit should be PascalCase; ones that return a value should be camelCase. That's the OrderCard / rememberOrderState distinction from Day 17, enforced.

ModifierParameter — a composable emitting UI should take a modifier: Modifier = Modifier as its first optional parameter. Day 19's convention, checked.

UnrememberedMutableStatemutableStateOf not wrapped in remember, which resets every recomposition. One of the highest-value checks in the set, because the symptom (state that never changes) doesn't look like a missing remember.

FrequentlyChangedStateReadInComposition — reading something like a scroll offset during composition. Day 82's problem, caught at edit time.

AutoboxingStateCreationmutableStateOf(0) where mutableIntStateOf(0) avoids boxing. Day 10's note, automated.

CoroutineCreationDuringComposition — launching from a composable body rather than an effect or a callback. Day 9's rule.

ProduceStateDoesNotAssignValue, RememberReturnType, UnrememberedAnimatable — smaller, and each catches a specific real bug.

The list is worth reading once in full. Several encode things this series spent a post explaining, and having them as warnings means nobody has to remember the post. That is the honest summary of the tools pillar's value: a check is a piece of knowledge that doesn't need to be transmitted.

Turning warnings into failures

The default severity for most of these is a warning, which in a large codebase means invisible. Promoting the ones you care about is the step that changes behaviour:

android {
    lint {
        warningsAsErrors = false
        error += listOf(
            "UnrememberedMutableState",
            "CoroutineCreationDuringComposition",
            "ModifierParameter",
        )
        abortOnError = true
    }
}

Do this incrementally. Promoting everything at once on an existing codebase produces four hundred failures and a baseline.xml that suppresses them all — which is the same as not having done it.

The workable path: promote one rule, fix its existing violations in a dedicated change, then promote the next. Each one becomes permanently true rather than aspirationally true.

Slack's compose-lints

Worth knowing about, because it covers conventions the official set doesn't:

lintChecks("com.slack.lint.compose:compose-lint-checks:<version>")

It adds checks for things this series argued for: composables that shouldn't return values, mutable parameters, state hoisting violations, ViewModel instances forwarded into child composables, missing content descriptions, and the "don't pass a NavController down" rule from Day 91.

It's the closest thing to an off-the-shelf encoding of Compose community consensus, and adopting it is cheaper than writing the equivalent rules yourself.

Writing your own

For a convention specific to your codebase — "screens must use AppTheme", "no direct Color(0x…) outside the theme package", "our AppButton rather than Material's Button" — a custom rule is a day's work and permanent.

The shape, roughly:

class NoRawColorDetector : Detector(), SourceCodeScanner {
    override fun getApplicableMethodNames() = listOf("Color")

    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
        if (context.file.path.contains("/ui/theme/")) return
        context.report(
            ISSUE, node, context.getLocation(node),
            "Use a MaterialTheme colour role rather than a literal (Day 44)",
        )
    }
}

The judgment about which conventions deserve this: rules that are objective, mechanically checkable, and that a reviewer currently repeats. A rule requiring taste — "this component is doing too much" — isn't a lint rule, and trying to make it one produces false positives that teach people to suppress warnings.

The editor actions

The other half of the tooling, and less discussed:

Live templatescomp scaffolds a composable, prev a preview. Small, and they push people toward the conventional shape by default.

"Surround with" wrapping a selection in a Box, Row, Column or if, which is the most-used refactoring in Compose work.

"Extract composable" pulls a selection into a new function with its parameters inferred — the mechanical half of Day 7's "move the read down".

The gutter preview icon on any @Preview, and the "copy image" action on a rendered preview, which is the fastest way to get a screenshot into a pull request.

None is remarkable individually. Together they make the conventional path the low-effort one, which is the same argument as lint from the other direction. Lint raises the cost of the wrong shape; the editor actions lower the cost of the right one, and a team feels the second more than the first.

How to prove it

Run ./gradlew lint on an existing codebase and read the Compose-category findings. Most teams find UnrememberedMutableState or AutoboxingStateCreation hits they didn't know about — real bugs and real allocations that nobody's review caught.

Then pick the convention your team repeats most in review, check whether a rule exists for it, and promote it to error. The measurement is how many times that comment appears in the next month's pull requests.

The honest version of that measurement includes the reverse: count how often the rule fires on code that was actually fine. More than occasionally means the rule encodes taste rather than a fact, and a rule people routinely suppress is worse than none — it teaches the whole team that warnings are noise.

What this generalizes to

The tools pillar's conclusion: a convention that isn't enforced is a preference. A document describes what should happen; a lint rule decides whether the build passes. Only one of those survives turnover, deadline pressure and a reviewer on holiday.

Three days of tooling share a shape. A preview applies pressure toward previewable design (Day 92). The inspector makes invisible recomposition visible (Day 93). Lint makes a convention mechanical. In each case the tool's value isn't what it shows you once — it's the behaviour it makes cheap enough to sustain.

Which is the argument for spending a day on tooling setup at the start of a project rather than at the point of pain. A lint rule added on day one prevents a class of mistake for the project's whole life; the same rule added in year two comes with four hundred violations and a negotiation.

Tomorrow, Day 95 opens the final pillar: edge-to-edge and the system surfaces.


Day 94 of a 100-day series on Jetpack Compose, working through the official documentation in order. Source: Compose lint checks.