One thing I have understood about MVI, is that the model creates the state and the view handles it. The view ALWAYS gets a FULL state from the model, meaning that every state given to the view includes information for every part of the view, every time. Have I undersood it correctly?
Given that 1 above is true, how do I update only a small part of the view if I get a full state every time? Example: the model consists of type Group and User. Groups contains a number of users, a name and a location. When editing a Group, I want to display the name, the location and a list of possible group members (User) with checkboxes for every user. The requirements says that the user list could change during editing of group; a user can be deleted, new users can join (added or removed from db) meanwhile the view is shown. So the list of users need to be able to update when editing a group. But the group name and location should only be updated initially, but not when the user list is updated. E.g. I don't want to call the name textview's .setText() more than once. How can I achieve this following the MVI view state principle?

- 109
- 8
1 Answers
But the group name and location should only be updated initially, but not when the user list is updated.
MVI stands for Model-View-Intent. Essentially, your View is modeled with a single state. Your View gets updated by listening to the output of this entire state. Whenever the user interacts with the View, through an Intent like a "click", the ViewModel (or whatever business logic class) updates only the affected fields of said state.
What you're asking for here through this requirement doesn't fit this pattern. If you wanted to satisfy the quoted requirement above, you would need separate states for group name, group location, and members list.
Then, whenever there was a change to the list, you only push an update to the list and any consumers of it would get triggered and update the UI accordingly, without bothering name and location.
MVI Solution
The view ALWAYS gets a FULL state from the model, meaning that every state given to the view includes information for every part of the view, every time.
Yes, the View will always get the entire state and that's okay. The important part of MVI is that the ViewModel will only update the fields that have changed.
How can I achieve this following the MVI view state principle?
We need to create a model that represents the state of your View. Assuming you're using Kotlin, we can do:
data class GroupsViewState(
val numOfUsers: Int,
val location: Location,
val members: List<User>
)
data class Location(
val x: Float,
val y: Float
)
data class User(
val isChecked: Boolean
)
Then, we need to define our Intents. Essentially, what actions can a user perform on the View state above? Here's an example:
sealed class GroupsIntent {
data class UpdateNumOfUsers(val number: Int) : GroupsIntent()
data class AddUser(val user: User) : GroupsIntent()
data class DeleteUser(val user: User) : GroupsIntent()
data class ToggleUser(val user: User): GroupsIntent()
// etc.
}
Kotlin has a powerful operator for its data classes that is key to making MVI work: copy
This operator allows you to update only certain fields within a data class.
Thus, in your ViewModel you would do something like:
class GroupsViewModel : ViewModel() {
// Internal MutableLiveData that we'll update based on Intents
private val _groupsState = MutableLiveData<GroupsViewState>()
// Exposes an immutable LiveData to consumers and triggers updates
// whenever the Mutable counterpart is updated.
val groupsState: LiveData<GroupsViewState> get() = _groupsState
// This is the appeal of MVI
// All Intents come through one funnel function and get relayed accordingly
fun processIntent(intent: GroupsIntent) {
when (intent) {
is AddUser -> addUser(intent.user)
is DeleteUser -> deleteUser(intent.user)
is ToggleUser -> toggleUser(intent.user)
}
}
// Copies the mutated list with new User appended to the View state
// **without** touching the other fields
private fun addUser(user: User) {
val currentViewState = getCurrentState()
val mutableMembers = currentViewState.members.toMutableList()
_groupsState.value = currentViewState.copy(members = mutableMembers.add(user))
}
private fun deleteUser(user: User) {
// Similar approach to above, but with delete logic
}
private fun toggleUser(user: User) {
// Toggle logic similar to above
}
private fun getCurrentViewState() = groupsState.value
}
Then, at the UI layer:
class GroupsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_groups)
// Bind to your Views
// etc.
setupObervers()
bindIntents()
}
// Subscribe to your ViewState's LiveData and
// update the View according to the changes
private fun setupObservers() {
viewModel.groupsState.observe(this) { viewState ->
numOfUsersTextView.text = viewState.numOfUsers
locationView.value = viewState.location // I made 'value' up
membersListViewWidget = viewState.members
}
}
// Bind your UI widget listeners to processIntent() with
// corresponding values
private fun bindIntents() {
numOfUsersText.setOnClickListener { num ->
viewModel.processIntent(UpdateNumOfUsers(num)
}
// etc.
}
}

- 665
- 2
- 10
- 28