Catch Compose stability regressions before they reach production.
Jetpack Compose skips recomposing a composable only when all its parameters are stable. When they're not, Compose recomposes on every state change — causing UI jank, battery drain, and frame drops that users notice. These regressions are invisible in code review.
compose-guard integrates into your CI pipeline and fails the build when a PR introduces new unstable composables that weren't there before.
Your team is writing Compose code. Someone adds a data class with a List<> property, or
passes a ViewModel directly into a composable. The UI starts recomposing 40x per second
instead of 2x. Nobody notices until production. Users complain about lag.
Google's Compose Compiler already detects this — but it writes the results to a text file that nobody reads.
compose-guard makes stability a hard gate, like a lint rule that can't be ignored.
Add to your module's build.gradle.kts:
plugins {
id("io.github.composeguard") version "0.1.0"
}Enable Compose compiler metrics (required — add once to your build):
// build.gradle.kts
android {
kotlinOptions {
freeCompilerArgs += listOf(
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${layout.buildDirectory.get()}/compose_metrics",
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${layout.buildDirectory.get()}/compose_metrics"
)
}
}Step 1 — Generate baseline (run once, commit the file):
./gradlew generateComposeGuardBaseline
# → creates compose-guard-baseline.json in your project root
# → commit this file to your repositoryStep 2 — Check in CI (runs on every PR):
./gradlew checkComposeGuardWhen a PR introduces a stability regression, the build fails:
╔══════════════════════════════════════════════╗
║ ❌ COMPOSE GUARD FAILED ║
╚══════════════════════════════════════════════╝
1 new stability regression(s) detected:
HomeScreen → now UNSTABLE (was stable in baseline)
unstable param: viewModel: HomeViewModel
Fix: Add @Stable/@Immutable to unstable types, or wrap lambdas in remember { }
Run './gradlew generateComposeGuardBaseline' ONLY after intentional changes.
Never update the baseline just to make the check pass.
# .github/workflows/compose-guard.yml
name: Compose Guard
on:
pull_request:
branches: [ main, master ]
jobs:
stability-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Build with metrics
run: ./gradlew compileDebugKotlin
- name: Check Compose stability
run: ./gradlew checkComposeGuardcomposeGuard {
// Fail build on new unstable composables (default: true)
failOnNewUnstable.set(true)
// Baseline file location (default: "compose-guard-baseline.json")
baselineFile.set("compose-guard-baseline.json")
// Metrics directory (default: build/compose_metrics)
// metricsOutputDir.set(layout.buildDirectory.dir("compose_metrics"))
}- Compose compiler metrics — Google's Kotlin compiler plugin generates a
*-composables.txtfile listing every@Composableand whether it's skippable/stable - Baseline — compose-guard reads that file and saves a JSON snapshot of which composables are stable
- CI check — on every build, compares current state to baseline. If a previously-stable composable is now unstable → build fails with actionable error
| Without compose-guard | With compose-guard |
|---|---|
| Stability regressions discovered in production | Caught at PR review time |
| Manual investigation with Android Studio profiler | Instant diagnosis in CI output |
| "Why is the app laggy?" — nobody knows | Exact composable + parameter identified |
Issues and PRs welcome. See CONTRIBUTING.md.
Apache 2.0 — see LICENSE.