Being in a state makes you composed!
In Jetpack Compose, while dealing with composables, we come across states and events. Clear understanding of both make your code more readable, maintainable and testable. In practice, we try to keep our composables stateless as much as possible.
But when it comes to interaction between composable and view model, it quickly becomes messy because we call view model functions directly. For instance
Button( onClick = { if (bankFormViewModel.validateInputs()) { // not good // do something } else { // show some error } }, )
As shown above, on click of Button, we check if our inputs are validated correctly or not and call the function validateInputs() on view model object.
Though this approach works fine but what if you need to perform one more step before checking the inputs validation. Then you’ll end up writing few more lines of code within the Button’s on click itself.
But does this change belong to the composable? Well. it does not!!
So what could be a better way to handle your composable interaction with the view model? As said in Thinking in Compose ,
Manipulating views manually increases the likelihood of errors.
With the same thought we can handle the interactions by providing some state of actions to our view model from composable. Let’s see a scenario with an example of creating a Bank Detail Form page which looks like this
Bank Form Screen
On this screen, when we click on the submit button, the app checks the four text fields validations and show a toast message if all are valid. So when we click on the button we pass an actionable state to our view model instead of calling its validate function directly.
We create a sealed class called UIEvent as follows
sealed class UIEvent { data class AccountChanged(val account: String): UIEvent() data class ConfirmAccountChanged(val confirmAccount: String): UIEvent() data class CodeChanged(val code: String): UIEvent() data class NameChanged(val name: String): UIEvent() object Submit: UIEvent() }
The class clearly indicates what all actions are coming from the composable. Now we pass any of these state to our view model to perform some action. In our example, we’ll pass Submit event and the button on click modifies to
Button( onClick = { bankFormViewModel.onEvent(UIEvent.Submit) }, )
Clean! Isn’t? Now even if we’ve to perform some extra steps later, we can easily do that without touching our on click lambda in composable.
So lets create the onEvent function in our viewmodel as follows
fun onEvent(event: UIEvent) { when(event) { is UIEvent.AccountChanged -> { _uiState.value = _uiState.value.copy( accountNumber = event.account ) } is UIEvent.ConfirmAccountChanged -> { _uiState.value = _uiState.value.copy( confirmAccountNumber = event.confirmAccount ) } is UIEvent.CodeChanged -> { _uiState.value = _uiState.value.copy( code = event.code ) } is UIEvent.NameChanged -> { _uiState.value = _uiState.value.copy( ownerName = event.name ) } is UIEvent.Submit -> { validateInputs() } } }
The function is self explanatory and very readable too. Now the only interaction we’ve between composable and view model is via onEvent function and some more state variables that we define for composable state changes.
Job Offers
Now comes the second part where we’ve to maintain the UI data state which is captured during the user interaction with views. So UI data state will be a data class which will hold the data which the UI needs to update itself based on the validation.
data class UIState( val accountNumber: String = "", val confirmAccountNumber: String = "", val code: String = "", val ownerName: String = "", val hasAccountError: Boolean = false, val hasConfirmAccountError: Boolean = false, val hasCodeError: Boolean = false, val hasNameError: Boolean = false, )
As we see it hold all the four text fields data and update the error fields based on the validations. These error fields are then consumed by our composable as state and update themselves.
This helps us maintaining and manipulating the state easily from view model.
Now the last part we need to add is a flow via which we can show a toast in our composable. For this purpose we use shared flow as
val validationEvent = MutableSharedFlow<ValidationEvent>()
On successful validation of all inputs, we send event as
viewModelScope.launch { if (!hasError) { validationEvent.emit(ValidationEvent.Success) } }
This event is consumed by our composable and shows a toast as follows
bankFormViewModel.validationEvent.collect { event -> when(event) { is ValidationEvent.Success -> { Toast .makeText(context,"All inputs are valid", Toast.LENGTH_SHORT) .show() } } }
Now that all of our code becomes self explanatory, readable and maintainable as we control our states from view model and made the composables stateless.
Checkout the full code for this sample project below
Hope this would be helpful.
Until next time!!
Cheers!
This article was originally published on proandroiddev.com on May 18, 2022