Why is ViewModel method reference not stable (causing recomposition) but lambda is? (Jetpack Compose)
Asked Answered
T

2

14

I have an MVI architecture with and activity containing the main screen

class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MyTheme {
        val viewModel: MainScreenViewModel = viewModel(factory = MainScreenViewModel.Factory)
        val state by viewModel.viewState.collectAsState()
        MainScreen(state, viewModel::handleEvent)
      }
    }
  }
}

The main screen looks somewhat like this:

@Composable
fun MainScreen(state: MainScreenState, onEvent: (Event) -> Unit) {
  TaskList(
    tasks = state.items,
    onEvent = onEvent,
  )
}

@Composable
fun TaskList(tasks: List<TaskState>, onEvent: (Event) -> Unit) {
  LazyColumn {
    items(items = tasks, key = { it.id }) {
      Task(state = it, onEvent = onEvent)
    }
  }
}

Now, if the ViewModel emits a new MainScreenState where one TaskState is changed, the TaskList recomposes. This is expected, as a new list object is passed. But not only the changed Task is recomposed, but every Task composable in the list. The reason for this is not the TaskState, it is stable.

What seems to cause the recomposition is the onEvent callback. If I change onEvent = onEvent to onEvent = { onEvent(it) } in the MainScreen, the problem is solved. But I do not understand why. Shouldn't method references be stable as-well? I have even checked the hashCode, it's always the same.

Triable answered 19/6, 2023 at 10:47 Comment(1)
I raised this issue here issuetracker.google.com/issues/318396527 I was having the same. It also has an attached application with it to try out the issueDulci
A
1

You need to look at the number of recompositions in release mode, not debug mode.

I don't know why, but thanks to this article https://itnext.io/exercises-in-futility-jetpack-compose-recomposition-6ea3cf9bc1b4

and https://github.com/theapache64/rebugger

I found that in release lambda was stable.

The problem is the combiler in debug mode

Amorino answered 19/12, 2023 at 13:30 Comment(1)
My compose compiler metrics are returning stable for both release and debug . When i run the build in release mode , with debuggable true and BP the relevant line , i can still see that the method reference lambda is being recomposed . compose is all over the placeDulci
S
-2

The reason why the Task composables are recomposed even when the TaskState is stable is because the onEvent callback is passed by reference. This means that when the TaskList composable is recomposed, it gets a new copy of the onEvent callback. This new copy of the callback is not aware of the previous state of the TaskState, so it calls the onEvent function with the new state.

To fix this, you can use a lambda expression to create a new onEvent callback for each Task composable. This way, each Task composable will have its own copy of the onEvent callback, and it will be aware of the previous state of the TaskState.

Here is the updated code:

@Composable
fun MainScreen(state: MainScreenState) {
  TaskList(
    tasks = state.items,
    onEvent = { event -> viewModel.handleEvent(event) }
  )
}

@Composable
fun TaskList(tasks: List<TaskState>, onEvent: (Event) -> Unit) {
  LazyColumn {
    items(items = tasks, key = { it.id }) {
      Task(state = it, onEvent = onEvent)
    }
  }
}
Schulze answered 20/6, 2023 at 6:24 Comment(1)
What do you mean by "passed by reference" - isn't everything in Java passed by pointer and you cannot change that? Second: why would this mean you get a new copy of the callback? It should always be the same pointer to the same viewmodel method, no? Third: I can completely remove the state argument and the problem persists, so "the callback not being aware of the previous state" does not make sense to me, why would it anyway?Triable

© 2022 - 2025 — McMap. All rights reserved.