Vadim Zhukov
Vadim Zhukov

Reputation: 347

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

Upvotes: 4

Views: 2477

Answers (3)

Tristan Elliott
Tristan Elliott

Reputation: 951

Resources

TLDR (Too long didnt read)

  • When a lambda is written, the compiler is creating an anonymous class with that code. If the lambda requires access to external variables(ie the ViewModel), the compiler will add those variables as fields that are passed into the constructor of the lambda. The ViewModel is violating the @Stable requirement that all public properties must also be @Stable

The remember fix


val updateClickedUser:(String,String,Boolean,Boolean)->Unit = remember(streamViewModel) { { username, userId, banned, isMod ->
        streamViewModel.updateClickedChat(
                username,
                userId,
                banned,
                isMod
            )
    } }
    ChatUI(
        twitchUserChat = twitchUserChat,
        updateClickedUser = { username, userId, banned, isMod ->
            updateClickedUser(
                username,
                userId,
                banned,
                isMod
            )

        },
    )

Upvotes: 1

Phil Dukhov
Phil Dukhov

Reputation: 88232

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.

Upvotes: 3

Richard Onslow Roper
Richard Onslow Roper

Reputation: 6863

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

Upvotes: -1

Related Questions