Wait... didn't we just compose this?
More newsletter coverage, community articles and integration examples
Guard your Compose UI efficiency. Catch recomposition regressions before your users.
Compose's recomposition behavior is an implicit contract — composables should recompose when their inputs change and stay stable otherwise. But that contract breaks silently, and today's options for catching it are limited:
- Layout Inspector — manual, requires a running app, can't automate, can't run in CI
- Manual tracking code —
SideEffectcounters,LaunchedEffectlogging, wrapper composables; invasive, doesn't scale, and ships in your production code - Neither gives you a testable, automatable contract you can enforce on every PR
Dejavu is a test-only library that turns recomposition behavior into assertions. Tag your composables with standard Modifier.testTag(), write expectations against recomposition counts, and get structured diagnostics when something changes — whether from a teammate, a library upgrade, an AI agent rewriting your UI code, or a refactor that silently destabilizes a lambda.
- Zero production code changes — just
Modifier.testTag() - One-line test setup —
createRecompositionTrackingRule() - Rich diagnostics — source location, recomposition timeline, parameter diffs, causality analysis
- Per-instance tracking — multiple instances of the same composable get independent counters
DejaVu 0.5.0 requires Android compile SDK 37 and min SDK 24. The release uses Compose Multiplatform 1.12.0; Android Compose 1.11 remains supported with an enforced BOM. See Compatibility before adding the dependency to an older Compose project.
// app/build.gradle.kts
dependencies {
androidTestImplementation("me.mmckenna.dejavu:dejavu:0.5.0")
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}Use your enforced Compose BOM for the unversioned Compose test artifacts. See the complete setup for the runner, compatible Android test dependencies, and KMP source sets.
@get:Rule
val composeTestRule = createRecompositionTrackingRule()
@Test
fun incrementCounter_onlyValueRecomposes() {
composeTestRule.onNodeWithTag("inc_button")
.performClick()
composeTestRule.onNodeWithTag("counter_value")
.assertRecompositions(exactly = 1)
composeTestRule.onNodeWithTag("counter_title")
.assertStable() // stable = zero recompositions
}This test assumes the counter screen is already set. Use createRecompositionTrackingRule<YourActivity>()
to launch an activity that sets that screen, or call composeTestRule.setContent { YourScreen() }
when using the plain rule. The Getting Started guide
includes a complete example.
dejavu.UnexpectedRecompositionsError: Recomposition assertion failed for testTag='product_header'
Composable: demo.app.ui.ProductHeader (ProductList.kt:29)
Expected: exactly 0 recomposition(s)
Actual: 1 recomposition(s)
All tracked composables:
ProductListScreen = 1
ProductHeader = 1 <-- FAILED
ProductItem = 1
Recomposition timeline:
#1 at +0ms — param slots changed: [1] | parent: ProductListScreen
Possible cause:
1 state change(s) of type Int
Parameter/parent change detected (dirty bits set)
See Error Messages Guide for how to read and act on each section.
Dejavu ships four portable Agent Skills for skills-compatible coding agents, including Codex, Claude Code, Cursor, GitHub Copilot and OpenCode:
dejavu-onboarding— add Dejavu to a project from scratch (gradle dependency, firstModifier.testTag, smallest possible passing test).dejavu-test-writer— author Compose UI recomposition tests using Dejavu's APIs (Android JUnit4 or KMPcommonTest).dejavu-error-triage— diagnose a failure using source evidence and distinguish real regressions from expected accuracy-test failures.dejavu-perf-loop— measure and reduce unnecessary application recompositions while preserving UI behavior and the agreed budget.
From your app's repository, use the Skills CLI to choose skills and target agents (requires Node.js/npm):
npx skills add mttmcknn/dejavuFor example, install all four for Codex and Cursor in that project:
npx skills add mttmcknn/dejavu --skill '*' --agent codex cursorAdd --global for installation across projects, or --copy for environments without symlink support. Other agents can consume the same complete skill folders; see the installation guide for manual installation, updates and verification.
Claude Code's existing marketplace installation also remains available:
/plugin marketplace add mttmcknn/dejavu
/plugin install dejavu@dejavu
The canonical bundles are real directories in skills/. In this checkout, .agents/skills/ and .claude/skills/ link to them for discovery. Each folder includes its required references and works outside this repository. Choose one installation method per agent to avoid duplicate skill entries.
Skill bundle 0.3.0 is versioned independently from the library. See the skill audit and evaluation guide for offline checks, model comparisons and evidence limits.
When you optimize a composable — extracting a lambda, adding remember, switching to derivedStateOf — Dejavu lets you write a test that captures the expected recomposition count. That improvement becomes part of your test suite: refactors, dependency upgrades, and new features all have to maintain it or explicitly update the expectation.
AI coding agents can refactor composables and restructure state, but they have no way to know whether their changes made recomposition better or worse. Dejavu gives them that signal. When an agent runs your tests and a Dejavu assertion fails, the structured error message tells it exactly which composable regressed, by how much, and why — turning recomposition count into an optimization metric the agent can target directly.
The bundled dejavu-test-writer and dejavu-perf-loop skills (see Agent Skills above) teach compatible agents how to author Dejavu tests and run an iterative perf-optimization loop without re-deriving the API from docs.
When AI agents or automated tooling modify your codebase, they can introduce subtle changes to recomposition behavior without touching any visible UI. Dejavu tests act as guardrails — if an agent's changes cause a composable to recompose more than expected, the test fails before the change is merged. You get the speed of automated refactoring with the confidence that recomposition behavior is preserved.
See the full Use Cases guide for examples.
// Exact count
composeTestRule.onNodeWithTag("tag").assertRecompositions(exactly = 2)
// Bounds
composeTestRule.onNodeWithTag("tag")
.assertRecompositions(atLeast = 1)
composeTestRule.onNodeWithTag("tag")
.assertRecompositions(atMost = 3)
composeTestRule.onNodeWithTag("tag")
.assertRecompositions(atLeast = 1, atMost = 5)
// Stability (alias for exactly = 0)
composeTestRule.onNodeWithTag("tag")
.assertStable()// Reset all counts to zero mid-test (Android)
composeTestRule.resetRecompositionCounts()
// Reset all counts to zero mid-test (KMP: JVM, iOS, WasmJs)
// Resets recomposition counts while preserving composition history.
// Use inside runRecompositionTrackingUiTest after initial composition,
// before the interaction whose budget is being asserted.
resetRecompositionCounts()
// Get the current recomposition count for a tag (Android)
val count: Int = composeTestRule.getRecompositionCount("tag")
// Stream recomposition events to Logcat (filter: "Dejavu")
// Useful for AI agents or external tools monitoring UI state
Dejavu.enable(app = this, logToLogcat = true)
// Disable tracking and clear all data
Dejavu.disable()Dejavu hooks into the Compose runtime's CompositionTracer API (available since compose-runtime 1.2.0):
- Intercepts trace calls —
Composer.setTracer()receives callbacks for every composable enter/exit - Maps testTag to composable — walks the
CompositionDatagroup tree to find which composable encloses eachModifier.testTag() - Counts recompositions — maintains a thread-safe counter per composable, incrementing on recomposition (not initial composition)
- Tracks causality —
Snapshot.registerApplyObserverdetects state changes; dirty bits detect parameter-driven recompositions - Reports on failure — assembles source location, timeline, tracked composables, and causality into a structured error
On Android, tracking runs in the app process and is accessible to instrumented tests. JVM, iOS,
and Wasm tests use the shared tracer through runRecompositionTrackingUiTest.
Supported Compose range for Dejavu 0.5.x: 1.11.x–1.12.x (BOM 2026.05.00 through 2026.08.00).
Minimum supported Compose: 1.11 (BOM 2026.05.00). Dejavu 0.5.x uses the Compose testing v2 APIs introduced with this line. For Compose 1.10, use Dejavu 0.3.1; that maintenance release preserves the older Compose line instead of allowing newer transitive artifacts to mask an unsupported combination. Validated with Kotlin 2.4.0 and its Compose compiler plugin.
Dejavu 0.5.x is built and released against Compose Multiplatform 1.12.0. Android consumers use the release BOM by default. To keep an older validated Android Compose line, enforce that BOM in both app and instrumentation-test dependencies; otherwise Gradle may select DejaVu's newer transitive baseline. Android consumers of 0.5.0 must use compile SDK 37, as declared in the AAR metadata. Use DejaVu 0.4.0 for the Compose Multiplatform 1.11 / compile SDK 36 release baseline.
val composeBom = enforcedPlatform("androidx.compose:compose-bom:2026.06.01")
implementation(composeBom)
androidTestImplementation(composeBom)| Compose BOM | Compose | Kotlin tested | Status |
|---|---|---|---|
| 2026.05.00 | 1.11.x | 2.4.0 | Minimum |
| 2026.06.01 | 1.11.x | 2.4.0 | Latest 1.11 checkpoint |
| 2026.08.00 | 1.12.x | 2.4.0 | Release baseline |
Dejavu supports Kotlin Multiplatform with the following targets:
| Target | Status | Notes |
|---|---|---|
| Android | Full support | Tag mapping via ui-tooling-data Group tree |
| Desktop (JVM) | Full support | Tag mapping via CompositionGroup + sourceInfo |
| iOS (arm64, simulatorArm64) | Supported | Same as JVM; Compose Multiplatform 1.11 no longer supports iosX64 |
| WasmJs (browser) | Supported | Async result and diagnostic-message regressions verified |
For non-Android platforms, use runRecompositionTrackingUiTest with setTrackedContent:
@OptIn(androidx.compose.ui.test.ExperimentalTestApi::class)
@Test
fun myComposable_isStable() = runRecompositionTrackingUiTest {
setTrackedContent { MyComposable() }
waitForIdle()
onNodeWithTag("my_tag").assertStable()
}runRecompositionTrackingUiTest is the KMP equivalent of Android's createRecompositionTrackingRule().
Return its result directly from your test so the Wasm runner waits for completion. The test body
can suspend; Dejavu keeps tracking enabled until the body finishes and then cleans up.
It handles all Dejavu lifecycle management automatically -- enabling the tracer and resetting state. setTrackedContent wraps setContent with the inspection tables
and sub-composition layout required for tag-to-function mapping.
Dejavu is validated against Compose 1.11's new composables and runtime paths via the
compose-experimental module — a staging area for recomposition coverage of experimental /
newest-Compose APIs before they graduate into the core accuracy suite. It exercises recomposition
tracking on JVM, iOS, Wasm, and Android instrumented; Android runs every supported BOM for:
- the experimental non-lazy
GridandFlexBoxlayouts, derivedMediaQuery/mediaQueryadaptive breakpoints,- the Styles API (
androidx.compose.foundation.style), movableContentOf, and- the experimental LinkBuffer composer runtime path (
ComposeRuntimeFlags.isLinkBufferComposerEnabled).
The Compose 1.12 baseline additionally validates keyed SideEffect, shrinking vararg effect and
remember keys, assertions using runWithoutImplicitWait, and nested movable content under
LinkBuffer. Exact counters continue using unkeyed SideEffect, including a deliberately inefficient
fixture whose keyed callback stays quiet during four recompositions. The experimental suite runs
26 tests on 1.12 and retains 20 on the supported Android 1.11 checkpoints.
DejavuComposeTestRule delegates the new hasPendingWork and runWithoutImplicitWait methods on
Compose 1.12. These methods require 1.12; the existing rule API remains covered on Android 1.11.
- Off-screen lazy items —
LazyColumn/LazyRowonly compose items that are visible. Items that haven't been composed don't exist in the composition tree, so Dejavu has nothing to track. Scroll them into view before asserting. - Non-Android instance diagnostics — unresolved tags can share a function-level count when multiple instances use the same composable. Android has the most complete per-instance diagnostics.
- Activity-owned Recomposer clock —
createAndroidComposeRuleuses the Activity's realRecomposer, not a test-controlled one. This meansmainClock.advanceTimeBy()can't drive infinite animations forward. UsecreateComposeRule(without an Activity) if you need a controllable clock. - Parameter change tracking precision — parameter diffs use
Group.parametersfrom the Compose tooling data API, which was designed for Layout Inspector rather than programmatic diffing. Parameter names may be unavailable, and values are compared viahashCode/toString, so custom types without meaningfultoStringshow opaque values.
Thanks to the authors and editors who have shared DejaVu:
- JetC.dev: #305 introduced the library, #306 featured the launch article, and #315 highlighted James Cullimore's experience using it.
- Android Weekly: #718 included the launch article and library listing; #728 featured Cullimore's follow-up article.
- James Cullimore: Dejavu, Compose, And The Difference Between Performance Wins And Guardrails describes adoption in a real Android app, recomposition regression protection, and an Android integration bug he helped resolve.
- AboutLibraries: Mike Penz's DejaVu integration commit adds recomposition stability tests for library lists and rows. This historical example uses DejaVu 0.3.1; follow the current setup guide for new integrations.
- Use Cases — locking in UI efficiency, AI agent guardrails, and CI enforcement
- Examples — test patterns for common scenarios
- Error Messages Guide — how to read and act on failure output
- Causality Analysis — understanding why composables recompose
We welcome contributions! Please see CONTRIBUTING.md for guidelines and CODE_OF_CONDUCT.md for our community standards.
Apache 2.0