androidandroid-jetpack-composeviewmodelandroid-viewmodelcompose-recomposition

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

  • 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.