Portal Berita dan Informasi Terbaik di Indonesia

A cleaner way to interact between Composable and ViewModel in Jetpack Compose | by Saurabh Pant | May, 2022

Being in a state makes you composed!

Source: https://developer.android.google

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 contains 4 fields of account number, confirm account number, bank code, owner name and a submit button. Clicking on button checks the inputs validation and show a toast on success.
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.

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!

Leave a Reply

Your email address will not be published.