
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
.
- Avoid using
Array in the data class constructor in Kotlin
- Avoid using
var in the data class constructor in Kotlin
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 stable, unstable, or skippable.
The Compose compiler provides diagnostic reports that can be enabled as part of the regular build process.
Relevant documentation:
- JetBrains: Compose Compiler Metrics
- Android Developers: Diagnose recompositions using compiler reports
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
isunstable
due tovar isMarried
ProfileState
, which containsUserProfile
, is also marked asunstable
- 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 asstable
ProfileState
becomesstable
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:
- Stability analysis — performed at compile time; determines if a type is
Stable
orUnstable
- 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
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.