Blog Infos
Author
Published
Topics
, , , ,
Published

 

In my previous articles, I showed why using var or Array in a Kotlin data class constructor leads to unexpected behavior and subtle bugs.
These issues mostly relate to how equals()hashCode(), and copy() behave under the hood — especially in collections like HashMap or HashSet.

Those problems are related to logic and data modeling, and they often appear when working with collections or comparing objects.

But now, with the introduction of Jetpack Compose, there’s another important reason to avoid using var in your data class constructors:
it directly affects stability analysis and recomposition behavior at runtime.

In this article, we’ll walk through a minimal Compose example to show how a single var can make your data class unstable, prevent skipping recompositions, and reduce performance.
You’ll also see how a small change — replacing var with val — helps Compose optimize rendering and avoid unnecessary recompositions.

Enabling Compose Compiler Metrics

To understand how var affects recomposition in Jetpack Compose, we need visibility into what the Compose compiler sees at build time — specifically, whether classes and functions are marked as stableunstable, or skippable.

The Compose compiler provides diagnostic reports that can be enabled as part of the regular build process.

Relevant documentation:

What to enable

In your gradle.properties file:

androidx.enableComposeCompilerMetrics=true
androidx.enableComposeCompilerReports=true

In your build.gradle.kts (module-level):

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
    val buildDirProvider = project.layout.buildDirectory

    compilerOptions.freeCompilerArgs.addAll(
        "-P",
        "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
            buildDirProvider.get().asFile.absolutePath + "/compose_compiler"
    )
    compilerOptions.freeCompilerArgs.addAll(
        "-P",
        "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
            buildDirProvider.get().asFile.absolutePath + "/compose_compiler"
    )
}

After building the project, the reports will be available under:

app/build/compose_compiler/

The key files are:

  • *-classes.txt: stability information for your classes
  • *-composables.txt: skippability and recomposition classification of your Composables
  • *-composables.csv: structured summary of Composables and their characteristics

These reports help detect unnecessary recompositions, which can be especially costly in large or deeply nested Compose layouts.

Minimal model setup

Let’s consider a simple Compose screen that displays a user profile card. The card shows basic information such as name, email, and marital status. Another user can view the profile and submit a rating — for example, as feedback or to indicate interest.

To focus on stability and recomposition behavior, we’ll look only at the data model and the screen state:

data class UserProfile(
    val id: String,
    val name: String,
    val email: String,
    val avatarUrl: String,
    var isMarried: Boolean // mutable property
)

data class ProfileState(
    val profile: UserProfile,
    val rating: Int
)

This state is managed in a ViewModel and exposed to the UI via StateFlow.
When the user updates the rating, only the rating field is updated — the profile object remains unchanged.

However, because UserProfile includes a var property, the Compose compiler marks it as unstable, which directly affects how recomposition is handled.

Here’s what the screen looks like in the UI:

Compose compiler stability output

Once the app is built with Compose compiler metrics enabled, we can inspect the generated stability report.

The file app_debug-classes.txt clearly shows that the UserProfile class is marked as unstable. This is caused solely by the presence of a var property in the constructor:

With var:

As you can see:

  • UserProfile is unstable due to var isMarried
  • ProfileState, which contains UserProfile, is also marked as unstable
  • Any composable that receives ProfileState as input will be treated as non-skippable by Compose

To demonstrate this, here’s how the recomposition count looks in Layout Inspector after interacting with the rating control:

Even though the UserProfile object did not change, UserCard still recomposes on every rating update — because Compose cannot guarantee that the value is stable and hasn’t changed.

Now let’s replace var with val and rebuild the project.

With val:

data class UserProfile(
    val id: String,
    val name: String,
    val email: String,
    val avatarUrl: String,
    val isMarried: Boolean
)

The stability report now shows:

 

  • UserProfile is now marked as stable
  • ProfileState becomes stable as well

The result: Compose now treats any function using UserProfile or ProfileState as potentially skippable.

And indeed, the Layout Inspector confirms that UserCard is no longer recomposed unnecessarily:

Compose may skip recomposition despite instability

Even when a data class is marked as unstable, Jetpack Compose may still skip recomposition. This behavior might seem unexpected, but it has a clear technical explanation.

Compose relies on two mechanisms to decide whether a Composable needs to recompose:

  1. Stability analysis — performed at compile time; determines if a type is Stable or Unstable
  2. Reference equality check — performed at runtime; compares object instances using ===

When a parameter is marked as unstable, Compose disables structural equality checks.
This means Compose no longer compares the contents of the object — it assumes the data might have changed.

However, it still compares object references. If the exact same instance is passed again, recomposition may be skipped, even for an unstable type.

For example:

val state by viewModel.state.collectAsState()
UserCard(profile = state.profile)

 

If state.profile refers to the same UserProfile instance as before, Compose may skip recomposing UserCard.

This is a runtime optimization — and it only works if the object is reused as-is. As soon as the object is replaced or copied, recomposition will happen, because Compose can no longer assume it’s safe to skip.

Why relying on this is a bad idea

This kind of recomposition skipping is fragile:

  • It only works if the same object reference is preserved
  • It breaks as soon as the model is rebuilt or copied
  • It hides recomposition costs that may appear later during refactoring or feature growth

In other words:

Just because recomposition doesn’t happen today doesn’t mean your setup is correct. The underlying instability still limits Compose’s ability to optimize your UI safely.

For predictable and maintainable Compose code, it’s better to avoid mutable properties in the constructor and rely on stable models. This allows Compose to apply both structural and reference-based optimizations — reliably and by design.

 

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Kobweb:Creating websites in Kotlin leveraging Compose HTML

Kobweb is a Kotlin web framework that aims to make web development enjoyable by building on top of Compose HTML and drawing inspiration from Jetpack Compose.
Watch Video

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kobweb

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author o ...

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kob ...

Jobs

Conclusion

Using var in a Kotlin data class constructor has always been discouraged — mostly due to how it affects equals()hashCode(), and collection behavior.

With Jetpack Compose, this design choice introduces an additional concern.

Mutable constructor properties make the class unstable from Compose’s perspective.
This limits how efficiently Compose can track changes and optimize recompositions.

Although Compose may still skip recomposition in some cases — for example, when the same object instance is reused — relying on such behavior is fragile and not recommended.

What to do instead
  • Use val in data class constructors whenever possible
  • Keep your UI models immutable and predictable
  • If mutable state is needed, declare it outside the constructor

Stable models allow Compose to reason about your UI more effectively and apply optimizations safely.

 

Avoiding var in data class constructors is not just about code style — it’s also a way to write faster, more predictable Compose UIs.

 

You might also like:
Found this useful?

If this article helped you understand how var affects stability and recomposition in Jetpack Compose — consider leaving a clap. It helps others discover the article.

If you’re working with Kotlin and Compose, feel free to follow — more practical articles coming soon.

 

Anatolii Frolov
Senior Android Developer
Writing honest, real-world Kotlin & Jetpack Compose insights.
📬 Follow me on Medium

This article was previously published on proandroiddev.com.

Menu