Software architecture is probably one of the most popular topics among developers, regardless of the platform you build for. With all the available frameworks and tools for asynchronous programming, dependency injection, networking, data caching and view-bindings, many patterns have evolved and some have stood out from the bunch.
However, it is impossible to find the golden-gem of architecture that will fit everyone, especially if you’re trying to build a sustainable infrastructure as the core. The solution? Constant upgrades and enhancements to fit your needs the best. This story is all about that. It will delineate our transition from one pattern to another, elaborate why we replaced a well worked-out MVP base for MVVM, and how we did it.
When starting a new project, we use our base (template) project as the ground floor. It contains all the elements we’ll certainly use to code an app:
- it imports all the dependencies we’ll use in the project
- it sets up networking/caching/etc. libraries in advance
- it sets up the dependency injection graph
- it contains dozens of utils and helper classes for easier, cleaner and faster coding
- it enforces you to build upon the ground floor and work with the architectural pattern it’s set-up for
Our MVP template project was created 5 years ago and has been constantly upgraded ever since. In our case, every screen we create (assuming there is a business logic) consists of three layers (the Model, the View and the Presenter) and a contract which binds them.
- the View should not contain any business logic and it should only manage the UI (for instance – bind views, programmatically set data which should be shown visually, handle click listeners, text watchers, animations etc.)
- the Presenter represents the layer which triggers the business logic classes, transforms and prepares the data for the View – basically, it is the middle man between the Model and the View
- the Model is responsible for database updates and/or web-server communication – it is a data provider.
- the Contract provides a list of methods which the View or the Presenter should implement – essentially, it enables communication between the two layers
MVP served us well through the years. It is an easy to follow design which deals well with the separation of logic. Unfortunately, it has some drawbacks which start to unfold as more code and more logic gets involved in the MVP triad.
The first problem is the tight-coupling between the View and the Presenter. Each View holds a reference to the Presenter and each Presenter holds a reference to the View. In order for the two layers to communicate, a communication channel has to be opened via the Contract. This approach results in less-generic code (since the Presenter can’t be used as a layer in the MVP triad for another View). In a heap of communication methods, both the View and the Presenter will rapidly expand in the code-line size. There is also an extra layer of difficulty while writing unit tests, since there is a View dependency in each Presenter.
However, the biggest downside for us were the UI updates. In order to update a View component with data which has to be processed, the View must specifically be instructed by the Presenter. In other words, the View can’t “automatically” listen for the Model updates which the Presenter should expose. Therefore, “auto”-UI updates are hardly feasible. In order to succeed, we’d have to moderate the middle layer – the Presenter.
Luckily for us, there are many different sets of tools that can be used to bridge a View and a business logic layer in a more efficient way. It is only a matter of the concept which suits the architecture you want to tailor the most. In particular, to untangle all the hanks MVP tends to create, but simultaneously keep some of its benefits (easy to understand and build upon, easily maintainable etc.) we decided to go with MVVM and, among other things, with theAndroid Architecture Components
Moreover, by switching to MVVM, we were able to resolve all the issues mentioned above:
- the View and the ViewModel (ex Presenter) are no longer tight-coupled
- the ViewModel code is more generic with proper abstractions, we reduced the previously rapid growth of the files for complex screens
- the code is more testable
- the “auto”-UI updates are live
Now, every screen consists of three layers:
- The View – still only handles the UI actions, without any business logic involved in any of its responsibilities
- The ViewModel – handles the communication of the view with the rest of the application (calling the business logic classes) and exposes the states/data to whomever needs to consume it using LiveData
- The Model – a data provider that updates the database or communicates with a web-server
The complete flow is as follows: there is a View on top, which only has a dependency to (a) ViewModel(s). Since there is no longer a one-to-one relationship between the View and (formerly) the Presenter, it is now possible to couple multiple ViewModels to any View, since a ViewModel contains no View references. A ViewModel depends on repositories for fetching/caching data, and a repository will depend on a local and/or remote data source. In testing jargon, there is only one layer beneath the one you’re about to test, that you should mock (with the exception of a repository). Finally, since the states/data is exposed to its listeners via LiveData, we get “auto”-UI updates by simply observing it.
The idea for the transition did not come overnight. Moreover, we want our architecture to be perfectly balanced, so it’s powerful enough to satisfy the requests of highly-complex projects, and yet lightweight enough to be easily upgradeable and maintainable.
While creating the new skeleton, we kept up to the latter as the compass:
- the architecture must leave us enough room to easily add up extra modules (e.g. UseCases etc.) for more complex projects, and it should be sufficient to build smaller and medium projects as is
- the structure we create must be easily testable
- the base set-up should not force us to create or update a large number of files before we even started to work on a screen itself
- we want to reduce boilerplate to minimum (e.g. RxJava calls etc.)
- we want a set of default actions to be present under the hood (e.g. default loader or displaying error messages, default schedulers etc.), but again, those actions must be easily overridable if necessary
- the code must be generic enough to fit all the cases we might encounter and easily adaptable if there is a case which is not directly supported by the foundations
That is a long list to cross, especially if you take into account all the MVP eyesores. We agreed on building a demo project where we’ll try to create the core which will obey our list the most. That project must be simple enough to let us easily experiment with the schemes we try to accomplish, yet robust enough to test all the standard application components (networking, local caching, etc). So, we made a forecast app. The process of putting the things together took about a month, considering all the projects we did aside. It included a lot of brainstorming and mutual consulting, since it’s sometimes hard to see the whole picture if you’re standing too close to the board. We are very pleased with the final result, and it worked very well for the tasks and the scenarios we’ve put it through so far.
Is it perfect? Definitely not. Is it better than it used to be? We believe so. To discover the true advantages and, even more importantly, the disadvantages, we’ll have to build dozens of applications upon it, the same as we did with the MVP skeleton. One thing is certain – there is a plenty of brushing up and improvements our new base projects will have to go through, but we will do our best to maximize the benefits we gain from it – to fasten-up and boost our development speed, performance and quality. Having a base project as a starting point for every new application you make is extremely useful – if you have managed to set it up to be equally powerful as flexible. Besides performance boost, it appoints a well-designed standard you have to obey right from the start, which makes sure your code is consistent, clean and well-written.
Is MVP bad? No. Is MVVM better? It depends. For our needs and the complexity of applications we create, MVVM should help us get rid off some drawbacks MVP entails. On the other hand, new drawbacks regarding MVVM will appear, and we have to be ready to ameliorate them since the benefits of a well-designed architecture will always surpass the disadvantages.
If you are interested in how we assembled our new base project, stay tuned for the next blog which will explain it in detail.
We’re available for partnerships and open for new projects. If you have an idea you’d like to discuss, share it with our team !