Why does a call to a logic method on a ViewModel with StateFlow as a property cause a recomposition even though the value has not changed?

ko2*_*2ic 2 android android-jetpack-compose compose-recomposition

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++
  }
}
Run Code Online (Sandbox Code Playgroud)

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 = "+")
  }
}
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

Why is CCompose recomposed?

Ben*_*ove 6

这里发生了一些事情。

首先,Compose 编译器不会自动记忆捕获不稳定类型的lambda 。您的 ViewModel 是一个不稳定的类,因此通过在 onClick lambda 中使用它来捕获它意味着您onClick将在每次重组时重新创建。

因为 lambda 正在重新创建,所以输入CCompose在重组过程中并不相等,因此CCompose会被重组。

这是 Compose 编译器当前的行为,但将来很可能会发生变化,因为我们知道这是一种常见情况,我们可以做得更好。

如果您想解决此问题,您可以自己记住 lambda。这可以通过在组合中记住它来完成,例如

val cComposeOnClick = remember(viewModel) { { viewModel.increaseCount() } }
CCompose(onClick = cComposeOnClick)
Run Code Online (Sandbox Code Playgroud)

或者更改您的 ViewModel 以使用 lambda 而不是increaseCount 函数。

class MyViewModel {
  val increaseCount = { ... }
}
Run Code Online (Sandbox Code Playgroud)

或者您可以在技术上使用 @Stable 注释您的 ViewModel 类,但我可能不会推荐这样做,因为正确维护该稳定合同将非常困难。

执行其中任何一项都可以CCompose跳过。但我还想提一下,如果CCompose只是一个小的可组合项,那么 1 次额外的重组实际上可能不会对您的应用程序的性能产生太大影响,只有当它实际上导致您出现问题时,您才会应用这些修复程序。

稳定性是一个很大的话题,我建议阅读这篇文章以获取更多信息。