
Did you know that almost all ViewModels are unstable?
When we first learn Compose,
we’re taught to use a stable class rather than an unstable class.
But ViewModels are unstable.
So why is it that no one says anything when we use unstable ViewModels?
How does Compose determine stability?
It is necessary to understand how the Compose compiler determines stability.
A class is considered stable if all its members are immutable or
types that are traceable by the Compose runtime.
data class Stable(
val a: Int, // Stable
val b: MutableState<Int>, // Stable, Traceable in the Compose runtime
val c: SnapshotStateList<Int> // Stable, Traceable in the Compose runtime
)
data class Unstable(
var b: Int // Unstable
)
data class Runtime(
val i: Interface // Uncertain, compose compiler don't know which implementation will be used at runtime.
)
interface Interface {
data object Impl : Interface
}
We can expect the compiler to determine stability as follows.
stable class Stable {
stable val a: Int
stable val b: MutableState<Int>
stable val c: SnapshotStateList<Int>
<runtime stability> = Stable
}
unstable class Unstable {
stable var b: Int
<runtime stability> = Unstable
}
runtime class Runtime {
runtime val i: Interface
<runtime stability> = Uncertain(Interface)
}
However, this determination applies only within the module where the Compose compiler plugin is declared.
When Stability Crosses Module Boundaries
I will demonstrate an example of creating a stable model in the Data Layer and injecting it into the ViewModel.


If you’ve injected a generally stable class,
you would naturally expect the ViewModel to be stable as well.
However, the result is as follows.

In this way, we can see that classes receiving injections from other layers also become unstable.
Why Unstable ViewModels Are Okay in Compose
A ViewModel typically injects a Repository or UseCase and acts as the connection point between the UI and business logic.
class ViewModel(
private val repository: Repository, // from domain or data layer
private val useCase: UseCase // from domain layer
) : ViewModel()
As we saw earlier, classes that inject classes from other layers are unstable.
Therefore, the ViewModel should also be an unstable class.
Yet we still wonder:
If the ViewModel is unstable, why is it acceptable?
Job Offers
Answer
The reason is simple.
We don’t pass the ViewModel itself to the composable we pass the stable state within the ViewModel.
data class TestState(
val data: String
)
@Composable
fun Screen(
viewModel: TestViewModel
) {
val state by viewModel.state.collectAsState()
Child(state)
}
@Composable
fun Child(
state: TestState
) {
Text(state.data)
}
A ViewModel is typically created only once in the top-level composable and passed as an argument. After that, it collects the state it manages and passes it down to child composables. Therefore, even if the ViewModel is unstable, it doesn’t actually cause problems.
In Closing
For monolithic projects, this isn’t a major issue.
Since all code compiles within a single module, stability assessments are straightforward.
However, if you’re considering a multi-module architecture and interested in Compose’s stability concepts,
this article will definitely be helpful.
This article was previously published on proandroiddev.com



