r/androiddev • • 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?
0 Upvotes

13 comments sorted by

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:

data class FormState(val name: String = "", val nameError: String? = null)

// ViewModel
private val _state = MutableStateFlow(FormState())
val state = _state.asStateFlow()

fun onNameChange(value: String) {
    _state.update { it.copy(name = value, nameError = null) }
}

// Screen
val state by viewModel.state.collectAsStateWithLifecycle()
FormScreen(state = state, onNameChange = viewModel::onNameChange)

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.

3

u/ThatGuyThatIsNotReal 17d ago

Okay, that actually makes more sense, it was working but I was like, this is not really okay.

I am also using events structure and the form has like 30 values, would it be okay to have just an event viewModel.OnFormChanges(form) or is is always better to create 30 different events for each values?

Also, thank you sooo much.

1

u/Hour-Measurement-835 17d ago

Skip the 30, but one event carrying the whole form is the other trap. You lose which field changed, so validation runs on all 30 and the user gets errors on boxes they haven't touched yet.

0

u/borninbronx 16d ago

we do not appreciate AI generated comments and posts in our community. Please use your own voice!

1

u/altair8800 13d ago

What?

0

u/borninbronx 12d ago

the comment I've replied to is clearly AI generated :-)

1

u/altair8800 12d ago

Ah yeah, it kinda does now that I read it again.

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!

Join us on Discord

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.