{"id":48225798,"url":"https://github.com/himattm/dejavu","last_synced_at":"2026-04-04T19:21:38.085Z","repository":{"id":341737770,"uuid":"1170076290","full_name":"himattm/dejavu","owner":"himattm","description":"A lightweight framework for tracking and asserting against Jetpack Compose recompositions.","archived":false,"fork":false,"pushed_at":"2026-03-26T13:49:54.000Z","size":1179,"stargazers_count":69,"open_issues_count":3,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-03-26T19:49:16.213Z","etag":null,"topics":["android","compose-testing","jetpack-compose","kotlin","recomposition"],"latest_commit_sha":null,"homepage":"https://dejavu.mmckenna.me/","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/himattm.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-01T17:03:14.000Z","updated_at":"2026-03-26T13:50:55.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/himattm/dejavu","commit_stats":null,"previous_names":["himattm/dejavu"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/himattm/dejavu","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/himattm%2Fdejavu","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/himattm%2Fdejavu/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/himattm%2Fdejavu/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/himattm%2Fdejavu/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/himattm","download_url":"https://codeload.github.com/himattm/dejavu/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/himattm%2Fdejavu/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31410210,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T10:20:44.708Z","status":"ssl_error","status_checked_at":"2026-04-04T10:20:06.846Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","compose-testing","jetpack-compose","kotlin","recomposition"],"created_at":"2026-04-04T19:21:37.475Z","updated_at":"2026-04-04T19:21:38.077Z","avatar_url":"https://github.com/himattm.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cpicture\u003e\n  \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"docs/assets/logo-dark.svg\"\u003e\n  \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"docs/assets/favicon.svg\"\u003e\n  \u003cimg src=\"docs/assets/favicon.svg\" width=\"40\" align=\"left\" style=\"margin-right: 12px;\"\u003e\n\u003c/picture\u003e\n\n# Dejavu\n\n*Wait... didn't we just compose this?*\n\n[![CI](https://github.com/himattm/dejavu/actions/workflows/ci.yml/badge.svg)](https://github.com/himattm/dejavu/actions/workflows/ci.yml)\n[![Maven Central](https://img.shields.io/maven-central/v/me.mmckenna.dejavu/dejavu)](https://central.sonatype.com/artifact/me.mmckenna.dejavu/dejavu)\n[![Compose](https://img.shields.io/badge/Compose-1.6.x–1.10.x-4285F4?logo=jetpackcompose\u0026logoColor=white)](https://developer.android.com/develop/ui/compose)\n[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n\n###### Featured In\n\u003ca href=\"https://jetc.dev/issues/305.html\"\u003e\u003cimg src=\"https://img.shields.io/badge/As_Seen_In-jetc.dev_Newsletter_Issue_%23305-blue?logo=Jetpack+Compose\u0026amp;logoColor=white\" alt=\"As Seen In - jetc.dev Newsletter Issue #305\"\u003e\u003c/a\u003e\n\n**[Full Documentation](https://dejavu.mmckenna.me)**\n\n**Guard your Compose UI efficiency. Catch recomposition regressions before your users.**\n\n## The Problem\n\nCompose'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:\n\n- **Layout Inspector** — manual, requires a running app, can't automate, can't run in CI\n- **Manual tracking code** — `SideEffect` counters, `LaunchedEffect` logging, wrapper composables; invasive, doesn't scale, and ships in your production code\n- Neither gives you a **testable, automatable contract** you can enforce on every PR\n\n## What Dejavu Does\n\nDejavu 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.\n\n- **Zero production code changes** — just `Modifier.testTag()`\n- **One-line test setup** — `createRecompositionTrackingRule()`\n- **Rich diagnostics** — source location, recomposition timeline, parameter diffs, causality analysis\n- **Per-instance tracking** — multiple instances of the same composable get independent counters\n\n## Quick Start\n\n### 1. Add dependency\n\n```kotlin\n// app/build.gradle.kts\ndependencies {\n    androidTestImplementation(\"me.mmckenna.dejavu:dejavu:0.3.0\")\n}\n```\n\n### 2. Write a test\n\n```kotlin\n@get:Rule\nval composeTestRule = createRecompositionTrackingRule()\n\n@Test\nfun incrementCounter_onlyValueRecomposes() {\n  composeTestRule.onNodeWithTag(\"inc_button\")\n    .performClick()\n    \n  composeTestRule.onNodeWithTag(\"counter_value\")\n    .assertRecompositions(exactly = 1)\n    \n  composeTestRule.onNodeWithTag(\"counter_title\")\n    .assertStable() // stable = zero recompositions\n}\n```\n\n`createRecompositionTrackingRule` wraps `createAndroidComposeRule` and resets counts before each test. For `createComposeRule()` or other rule types, see [Examples](https://dejavu.mmckenna.me/examples/).\n\n## What a Failure Looks Like\n\n```\ndejavu.UnexpectedRecompositionsError: Recomposition assertion failed for testTag='product_header'\n  Composable: demo.app.ui.ProductHeader (ProductList.kt:29)\n  Expected: exactly 0 recomposition(s)\n  Actual: 1 recomposition(s)\n\n  All tracked composables:\n    ProductListScreen = 1\n    ProductHeader    = 1  \u003c-- FAILED\n    ProductItem      = 1\n\n  Recomposition timeline:\n    #1 at +0ms — param slots changed: [1] | parent: ProductListScreen\n\n  Possible cause:\n    1 state change(s) of type Int\n    Parameter/parent change detected (dirty bits set)\n```\n\nSee [Error Messages Guide](https://dejavu.mmckenna.me/error-messages/) for how to read and act on each section.\n\n## Use Cases\n\n### Lock In Efficiency Gains\n\nWhen 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.\n\n### Give AI Agents a Recomposition Signal\n\nAI 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.\n\n### Guardrail Against Unexpected Changes\n\nWhen 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.\n\nSee the full [Use Cases](https://dejavu.mmckenna.me/use-cases/) guide for examples.\n\n## API Reference\n\n### Assertions\n\n```kotlin\n// Exact count\ncomposeTestRule.onNodeWithTag(\"tag\").assertRecompositions(exactly = 2)\n\n// Bounds\ncomposeTestRule.onNodeWithTag(\"tag\")\n  .assertRecompositions(atLeast = 1)\n\ncomposeTestRule.onNodeWithTag(\"tag\")\n  .assertRecompositions(atMost = 3)\n\ncomposeTestRule.onNodeWithTag(\"tag\")\n  .assertRecompositions(atLeast = 1, atMost = 5)\n\n// Stability (alias for exactly = 0)\ncomposeTestRule.onNodeWithTag(\"tag\")\n  .assertStable()\n```\n\n### Utilities\n\n```kotlin\n// Reset all counts to zero mid-test (Android)\ncomposeTestRule.resetRecompositionCounts()\n\n// Get the current recomposition count for a tag (Android)\nval count: Int = composeTestRule.getRecompositionCount(\"tag\")\n\n// Stream recomposition events to Logcat (filter: \"Dejavu\")\n// Useful for AI agents or external tools monitoring UI state\nDejavu.enable(app = this, logToLogcat = true)\n\n// Disable tracking and clear all data\nDejavu.disable()\n```\n\n## How It Works\n\nDejavu hooks into the Compose runtime's `CompositionTracer` API (available since compose-runtime 1.2.0):\n\n1. **Intercepts trace calls** — `Composer.setTracer()` receives callbacks for every composable enter/exit\n2. **Maps testTag to composable** — walks the `CompositionData` group tree to find which composable encloses each `Modifier.testTag()`\n3. **Counts recompositions** — maintains a thread-safe counter per composable, incrementing on recomposition (not initial composition)\n4. **Tracks causality** — `Snapshot.registerApplyObserver` detects state changes; dirty bits detect parameter-driven recompositions\n5. **Reports on failure** — assembles source location, timeline, tracked composables, and causality into a structured error\n\nAll tracking runs in the app process on the main thread, directly accessible to instrumented tests.\n\n## Compatibility\n\n**Minimum:** compose-runtime 1.2.0 (CompositionTracer API). Requires Kotlin 2.0+ with the Compose compiler plugin.\n\n| Compose BOM | Compose | Kotlin | Status |\n|---|---|---|---|\n| 2024.06.00 | 1.6.x | 2.0.x | Tested |\n| 2024.09.00 | 1.7.x | 2.0.x | Tested |\n| 2025.01.01 | 1.8.x | 2.0.x | Tested |\n| 2026.01.01 | 1.10.x | 2.1.x+ | Tested |\n| 2026.03.01 | 1.10.x | 2.1.x+ | Baseline |\n\n## Kotlin Multiplatform\n\nDejavu supports Kotlin Multiplatform with the following targets:\n\n| Target | Status | Notes |\n|--------|--------|-------|\n| Android | Full support | Tag mapping via `ui-tooling-data` Group tree |\n| Desktop (JVM) | Full support | Tag mapping via `CompositionGroup` + `sourceInfo` |\n| iOS (arm64, simulatorArm64, x64) | Supported | Same as JVM; `LazyVerticalGrid` has upstream Compose runtime bug |\n| WasmJs (browser) | Supported | Exception propagation limited in test runner |\n\n### KMP Test Setup\n\nFor non-Android platforms, use `runRecompositionTrackingUiTest` with `setTrackedContent`:\n\n```kotlin\n@Test\nfun myComposable_isStable() = runRecompositionTrackingUiTest {\n    setTrackedContent { MyComposable() }\n    waitForIdle()\n    onNodeWithTag(\"my_tag\").assertStable()\n}\n```\n\n`runRecompositionTrackingUiTest` is the KMP equivalent of Android's `createRecompositionTrackingRule()`.\nIt handles all Dejavu lifecycle management automatically -- enabling the tracer, resetting state,\nand cleaning up after each test. `setTrackedContent` wraps `setContent` with the inspection tables\nand sub-composition layout required for tag-to-function mapping.\n\n### Known Gaps\n\n- **iOS/WasmJs: `LazyVerticalGrid` crash** — The Compose runtime's internal slot table hash implementation crashes on iOS/Native and WasmJs when `LazyVerticalGrid` is in the composition. This is an upstream Compose bug, not a Dejavu issue. `LazyColumn`, `LazyRow`, and all other composables work correctly when `LazyVerticalGrid` is not present.\n- **WasmJs: Exception propagation** — The Wasm test runner intercepts exceptions at a higher level than the test code, preventing try-catch from capturing `AssertionError` messages. Assertion *behavior* (pass/fail) works correctly; only error message inspection is affected. Parameter validation exceptions (`IllegalArgumentException`) are caught correctly when structured inside `runComposeUiTest`.\n\n## Known Limitations\n\n- **Off-screen lazy items** — `LazyColumn`/`LazyRow` only 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.\n- **Activity-owned Recomposer clock** — `createAndroidComposeRule` uses the Activity's real `Recomposer`, not a test-controlled one. This means `mainClock.advanceTimeBy()` can't drive infinite animations forward. Use `createComposeRule` (without an Activity) if you need a controllable clock.\n- **Parameter change tracking precision** — parameter diffs use `Group.parameters` from the Compose tooling data API, which was designed for Layout Inspector rather than programmatic diffing. Parameter names may be unavailable, and values are compared via `hashCode`/`toString`, so custom types without meaningful `toString` show opaque values.\n\n## Further Reading\n\n- [Use Cases](https://dejavu.mmckenna.me/use-cases/) — locking in UI efficiency, AI agent guardrails, and CI enforcement\n- [Examples](https://dejavu.mmckenna.me/examples/) — test patterns for common scenarios\n- [Error Messages Guide](https://dejavu.mmckenna.me/error-messages/) — how to read and act on failure output\n- [Causality Analysis](https://dejavu.mmckenna.me/causality-analysis/) — understanding why composables recompose\n\n## Contributing\n\nWe welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for our community standards.\n\n## License\n\nApache 2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhimattm%2Fdejavu","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhimattm%2Fdejavu","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhimattm%2Fdejavu/lists"}