True MVVM: Composing ViewModels
Model-View-ViewModel (MVVM) has become one of the most popular architectural patterns in Android development. After Google introduced ViewModel as an architecture component in Jetpack, it has gotten even more popular, and since several good articles explain what MVVM is and how to employ it in Android, we will not be focusing on that part much.
Our main objective in this article is to describe how we apply MVVM in the FlixBus Android passenger app, and in the process, we are going to elaborate on an idea: Composing ViewModels.
Before getting into details, we will provide a brief overview of the MVVM pattern and how we understand it because there are several different interpretations of MVVM even though the idea is quite simple.
MVVM and the Jetpack ViewModel
Here is an illustration of the MVVM architectural pattern that shows the relationship between the View, ViewModel, and Model.
This diagram assumes that the Android Data Binding Library is used for passing data between View and ViewModel. However, it is not necessary, and that part can be replaced by binding data in view related classes (e.g. Activity/Fragment) manually.
When it comes to applying MVVM on Android applications, the ViewModel part is generally implemented by using the Android Jetpack ViewModel library. The Android ViewModel helps store and manage UI related data in a lifecycle aware manner. However, using it does not mean you are following the MVVM pattern. It does not enforce anything related to the MVVM ViewModel definition. Therefore, naming this class as ViewModel is a bit unfortunate. It is now a really common assumption in the Android community that when Android ViewModel is used we are conforming to MVVM, but that is not “True MVVM”.
“The Android ViewModel helps implement the MVVM pattern, but using Android ViewModels does not mean that we are following MVVM rules.”
Idea: Composing ViewModels
After aligning on the MVVM definition, we can now dive into how we apply MVVM in our app. Before talking about implementation details, it will be better that we elaborate on an idea: Composing ViewModels because our MVVM implementation depends on this idea.
Let’s start with Views. In Android, given a canvas, you can draw any component (e.g. Shapes, Text) onto the screen and create your own components (e.g., Button) from scratch. However, the Android view system already provides these components and you can compose your views using text views, buttons, layouts, etc.
In addition to this, nowadays Android UI development is trying to shift from imperative UI programming to declarative UI programming. Jetpack Compose is the new declarative UI library from the Android team. The main idea behind it is composing views using composable functions.
As we have seen, Views are composable. Why not compose ViewModels as well?
Even though mobile screens are smaller compared to traditional computer screens, their UI can still become complex to manage if we do not follow the Single Responsibility Principle. In the early days of Android development, the general trend was to put everything in a single class related to a particular screen, namely Activity or Fragment. Then these classes become bigger and bigger when new logic is added and they become hard to manage. After a while, architectural patterns, such as MVC, MVP, MVVM, and MVI, came to the rescue and people started to break big Activities/Fragments into manageable units such as View, ViewModel, and Model. This solves the problem up to a certain extent. However, ViewModels are now the next hot spot to become unmanageable because they will contain most of the UI logic. That is where we can push one step further and break big ViewModels into manageable chunks.
“In order to make our UI logic more maintainable, we can compose ViewModels using smaller manageable ones by composition.“
How to compose ViewModels?
We are going to implement a screen which will populate a list of rectangles. A rectangle is composed of a circle and a triangle as shown in the figure. A circle has a radius property and a triangle has a length property, assuming that all vertices have the same length. In addition to this, there is a toolbar at the top with a title.
Let’s start creating ViewModels in a bottom-up manner. Note that these are not complete implementations. The objective of the example is to sketch the skeleton of UiModel hierarchy and emphasize the important parts.
Note that we are using UiModel as a suffix for naming child-ViewModels here. Only the root ViewModel needs to extend Android Jetpack ViewModel, child ViewModels do not need to extend it. In order to make this distinction clear, we came up with this convention so that child ViewModels have a UiModel suffix. Note that UiModels are still ViewModels in terms of MVVM perspective.
Now we are ready to compose a new UiModel (i.e. RectangleUiModel) from existing ones.
We need a container ViewModel which will be composed of a Toolbar and rectangles, namely RectanglesViewModel. Here the suffix is ViewModel because it is the root ViewModel and it extends the Android Jetpack ViewModel class. Let’s start with creating our ToolbarUiModel.
Then we need to create rectangles. However, we have a problem. We do not know the number of rectangles in advance, hence the number of UiModels to inject. What should we do then? The solution is to create a builder function for RectangleUiModel and also for its children and use them while creating items.
Now we have all the building blocks of RectanglesViewModel and compose it using these blocks.
How to construct a hierarchy using dependency injection?
In our app, we use Dagger 2 for dependency injection. As it could be followed from the example implementation, the ViewModel/UiModel hierarchy is built using dependency injection. There are 2 different methods of injection:
- Injecting UiModels directly into the parent
- Injecting UiModels creator functions into the parent
The second method of injection is useful when the parent does not know the exact number of UiModel instances, especially for representing lists. In addition to this, it might also be used for conditional creation scenarios. If a UiModel does not need to be part of the parent in all scenarios but could be created optionally in some certain cases, then there is no need for direct injection of UiModel itself to the constructor. It might make sense to inject the creator function and execute it when necessary to create an instance.
How to pass parameters to child UiModels?
UiModels are created to be self-contained as much as possible. However, they may still need some dynamic parameters from the parent which cannot be passed through the constructor. Here, the easiest solution is to have a method that accepts parameters from the parent on initialization. We apply this solution so that when a child UiModel needs some dynamic initialization parameter from the parent, it can accept it through an initialize(param: Type) method. For example, if a UiModel needs viewModelScope to run some coroutines then it is passed down through the hierarchy to child UiModels. Hence, when the parent ViewModel scope is cancelled all UiModels coroutines are also cancelled.
How to bind data to the Views?
One of the main benefits of MVVM is that ViewModels don’t have any references to the Views. This means we can implement different Views which use the same ViewModel. In other words, we can replace our View implementation without changing our ViewModel. If we consider the current paradigm shift in Android UI development (Jetpack Compose), which tends to compose Views using Kotlin, we can clearly see the benefit of having ViewModel abstraction. If we want to switch to Jetpack Compose at some point, in an ideal scenario the only part that should be changed will be the View part.
Since all our views are using Android XML layouts and we employ data binding, our example will also follow the same. But it can be easily adapted to Jetpack Compose. Let’s continue with our Rectangles example here and try to sketch the sample skeleton for binding data to our Views.
We provide two examples that show how to bind UiModels to XML Views. To keep it simple and clear, some XML layout parameters are removed and only parameters that are related to the example itself are kept. These examples show how to construct the UiModel hierarchy.
In this article, we elaborate on the idea of composing ViewModels. We have already experimented with this approach in our app even in some complex views such as our cart and checkout screens, and we have seen several benefits of this approach. Let’s conclude the article with some of these benefits:
- Reduced complexity in parent ViewModels: The parent ViewModel has become a host for child UiModels, and generally it only works as a coordinator between them.
- Local dependency injection in child UiModels: There is no more need to inject everything into a big ViewModel. Since all UiModel hierarchy supports dependency injection we can inject local dependencies to child UiModels. This helps us have maintainable constructors that accept only relevant dependencies. Rather than having a big ViewModel that has a constructor with several dependencies, it makes more sense to have children UiModels which have constructors with a few and more relevant dependencies.
- Easy to add/remove views from an existing UI: Our app is evolving and we are always adding new features and removing existing ones when they are not needed anymore. In order to be able to handle such a dynamic environment, it is crucial to structure your code so that it is easy to add and remove features. More granular UiModels helps us easily add new views by composition. Moreover, it is really easy to remove a UiModel from the parent when it is not needed anymore.
- Easy to test: When UI becomes complex, then it also becomes hard to test it. With the help of granular UiModels, complex UI logic can be split into more consumable chunks. This does not only reduce the complexity of testing logic but also helps to focus on what to be tested. It is quite easy to miss test cases when you are dealing with a big ViewModel. However, with small UiModels it becomes easier to populate and cover all the test cases.
- Reusability: The main purpose of composing ViewModels is not reusability. However, sometimes there might be some reusable components that are used in different screens of the application such as progress views, error views, etc. For example, it is possible to create UiModels like ProgressUiModel and ErrorUiModel so that you can inject them into relevant screen UiModels along with their views when needed.