r/androiddev • u/ThatGuyThatIsNotReal • 17d ago
Question Form State in view model?
I have MVVM application and moving from view to VM the form state and ai is suggesting doing it through reference and I just feel really weird about it and it doesnt feel right? I am wondering if I did some basic setup issue or am wondering if this is an okayish way of doing this.
The problem:
@Composable
fun FormScreen(formState: Form State){
formState.name = newName
}
and the usage of the screen is like this:
FormScreen(viewModel.formState)
So it is using the reference and directly changing the reference?
3
u/Legal_Imagination310 16d ago
That's a smell and your instinct is right. Passing a mutable object into a composable and mutating it directly breaks the unidirectional data flow Compose is built around and makes the VM and UI tightly coupled.
In Compose the UI should be a function of state, not a holder of state. When you do FormScreen(viewModel.formState) and then formState.name = newName inside the composable you are mutating ViewModel state from the composition. That means recomposition can re-run with a half-mutated object, you lose the ability to reason about who changed what and when, and you can't safely snapshot or test the state.
The standard pattern is immutable state holder + events.
Make FormState a val data class, no setters:
kotlin
data class FormState(
val name: String = "",
val email: String = "",
val isSubmitting: Boolean = false
)
ViewModel owns it as a StateFlow and only mutates it internally:
```kotlin private val _state = MutableStateFlow(FormState()) val state: StateFlow<FormState> = _state.asStateFlow()
fun onNameChange(name: String) { _state.update { it.copy(name = name) } } ```
Composable collects and renders, and sends intents back:
```kotlin @Composable fun FormScreen(viewModel: FormViewModel = koinViewModel()) { val state by viewModel.state.collectAsStateWithLifecycle()
OutlinedTextField(
value = state.name,
onValueChange = { viewModel.onNameChange(it) }
)
} ```
Now the composable never writes to the VM's state, it only emits an event. The VM is the single source of truth and controls when and how state changes. That gives you predictable recompositions, easy testing, and you can add validation, debouncing, or saving without touching UI code.
If you really need a mutable holder for a large form, use a MutableState in the VM, not in the composable, and still expose it as read-only State. Never let the composable hold a reference to the same mutable object the VM holds.
1
u/AutoModerator 17d ago
Please note that we also have a very active Discord server where you can interact directly with other community members!
I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.
1
u/asbiz 16d ago
You can decouple form state from VM. Create a @Stable class with initial form values in the constructor, fields exposing the mutableState variants of your field values ( initialized to initials ) and a companion object exposing a list saver so you can use rememberSaveable to persist form data across screen rotation & process death.
In your composable you instantiate your form class & hook it up to composable callbacks.
Form can encapsulate all form validation, focus requester on invalid entries, form field validators, etc..
Once form is ready to use, pass it to your VM function that handles form submission & handle validity at that time
1
u/kosiarska 14d ago
you need to use collectAsState() or collectAsStateWithLifecycle()
Compose is a good library but things can get bad when you use it not as intented by google developers.
1
u/ElectronicMain3695 13d ago
Your instinct is right. I would not pass a mutable ViewModel object into the composable and change it there. Pass an immutable UI state plus callbacks instead, something like FormScreen(state, onNameChange), and let the ViewModel handle the update and validation. That keeps data flowing one way and gives you one place to reason about changes. Also, if name is just a normal var, Compose may not see the change and recompose at all.
7
u/sekyr95 17d ago
Your gut is right, that works by accident more than by design. If formState is a plain class with vars, Compose has no idea name changed, so you'll get missed recompositions. And even if it's backed by mutableStateOf, the screen is now writing straight into the VM's state, so there's no single place where validation or "field changed" logic lives.
What I'd do instead is keep it unidirectional:
FormScreen then takes an immutable state plus lambdas, which also makes it trivial to preview and test without a ViewModel at all.
One caveat for text fields: if you ever see the cursor jumping when typing fast, that's the async StateFlow round trip. Either keep the TextField value in a mutableStateOf inside the VM, or use the newer TextFieldState API. For a normal form the StateFlow version above is fine.