I would also add to the above statement:
• Immutable ui state data. Your UI state that lives in your ViewModel should be in an of itself an immutable object (usually a data class) that gets put into a MutableState object (or LiveData if you really want, but if you're pure compose, I prefer MutableState). So when anything in the state changes, you create a copy of the state along with the mutations and set it's new value on the MutableState. This lets your composables automatically subscribe to the changes. Most people keep a single UIState object, though you could use multiple ones if you really wanted.
• Unidirectional data flow. Your VM should expose your UIState either as a State (not MutableState as you don't want outside classes to change the value direction) or as a kotlin Flow. Your composables then use the State (or convert a flow using observeAsState) to render themselves. The UI then can trigger mutations by asking the VM to do operations, which internally would mutate the state.
• Keep your composables layered and VM agnostic. I typically create a composable which will take the UIState object along with lambda functions for callbacks, and render the screen for one particular state. On top of that, i'll create another composable which will interact with the view model by accessing the state and passing to the first composable, and by creating lambda functions that talk to the viewmodel. This keeps the view layer pure and easily testable.