Search code examples
androidkotlinandroid-jetpack-composemutablestateflow

Variable not updating from viewModel flow


Can someone tell me why otp value not updated by resetText? Steps:

  1. type some text in TextField
  2. click "Reset text"

Is it bug or some issues with my code?

ViewModel

@HiltViewModel
class MyViewModel @Inject constructor() : ViewModel() {
    val state = MutableStateFlow(SomeState())

    fun resetText() {
        state.update { it.copy(text = null) }
    }

    fun changeText() {
        state.update { it.copy(text = Math.random().toString()) }
    }
}

State class

data class SomeState(
    val text: String? = null,
)

View

@Composable
fun SomeView(modifier: Modifier = Modifier) {
    val viewModel = hiltViewModel<MyViewModel>()
    val state by viewModel.state.collectAsState()
    var otp by rememberSaveable(state.text) { mutableStateOf(state.text) }
    Column(
        modifier = modifier
            .padding(horizontal = 16.dp)
            .background(Color.White)
            .fillMaxSize(),
        verticalArrangement = Arrangement.SpaceEvenly,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        TextField(
            value = otp.orEmpty(),
            onValueChange = { otp = it }
        )
        Button(onClick = { viewModel.resetText() }) {
            Text(text = "reset text")
        }
        Button(onClick = { viewModel.changeText() }) {
            Text(text = "Change text")
        }
    }
}

P.S. I made it work by changing resetText to

fun resetText() {
    viewModelScope.launch {
        state.update { it.copy(text = "") }
        delay(100)
        state.update { it.copy(text = null) }
    }
}

But it's obviously a bad option.


Solution

  • I assume what you are seeing is that when you enter text manually it doesn't reset when you call the reset method. This is because you are resetting the model but not the local copy of the data in otp. This, in general, violates the principle of single owner of the state by introducing a copy that then must be manually synchronized.

    Consider only having one copy of the data. For example,

    @Composable
    fun SomeView(modifier: Modifier = Modifier) {
        val viewModel = hiltViewModel<MyViewModel>()
        val state by viewModel.state.collectAsState()
        Column(
            modifier = modifier
                .padding(horizontal = 16.dp)
                .background(Color.White)
                .fillMaxSize(),
            verticalArrangement = Arrangement.SpaceEvenly,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            TextField(
                value = state.text.orEmpty(),
                onValueChange = { viewModel.updateText(it) }
            )
            Button(onClick = { viewModel.resetText() }) {
                Text(text = "reset text")
            }
            Button(onClick = { viewModel.changeText() }) {
                Text(text = "Change text")
            }
        }
    }
    

    Alternately, consider updating the copy of the state when the model is reset,

            Button(onClick = {
                viewModel.resetText()
                otp = "" // Also reset the local copy of the state
            }) {
                Text(text = "reset text")
            }
    

    Note the value of otp still doesn't go anywhere. In this case you would need to add an "Submit" or "Apply" action that would update the model.