Jetpack Compose Observe mutableStateOf in ViewModel

Viewed 2692

I have a need to update the user profile switch

  1. ViewModel
class ProfileViewModel : BaseViewModel() {

    var greet = mutableStateOf(user.pushSetting.greet)
    var message = mutableStateOf(user.pushSetting.message)
    var messageDetails = mutableStateOf(user.pushSetting.messageDetails)

    var follow = mutableStateOf(user.pushSetting)
    var like = mutableStateOf(user.pushSetting.like)
    var comment = mutableStateOf(user.pushSetting.comment)

    fun updateUser() {
        println("--")
    }
}

2.Composable

@Composable
fun SettingCard(viewModel: ProfileViewModel) {

    Lists {
        Section {
            TextRow(text = "手机号码") { }
            TextRow(text = "修改密码", line = false) { }
        }

        Section {
            SwitchRow(text = "新好友通知", checkedState = viewModel.greet)
            SwitchRow(text = "新消息通知", checkedState = viewModel.message)
            SwitchRow(text = "消息显示详细", line = false, checkedState = viewModel.messageDetails)
        }
    }
}

3.SwitchRow

@Composable
fun SwitchRow(text: String, line: Boolean = true, checkedState: MutableState<Boolean>) {

    ListItem(
        text = { Text(text) },
        trailing = {
            Switch(
                checked = checkedState.value,
                onCheckedChange = { checkedState.value = it },
                colors = SwitchDefaults.colors(checkedThumbColor = MaterialTheme.colors.primary)
            )
        }
    )
}

How can I observe the change of the switch and call updateUser() in ViewModel

I know this is a way, but it is not ideal. The network update will be called every time it is initialized. Is there a better solution?

LaunchedEffect(viewModel.greet) {
     viewModel.updateUser()
}
2 Answers

The best solution for this would be to have unidirectional flow with SwitchRow with a lambda as @Codecameo advised.

But if you want to observe a MutableState inside your Viewmodel you can use snapshotFlows as

var greet: MutableState<Boolean> = mutableStateOf(user.pushSetting.greet)

init {
    snapshotFlow { greet.value }
        .onEach {
            updateUser()
        }
        .launchIn(viewModelScope)
        //...
}

Create a Flow from observable Snapshot state. (e.g. state holders returned by mutableStateOf.) snapshotFlow creates a Flow that runs block when collected and emits the result, recording any snapshot state that was accessed. While collection continues, if a new Snapshot is applied that changes state accessed by block, the flow will run block again, re-recording the snapshot state that was accessed. If the result of block is not equal to the previous result, the flow will emit that new result. (This behavior is similar to that of Flow.distinctUntilChanged.) Collection will continue indefinitely unless it is explicitly cancelled or limited by the use of other Flow operators.

Add a callback lamba in SwitchRow and call it upon any state change

@Composable
fun SettingCard(viewModel: ProfileViewModel) {
Lists {
    Section {
        TextRow(text = "手机号码") { }
        TextRow(text = "修改密码", line = false) { }
    }

    Section {
        SwitchRow(text = "新好友通知", checkedState = viewModel.greet) {
            viewModel.updateUser()
        }
        SwitchRow(text = "新消息通知", checkedState = viewModel.message) {
            viewModel.updateUser()
        }
        SwitchRow(text = "消息显示详细", line = false, checkedState = viewModel.messageDetails) {
            viewModel.updateUser()
        }
    }
}
}

@Composable
   fun SwitchRow(
   text: String,
   line: Boolean = true,
   checkedState: MutableState<Boolean>,
   onChange: (Boolean) -> Unit
) {
ListItem(
    text = { Text(text) },
    trailing = {
        Switch(
            checked = checkedState.value,
            onCheckedChange = {
                onChange(it)
                checkedState.value = it
            },
            colors = SwitchDefaults.colors(checkedThumbColor = MaterialTheme.colors.primary)
        )
    }
)
}

Another approach:

You can keep MutableStateFlow<T> in your viewmodel and start observing it in init method and send a value to it from SwitchRow, like viewModel.stateFlow.value = value. Remember, MutableStateFlow will only trigger in the value changes. If you set same value twice it will discard second value and will execute for first one.

val stateFlow = MutableStateFlow<Boolean?>(null)

init {
    stateFlow
        .filterNotNull()
        .onEach { updateUser() }
        .launchIn(viewModelScope)
}

In switchRow

viewmodel.stateFlow.value = !(viewmodel.stateFlow.value?: false)

This could be one potential solution. You can implement it in your convenient way.

Related