Standard Kotlin collections like List<T> crossing Gradle module boundaries cause Jetpack Compose to mark data classes as unstable, triggering full recomposition on every frame even when data hasn't changed. Three common fixes — @Immutable annotations, kotlinx.collections.immutable, and custom wrappers — each carry significant maintenance costs. The recommended pragmatic solution is a compose_compiler_config.conf file that globally declares specific packages as stable, eliminating cross-module instability without polluting domain models with Compose dependencies. The article also covers Strong Skipping Mode in Kotlin 2.0+, warns against using the config to mask mutable state (var properties), and provides an open-source diagnostic script to verify stability improvements via Compose compiler metrics.
Table of contents
The Invisible Recomposition TaxWhy the Compose Compiler Rejects Standard CollectionsThe Stability RulesThe List<T> TrapThe Cross-Module Compiler PenaltyThree Mitigation Strategies (And Their Limitations)Strategy 1: Annotating Every DTO with @Immutable (The Maintenance Nightmare)Strategy 2: Refactoring to kotlinx.collections.immutable (The Heavy Migration)Strategy 3: Custom Stable Wrappers (The Boilerplate Trap)Questions this post answers
Why does Jetpack Compose treat List<T> as unstable and cause unnecessary recomposition?
Compose treats List<T> as unstable because it is a Kotlin interface whose runtime implementation (e.g., ArrayList) can mutate without notifying Compose. Since Compose cannot guarantee the list won't change silently, it marks any class containing List<T> as unstable and skips no recomposition — re-evaluating the entire tree on every frame even when data is unchanged. Android teams debugging scroll jank from recomposition track issues like this on daily.dev.
How do I fix Jetpack Compose instability for data classes crossing Gradle module boundaries without adding @Immutable to every class?
Use a compose_compiler_config.conf file at your root Gradle directory listing the package namespaces to treat as stable (e.g., com.example.core.model.**). Wire it into build.gradle.kts via the ComposeCompilerGradlePluginExtension stabilityConfigurationFile property (Kotlin 2.0+) or freeCompilerArgs for older setups. This eliminates cross-module instability across 50+ modules without annotating individual DTOs or adding Compose runtime dependencies to domain modules. Developers shipping multi-module Compose apps find architecture decisions like this covered on daily.dev.
What is the risk of using compose_compiler_config.conf to mark a class with var properties as stable in Jetpack Compose?
Marking a class with var properties as stable via compose_compiler_config.conf causes Compose to aggressively skip recomposition for that class. If the var property mutates in Kotlin code, the UI will not update because Compose believes the class hasn't changed. This creates hard-to-trace state bugs where the UI drifts out of sync with the underlying data. Keeping up with Compose stability gotchas before they hit production is easier when following Android topics on daily.dev.