This application counts up A when C is pressed. Since A is the only one that has changed, I expected the recomposition to be just that. But C is also recomposition.
Here is the code.
ViewModel exposes StateFlow.
class MainViewModel : ViewModel() {
private val _count: MutableStateFlow<Int> = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increaseCount() {
_count.value++
}
}
CCompose
calls increaseCount()
.
@Composable
fun CountUpScreen(
modifier: Modifier = Modifier,
viewModel: MainViewModel = viewModel(),
) {
val count: Int by viewModel.count.collectAsState()
SideEffect { println("CountUpScreen") }
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally,
) {
ACompose(
count = count
)
BCompose()
CCompose {
viewModel.increaseCount()
}
}
}
@Composable
private fun ACompose(count: Int) {
SideEffect { println("ACompose") }
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "$count"
)
}
}
@Composable
private fun BCompose() {
SideEffect { println("BCompose") }
Text(
text = "I am composable that will not be recompose"
)
}
@Composable
private fun CCompose(onClick: () -> Unit) {
SideEffect { println("CCompose") }
Button(onClick = {
onClick()
}) {
Icon(Icons.Outlined.Add, contentDescription = "+")
}
}
The following are the results of the logs that were made to count up.
I/System.out: CountUpScreen
I/System.out: ACompose
I/System.out: CCompose
Why is CCompose recomposed?
There are a couple of things going on here.
First, the Compose compiler does not automatically memoize lambdas that capture unstable types. Your ViewModel is an unstable class and as such capturing it by using it inside the onClick lambda means your onClick
will be recreated on every recomposition.
Because the lambda is being recreated, the inputs to CCompose
are not equal across recompositions, therefore CCompose
is recomposed.
This is the current behaviour of the Compose compiler but it is very possible this will change in the future as we know this is a common situation we could do better in.
If you want to workaround this behaviour, you can memoize the lambda yourself. This could be done by remembering it in composition e.g.
val cComposeOnClick = remember(viewModel) { { viewModel.increaseCount() } }
CCompose(onClick = cComposeOnClick)
or change your ViewModel to have a lambda rather than a function for increaseCount.
class MyViewModel {
val increaseCount = { ... }
}
or you could technically annotate your ViewModel class with @Stable but I probably wouldn't recommend this because maintaining that stable contract correctly would be quite difficult.
Doing either of these would allow CCompose
to be skipped. But I would also like to mention that if CCompose
is just a small composable then 1 extra recomposition probably doesn't actually have much effect on the performance of your app, you would only apply these fixes if it was actually causing you an issue.
Stability is quite a large topic, I would recommend reading this post for more info.