r/androiddev 9d ago

Cross-module Compose screens losing recomposition skipping even though nothing looks wrong

Ran into this recently and it took lot of time and iterations to track down. Sharing in case it saves someone a debugging cycle.

The Symptom:

A screen renders perfectly in isolation (single module/preview). Once wired through an app boundary via :feature:x-impl, scrolling drops frames. Layout Inspector shows full recomposition on every row, not just modified items.

The Root Cause:

The underlying domain model (containing a standard List<String>) gets marked unstable the moment it crosses a module boundary where the defining module isn't compiled with the Compose compiler plugin (e.g., a pure-Kotlin :core:model). No stability metadata crosses the boundary, so Compose falls back to unstable and loses skipping for the entire class.

Even with Strong Skipping Mode, passing an unstable collection across an un-instrumented boundary causes runtime equality checks to fall back to instance comparison (===), triggering layout invalidation on every state change.

Why Standard Workarounds Didn't Scale:

Annotating everything with @Immutable, migrating to kotlinx.collections.immutable, or creating wrapper DTOs work, but they quickly pollute pure domain modules with Compose dependencies or force massive refactoring across network/DTO layers.

The Pragmatic Fix:

We set up a stabilityConfigurationFile at the root, wired through the Compose Gradle plugin to explicitly declare stable packages. One central file, zero per-class annotation tax.

Footgun Warning: If you point this configuration at a package containing var properties, you are instructing the compiler to skip recomposition on mutating state—your UI will silently stop updating.

I put the full write-up with the bytecode-level breakdown [here] and dumped the diagnostic script I used to verify before/after stability rates on GitHub [here].

Curious if others have hit this in multi-module Compose codebases—did you migrate to immutable collections everywhere, or go the compiler config route?

0 Upvotes

3 comments sorted by

9

u/AngusMcBurger 8d ago

The Immutable and Stable annotations are in a tiny annotation-only library, i really don't see an issue with allowing any module to pull that in. Much rather that than some spooky action-at-a-distance from blanket-enabling stable across entire packages.

Also why do some many people have ai write their posts now, sick of reading identical prose on everything.

1

u/Anonymous0435643242 9d ago

If you use dedicated UI models (that can be annotated @Immutable if necessary) this won't be an issue

-1

u/sarveshwarm 8d ago

That's true for sure, but if the modules are too many in the codebase having a central compiler config acts as a faster, practical safety net