7.2 Implementation Ideas
Note: MVVM has many different approaches to its implementation. I will show some of the ways that this pattern can be implemented, but keep in mind that there is no one right approach.
View
The View is pretty straightforward, you're just creating composables to display the information that you need to. In this section I'll just provide some general tips and best practices for creating your View in MVVM:
Always pass the minimum amount of information to your view
This tip helps keep our code debuggable. Let's say we need to navigate from Screen A to Screen B. We have two options. We could pass the entire
navController
down to Screen A, or we could pass a function,navigateToScreenB: () -> Unit
to Screen A. If we pass the entirenavController
, we are also giving Screen A access to many other methods that it could use to influence the screen we are on and our navigation stack. Let's say in the future we have an issue with navigation in our app. If we only passednavigateToScreenB
function to Screen A, then we would instantly have the guarantee that Screen A only navigates to screen B, and we would have to do less debugging on that screen, as opposed to passing the entirenavController
.A corollary to this tip is that ViewModels should never be passed into your Composables. They can be injected, but should never be passed down directly as a parameter.
Try to minimize business logic in the view
Let's say you're working on Eatery, and you want to determine what the title should show on the home screen. Suppose that if there is more than 1 eatery open, we want to show the text "UAW is not on strike!" is our app title, and if there are no dining halls open, we want to show the text "The strike is still ongoing 😔". It could be tempting to pass the list of Eateries directly to the composable, and have the composable determine which text to show. But it is more appropriate to pass a
screenTitle: String
down to the view. This keeps business logic out of the view and allows you to easily write tests for your ViewModel to determine what your screen displays in what conditions.
ViewModel
I gave an example of a fairly standard ViewModel in my guide on Flows that shows how to incorporate Flows into a ViewModel. I recommend taking a look at that one first. Here I'll give an example of another common ViewModel use case: loading an asynchronous resource.
There's a lot going on in this example that shows common practices with ViewModels, so let's break this analysis down:
ViewState
For true best practices, each ViewModel should have a corresponding ViewState data class
. Sometimes this practice can be a bit overkill, especially if your ViewModel only exposes a couple of fields to the View. But generally this idea helps keep things managable as your code scales. Some reasons that this approach tends to be a good idea:
It encourages a clear separation of concerns between the ViewModel and the View. The purpose of the ViewModel is to expose the UI state to the View, and the ViewModel should provide exactly the information that the View needs to the View.
It simplifies state management by encapsulating all relevant UI data in a single object, making it easier to observe, test, and update the UI.
Compose relies on state-driven UI rendering. A dedicated ViewState ensures UI recomposes automatically when state changes, aligning with Compose’s reactive nature.
Interactions With the Model
We can see that the ViewModel has two main purposes for interacting with the model:
Packaging the information from the model into a readable ViewState
The function
observePictureRepository
is responsible for this. We read whatever the latest data is from the model, and then update the UI state accordingly.
Allowing UI events to flow from the View to the Model.
This is what the
loadNextPage
function is for. When the UI wants us to load a new page of pictures, it indicates that by calling theloadNextPage
function. This function calls upon thepictureService
to get more pictures from the API, and then it indicates this change to thepictureRepository
, by providing it with the latest picture data through itsput
method.
Model
Remember there is no single agreed upon implementation of what a Model needs to look like, so I'm going to share a couple of the main variations I've worked with throughout my time as an Android developer.
Model as a Cache
I call this section "Model as a Cache" because the model more just functions as a data structure, and doesn't have much special functionality. Room and DataStore can be thought of as implementations of this approach as well, but you might want to make a Model that wraps these for further abstraction. Let's take a look at an example of building a data cache from scratch:
We can see that this PictureRepository
simply acts as a data structure, almost like a regular Map. Except we use Flow
so users of the repository can observe updates to the data live as they occur. We expose a put
that updates the idsToPicture
Flow, so then any observers using either observe
or observeAll
will be updated properly.
A more complicated repository might expose multiple Flows, with multiple respective put
and observe
methods, and then it could be the responsibility of the ViewModel to aggregate the data into what the UI wants to display.
Model as a Data Source
Let's consider a data source as a more abstract notion of a cache. So the ViewModel should be able to call upon this data source and get the data that it needs. It doesn't need to worry about any of the implementation details or where the data source gets its data (as opposed to the previous example, where the ViewModel places data in the model). Let's use a trimmed version of Eatery Blue's EateryRepository
as an example:
We can see that this model has a cache nested as a part of the model. The model is now responsible for fetching all of its data, and this part has been abstracted away from the ViewModel. The model could also expose methods for modifying its data. Although this works great for a simple example such as read-only Eatery details, sometimes it can become complicated to have one file that calls the API and manages the ViewState in one.
We also have to try and find a delicate balance between the responsibilities of the Model and the ViewModel. For example, this EateryRepository
already packages the data in a MutableStateFlow<EateryApiResponse<List<Eatery>>>
. If I wanted a ViewModel for the screen that shows all eateries, then I would basically just be exposing this state flow, and not much else. This makes the Model feel more like a ViewModel, since it's already doing the work of aggregating the data into this more readable form of EateryApiResponse
In the real Eatery codebase, the ViewModel combines the userPreferencesRepository
and the eateryRepository
to aggregate the data and provide the View with information about what eateries are favorited. This gives a nice purpose for the ViewModel. But the point is that we never want to have a single Model have too many responsibilities, to the point where its corresponding ViewModel just becomes boilerplate.
Last updated