# Introducing Dejavu: Recomposition Testing for Jetpack Compose

## Where This Idea Came From

At Square, most of the app is built on [Workflow](https://github.com/square/workflow-kotlin). There is an internal testing framework that lets you assert exactly how many render passes are triggered for a given interaction which is then enforced by CI.

I led the team that optimized the Select Payment Method screen in the Square Point of Sale app. We traced through the render tree, cut unnecessary render passes, and **reduced overall latency by 30%!** Each render was doing extra work, so consolidating effort and lowering the render count directly contributed to better performance.

The bigger win was what happened *after*. We wrote tests that asserted the the new and improved render counts we'd achieved. If a future change caused extra renders to show up, CI would fail and our performance gains couldn't silently regress.

## The Problem with Compose

Workflows have renders. Compose has recompositions.

There's only a few ways to get insights into how Composables are performing.

*   [Layout Inspector](https://developer.android.com/develop/ui/compose/tooling/debug#recomposition-counts) requires being in an IDE¹, manually checking a running app, and it doesn't run in CI.
    
*   "`SideEffect` [guarantees that the effect executes after every successful recomposition](https://developer.android.com/develop/ui/compose/side-effects#sideeffect-publish)" and can be used to count them. But they litter production code with debugging infrastructure.
    

Neither gives you a testable contract enforceable on every PR.

## Introducing Dejavu

Dejavu turns recomposition behavior into test assertions. No production code changes beyond `Modifier.testTag()`. (which you are probably using already)

### Setup

```kotlin
// Create a Recomposition Tracking Rule
@get:Rule
val composeTestRule = createRecompositionTrackingRule()

@Test
fun incrementCounter_onlyValueRecomposes() {
  // Perform an action
  composeTestRule.onNodeWithTag("inc_button")
    .performClick()

  // Assert that Composables change like you expect
  composeTestRule.onNodeWithTag("counter_value")
    .assertRecompositions(exactly = 1)

  // Or assert that they remain stable
  composeTestRule.onNodeWithTag("counter_title")
    .assertStable() // asserts recompositions = 0
}
```

Same pattern as any Compose UI test.

*   Find a node by tag
    
*   Perform an action
    
*   Assert your expected recomposition count!
    

When a test fails, you get diagnostics that tell you *why*:

```plaintext
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 the [Error Messages Guide](https://dejavu.mmckenna.me/error-messages/) for more information.

### What Makes Dejavu Different

Dejavu hooks into [CompositionTracer](https://developer.android.com/reference/kotlin/androidx/compose/runtime/CompositionTracer) so there's no compiler plugin, bytecode manipulation, or Gradle plugin.

[Causality analysis](https://dejavu.mmckenna.me/causality-analysis/) is a best effort attempt to explain *why* we had a recomposition. It tracks `Snapshot` state changes and maps dirty bits back to parameter slots.

All Composable are tracked by default. `testTag` only required for the assertion API with `onNodeWithTag("x")`.

The tracer itself tracks every composable that gets traced via `CompositionTracer`, regardless of whether it has a `testTag`. The tag mapping just bridges between `testTag` and function name so assertions can find the right counter.

## An AI Agent's Blind Spot

AI agents are writing Compose code. They refactor screens, hoist state, extract components, and they're *mostly* good at it. There's currently no way for an Agent to deterministically know it negatively (or positively) affected recompositions.

We humans are relying on the model to get it right as the ever growing avalanche of PR reviews comes barreling towards us. UI tests could pass and look fine in the CI recordings, but users start leaving negative reviews about jank.

Dejavu gives you, your CI, and your Agents the ability to run UI tests and validate that there are no unexpected changes.

The error messages work for both audiences. A human or an agent can read the failure and knows what to fix. And if an agent regresses recomposition CI fails and the PR doesn't merge.

You can also stream events in real time with `Dejavu.enable(app, logToLogcat = true)`. An agent running `adb logcat -s Dejavu` gets a live feed of per-instance composition state while iterating on a running app.

## Get Started

```kotlin
androidTestImplementation("me.mmckenna.dejavu:dejavu:0.1.2")
```

> [Full documentation](https://dejavu.mmckenna.me) · [Getting Started](https://dejavu.mmckenna.me/getting-started/) · [Examples](https://dejavu.mmckenna.me/examples/) · [API Reference](https://dejavu.mmckenna.me/api-reference/) · [GitHub](https://github.com/himattm/dejavu)

If you've ever stared at Layout Inspector wondering why a composable recomposed, or wished you could just *assert* on it in a test, give this a try and please let me know how it goes!

## Whats next?

Lots of real testing! I added a pretty expansive test suite to measure correctness, but Compose is so expressive and adaptable I'm sure there are things I missed. If you find bugs I'd love to know what they are.

* * *

## Footnotes

1.  At Square we were beginning to have weekly discussions about the value of an IDE in today's AI enabled world. Whats your take?
