MVI for Compose (Part 1)
- Problems and benefits of MVVM (you are here)
- Custom MVI
- Practical examples, continued implementation
- Automating/reducing boilerplate
- Best practices and conclusions
Normally, when you develop a mobile application, it is necessary to make a number of technical decisions, such as which technology stack to choose, how to organise code review and work in a team, and what to do to eventually make the project grow without increasing amount of time spent on changes and make onboarding help new people understand and accept rules in a team faster.
About a year ago our project entered the European market and we made a decision to establish these rules. One of the changes was introduction of Jetpack Compose into our application. We started to use Compose together with Android ViewModel, which provides helpful functionality out of the box, but the level of freedom it could give didn’t suit us. The solution was to write our own MVI implementation based on ViewModel in order to accelerate the development process in the future and establish the rules for employees in the mobile department to follow when developing our application.
In this article, I am going to share the problems we found while working with Android ViewModel and the solution, which we developed for Jetpack Compose.
DISCLAIMER: The implementation described in this article is the result of work on a production project with certain requirements. The code and implementation are not a silver bullet and can be modified for different cases or even written in another way.
ViewModel usage and problems
Briefly speaking, there are no major problems. The tool is fully suitable for working with Compose, state storing and user actions processing on the screen. The thing that did not suit me when working with ViewModel:
- Variety of different observable data holders (LiveData’s, single LiveData, StateFlow, MutableState)
- A large number of functions inside ViewModel and violation of Single responsibility principle.
- Parallel and simultaneous update of UI state from anywhere in ViewModel
Let’s have a closer look
Different observable data holders
Currently, there are a few options to subscribe for screen state change.
Here is a little about their pros and cons.
Several LiveData containers
Pros: It is a convenient approach for describing the state of different parts screens, each LiveData container contains a certain type of data and triggers the update of only the necessary screen components.
Cons: ViewModel contains a large amount of data and a few places where single LiveData can change. Different LiveData change independently of each other and can lead to inconsistent UI.
Single events require a custom SingleLiveEvent implementation.
Subscription is required in Composable function for each LiveData container.
Single LiveData state
It basically has the same problems as several LiveData, but what is different now is that there is one LiveData now and it contains the whole screen state, you can forget about a large number of LiveData fields and multiple subscriptions at @Composable function.
StateFlow
It is similar to a single LiveData distance, with the only difference being is that it doesn’t know anything about lifecycle of Activity/Fragment and requires additional steps in @Composable. It is described quite well in Manuel Vivo’s article.
MutableState
It is an observable container for working with recomposition Compose. It is easier to subscribe to it from UI, but it loses all the advantages of Flow api. There is also a problem with ViewModel becoming dependant on Compose library.
If usage is not limited by static analisys tools your project might end up with every single approach above, which later will take time to refactor.
A large number of functions inside ViewModel and violation of Single responsibility principle.
The MVVM approach does not regulate writing functions inside ViewModel in any way. Without regulatory engineering practices in all view models, you are going to see multiple public and private functions which perform several things at once, and how side effects update the state of your UI. In the case of RxJava and using Subject your code can become a labyrinth of asynchronous events. Debugging such code is real bet very energy consuming.
In order to fix these problems, one has to separately discuss engineering practices with the team. However, if the code does not regulate its own writing and there are no tools for static program analysis, over time ViewModel will become a place which is going to be difficult for one to sort out.
At the same time. ViewModel can violate single responsibility principle. Apart from storing screen state, it is also an interface for running operations and changing screen state.
Simultaneous UI update in ViewModel
If screen state is declared as single class then the more complex the screen logiс is. the more methods are involved in changing UI state. In this case the approach with several LiveData makes maintenance easier as the code shows what exactly changes state UI. With most uni directional flow approaches there is one state and it is affected by all operations. Debugging access to fields can be helpful. but it become a routine task due to the presence of Race condition.
So, does ViewModel have any benefits? The answer is yes. Its ability to outlive (survive) UI, usage of SavedStateHandle, and presence of ViewModelScope are three features which ViewModel has and which are going to help us implement MVI paradigm.
More about using them in my next article!