Key Idea

MVVM stands for model-view-viewmodel, let's go over each of these and see how they provide a framework for state management.

View

The view is what you are already most familiar with, it consists of the composables that the user sees as they explore the app. Here's an example of a CommentView composable:

@Composable
fun CommentView(
    username: String,
    commentText: String
) {
    Column(modifier = Modifier.padding(8.dp)) {
        Text(
            text = username,
            fontWeight = FontWeight.Bold,
            fontSize = 16.sp,
            modifier = Modifier.padding(bottom = 4.dp)
        )
        BasicText(text = commentText)
    }
}

Notice that CommentView contains only logic for displaying the username and commentText. This is an important detail that generalizes to the concept of views as a whole: views should not store any business logic. Ideally, the view's only responsibility should be to properly display a state. In this case, the state of CommentView is the username text and commentText. So then where should the business logic go? Read on to discover 🤩

ViewModel

The main purpose of a ViewModel from a coding standpoint store the state of a screen such that it survives configuration changes, such as the user rotating their screen or changing their screen size (like splitting to multiple windows with a folding phone). But from an organization standpoint, the ViewModel takes a larger role than just the state holder. The ViewModel is also responsible for storing the "business logic", or intermediary logic that we use to process data between the View and the Model. A common example of business logic is password validation. If we want to make sure a password is secure enough before sending it off to the Model to create the user's account, this validation logic should be stored in the ViewModel.

So to reiterate, a ViewModel has two main functions:

  1. Retrieve data from the Model, and nicely package it into a digestible state for the View

  2. Process / validate user input from the view and send it to the Model.

Having our code separate in this way makes testing and maintenance far easier. If you need to change the business logic, then you shouldn't need to touch the UI at all. If you want to change how the UI looks, you shouldn't need to change any business logic to accommodate that. This also allows for easier testing. We can test our business logic in a completely separate environment from our UI.

The ViewModel can send notifications to the UI, also referred to as effects. For example, if the UI requests the ViewModel to validate a credit card number and the validation fails, the ViewModel can trigger an effect to show a snackbar indicating the number is invalid.

Model

The model acts as a bridge between the ViewModel and the actual database that we use. The model abstracts away common data operations that the ViewModel might use. This way, if the database changes in the future, we only have to change these abstractions without needing to change the business logic.

The value of the Model abstraction

Here's an example of how a model could be valuable: let's say that we have a fitness app and we want to show the user a monthly summary of their workout statistics. At this fitness app company, the backend team doesn't have the bandwidth to create a custom summary endpoint just for your use case, so they expect you to assemble the data from other endpoints.

You create two functions in your model: getMonthlySteps and getMonthlyCalories. You use these in your ViewModel to nicely package the data into a summary data class for your View, and all is well.

Later down the road, the backend team decides it's too costly for you to make two API calls to generate one summary item, so they decide to give you a custom summary endpoint that contains both of these data points. Then because of your great architectural decisions, all you have to do is change the implementation of getMonthlySteps and getMonthlyCalories. The method spec can stay the same, so their use in the ViewModel can stay the same too. To reimplement this to only make one API call, you might cache the resulting summary item from getMonthlySteps and have getMonthlyCalories read from the cache, or vice versa.

Events

Just as your ViewModel can send notifications to your View, you may want your Model to send notifications to your ViewModel. For example, if the Model abstracts away a long-running database operation from the ViewModel, and this operation fails, we may want to notify the ViewModel that this is the case. With this notification, the ViewModel could update the state of the View, or even send an effect to the View alerting it.

Unidirectional data flow

You may have noticed a pattern throughout this chapter: state flows down, and events flow up.

The Model holds the actual data that the view needs for its state, this data flows down to the ViewModel. The ViewModel aggregates and packages this data into a nice readable form for the View, and then sends this state down to the view.

The view sends events to the ViewModel, for example the user pressing a button. The ViewModel then processes this event and sends it to the Model, which then processes the event and sends it to the database.

This is a nice pattern to be aware of, since being mindful of it when creating an app provides benefits that help with scalability and testability. The idea even extends beyond Android development, as you'll see it used in other frontend frameworks. I highly recommend reading the section on unidirectional data flow from the Android documentation. It explains the concept much better than I can and provides great guidance on how to follow the principle.

Last updated