'Why recomposition happens when call ViewModel in a callback?

I completely confused with compose conception. I have a code

@Composable
fun HomeScreen(viewModel: HomeViewModel = getViewModel()) {
    Scaffold {
        val isTimeEnable by viewModel.isTimerEnable.observeAsState()
        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center,
            modifier = Modifier
                .fillMaxSize()
                .background(Color.Black),
        ) {
            Switch(
                checked = isTimeEnable ?: false,
                onCheckedChange = {
                    viewModel.setTimerEnable(it)
                },
            )
            Clock(viewModel.timeSelected.value!!) {
                viewModel.setTime(it)
            }
        }
    }
}

@Composable
fun Clock(date: Long, selectTime: (date: Date) -> Unit) {
    NumberClock(Date(date)) {
        val time = SimpleDateFormat("HH:mm", Locale.ROOT).format(it)
        Timber.d("Selected time: time")
        selectTime(it)
    }
}

Why Clock widget recomposes when I tap switch. If I remove line selectTime(it) from Clock widget callback recomposition doesn't happen. Compose version: 1.0.2



Solution 1:[1]

This is because in terms of compose, you are creating a new selectTime lambda every time, so recomposition is necessary. If you pass setTime function as a reference, compose will know that it is the same function, so no recomposition is needed:

Clock(viewModel.timeSelected.value!!, viewModel::setTime)

Alternatively if you have more complex handler, you can remember it. Double brackets ({{ }}) are critical here, because you need to remember the lambda.

Clock(
    date = viewModel.timeSelected.value!!,
    selectTime = remember(viewModel) {
        {
            viewModel.setTimerEnable(it)
        }
    }
)

I know it looks kind of strange, you can use rememberLambda which will make your code more readable:

selectTime = rememberLambda(viewModel) {
    viewModel.setTimerEnable(it)
}

Note that you need to pass all values that may change as keys, so remember will be recalculated on demand.


In general, recomposition is not a bad thing. Of course, if you can decrease it, you should do that, but your code should work fine even if it is recomposed many times. For example, you should not do heavy calculations right inside composable to do this, but instead use side effects.

So if recomposing Clock causes weird UI effects, there is probably something wrong with your NumberClock that cannot survive the recomposition. If so, please add the NumberClock code to your question for advice on how to improve it.

Solution 2:[2]

This is the intended behaviour. You are clearly modifying the isTimeEnabled field inside your viewmodel when the user toggles the switch (by calling vm.setTimeenabled). Now, it is apparent that the isTimeEnabled in your viewmodel is a LiveData instance, and you are referring to that instance from within your Composable by calling observeAsState on it. Hence, when you modify the value from the switch's onValueChange, you are essentially modifying the state that the Composable depends on. Hence, to render the updated state, a recomposition is triggered

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Richard Onslow Roper