I am using the same view model/composable pair for a creation form and an edit form. In the creation form, the text input starts as empty, but in the edit form, the initial text input comes from the database. In the view model, I am combining input that comes from the database with the current text input to show different states to the user. I created a TextFieldState in the composable to handle the text field input on the UI end and a StateFlow in the view model to handle the input on that side. I'm having trouble syncing the data bidirectionally. The latest value needs to come from the UI but the initial value needs to come from the database. Here's a simplified example:
data class FormUiState(val name: String, val changed: Boolean)
class FormViewModel : ViewModel() {
private val name = MutableStateFlow("")
val uiState = combine(name, getExistingNameFromDb()) { name, existingName ->
FormUiState(
name = name,
changed = name != existingName,
)
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000L),
FormUiState(name = "", changed = false)
)
init {
viewModelScope.launch {
name.value = getExistingNameFromDb().first()
}
}
fun updateName(value: String) {
name.value = value
}
private fun getExistingNameFromDb() = flowOf("foo")
}
@Composable
fun FormScreen(modifier: Modifier = Modifier, viewModel: FormViewModel = viewModel()) {
val name = rememberTextFieldState()
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(name) {
snapshotFlow { name.text.toString() }.collect { viewModel.updateName(it) }
}
LaunchedEffect(uiState.name) {
if (uiState.name != name.text) {
name.setTextAndPlaceCursorAtEnd(uiState.name)
}
}
Column(modifier = modifier) {
OutlinedTextField(state = name, label = { Text("Name") })
Text(text = "Changed: ${uiState.changed}")
}
}
With what I have implemented now, I can only get the value to go one way from the UI to the view model. I also tried using a value-based text field, but it causes the updates to the text input in the UI to skip some values. I also tried moving the TextFieldState to the view model, but then I can't run the view model tests.
How do I bidirectionally sync text field input between a view model and a composable?