How Runtastic Developers Chose the Proper Architecture for Our Android Apps
Written by Andrii Khrystian, Android Developer
Architecture is always difficult and the subject of many discussions. Throughout the history of Android development there have been a lot of different approaches, depending on projects and features. Most of us can remember a time when there was no architecture, when developers focused all business logic on Activity or Fragment. At one time there were some data managers and HTTP calls moved to ApiManagers, etc. There were also some libraries like Mortar and Flow that gave us some architectural solutions and an idea of how architecture could be done better.
At Runtastic we used our custom Model-View-Presenter solution. It was a nice approach and helped us reach one important goal: testability. But when Google released Architecture Components, we decided to re-evaluate our approach. However, choosing solutions is even more complex than implementing them. There are millions of thoughts about the architecture; each solution could lead to hours of discussions. We decided the best approach was to implement different solutions and just compare them.
First, we sat together and defined the requirements for our future architecture:
- Easy to use: architecture needs to be clear for all team members and newcomers
- We need to have fully testable components
- No overhead for simple use cases
- At the same time, we need to support complex use cases
- Support state management for different components
- Components should be well defined and should have clear and solid responsibilities
- Has to work with Android, not against it
For implementation we’ve chosen following architectures:
- Model — View — Presenter
- Model — View — ViewModel
- Model — View — Intent
- Without any architecture
We set up a simple use case which was similar to features we work with every day, like a shopping cart screen where you can add and remove items. Additionally, the state of the cart needed to be maintained throughout configuration changes.
MVP is a typical and well established architecture in modern Android development, and the basic idea is to split logic within different components. This architecture was the first architecture that brought consistency and readability to the code. It’s also possible to write proper tests. MVP defines several components with different responsibilities.
The first one is the Model. In our case, the model is responsible for the data layer. In some examples people are using the Interactor, but it’s not the same. For such cases the combination of Interactor and any data classes creates the Model. I’ve also seen examples where some responsibilities of the Model were taken by the Presenter, which is not correct, in our opinion. So what is the Model in our shopping cart use case?
It looks straightforward. We have functions for the data access, such as items() and also action functions, which can update our data. We also have such functions as isEditing() which requests the state of the model.
But what is the state? I would say the state question is one of the most discussed questions when defining the architecture. To answer this question let’s check the implementation of some functions in the Model.
ModelState is a just simple model, which holds all items and IDs of items to delete.
Model takes as parameters the DAO object and instance of Parcelable? to store the model state.
Here is the algorithm for how we deal with the ModelState. Immutable state is a BehaviorSubject which is responsible for handling model changes.
The second component is the Presenter. In our implementation the Presenter is a bridge between Model and View. So we have here only one function:
and implementation is super clear and easy:
At the same time, it subscribes to the View actions and Model observables. As you can see, there is no communication callback hell between Model and Presenter. So let’s check what is going on in the View.
These functions are used for subscribing to UI actions and updating RecyclerView Adapter. In our case we use RxJava BehaviorSubject. The way to update views could be different, depending on the use case.
A few more words about the model state: as soon as we have an instance of the Model in our Activity, we have also an instance of ModelState, and in that case, saving is really simple to implement.
for saving, and to restore instance we can get it from Bundle in onCreate function:
Let’s make a small summary of components:
Model has access to data, provides an interface to perform actions, has a state, and provides an interface to update the state.
Presenter updates the model, notifies View about changes, has instances of View and Model.
View initializes all components, calls Presenter function bind(), produces Observables for the Presenter.
And what about testing? This architecture could be easily covered by unit tests. MVP looks simple and straightforward. But maybe there’s something better?
MVVM has established itself as a popular architecture pattern over the last few years. It might seem that this architecture is similar to MVP, and we just replaced Presenter by ViewModel, but that’s not exactly not true. The main differences are the responsibilities of the components.
Model looks different. Now it provides stream to get data and also can delete items depending on the ID.
Now most responsibilities have moved to ViewModel. ViewModel especially handles actions:
As you can see, ViewModel converts data to the UI interpretation. Implementation is also really simple, and it is important to mention that we have state handling in the ViewModel.
It’s similar to how we did this for the MVP.
The view looks completely different:
It’s not really obvious, and honestly we don’t really need an interface here; it could be just a private function. But let’s keep it for consistency. The most interesting part here is the implementation, of course:
As you can see, everything is here — receiving results from ViewModel, and also subscribing to the UI Views events. In our case, this function returns Disposable, to dispose of all subscriptions inside onStop() function. It looks much nicer than a bunch of overridden functions. Let’s summarize MVVM’s key points:
Model has access to data, has an interface to change data, notifies ViewModel when something was changed, doesn’t have any references to View or ViewModel, is fully independent.
ViewModel has an instance of the model and can ask for data and change the data, responsible for mapping data to UI, has a state.
View initializes all components, calls ViewModel functions to update the model, subscribes to data changes, and provides UI interactions to the ViewModel.
Also components are easily testable.
As a bonus, we also tried MVI (Model-View-Intent) architecture. To be honest, I really like this one. For the MVI we chose a simple use case. We just click on the button, and we should update the TextView.
Because of the use case, our MVI should also be as easy as possible. So we have ViewModel, Intents, ViewStates (in our case it’s just one view state). First, I want to write a bit about Intents. It’s important to mention that they don’t have anything similar to Android Intents. Here Intent is just an Object, which is produced by the View. Depending on the type of Intent, we decide what to do next . We can update data or, in our case, just post another object, which is called ViewState, to update the user interface. Let’s look at some code:
Here we restore ViewState and pass it as an argument to the ViewModel. In onStart we call function processIntents to prepare ViewModel to receive Intents. And also here we are subscribing to ViewState updates:
and adding to subscriptions, of course. One more important part is Intents initialization:
Here we just observe UI events and map them to a particular Intent. In ViewModel function, proccessIntents looks like this:
So as I mentioned above, depending on the Intent we change/produce ViewState.
Originally MVI was more complex. If we have a data layer, we had to implement Results, and the producer. That’s it.
It’s always difficult to compare architecture, and it’s even more difficult to find a common solution which would be suitable for everybody. When developing a real feature, you can always find some problems which are not obvious in simple cases. Let’s compare these architectures:
Green — yes, red — no, and orange — somewhere in between. A few details:
Easy to use
MVP is one of the first architectures for Android developers; it’s easy and understandable.
MVVM is on the same level as MVP. Not too many components are required to create this architecture.
MVI is kind of a new architecture and has a lot of different components, especially for huge features. It could be difficult to understand for Junior developers or beginners.
With MVP it is difficult to define strict rules for the Presenter, View, and Model. The view could have a bunch of overridden functions, which can relate to Presenter too.
MVVM responsibilities are clear. Here it is possible to define rules for each component.
MVI is even more strict than MVVM, because of the number of components.
MVP is difficult to read, understand, and support because of overridden functions in View, and communication between Presenter and View.
MVVM is really consistent if we use good communication between components. But if callbacks are used, it would be the same mess as MVP.
MVI rules are strict and code is consistent.
All architectures could be well-tested
No overhead for simple use cases
All architectures add overhead to development. To implement some easy features, we have to create all of those components and define communication between them.
By default, there is no State management for architecture, but it is easy to implement all of them.
Given the different use cases at Runtastic, our features range from being really simple to complex. None of the architectures above were flexible enough to support us without adding too much overhead.
We came to the conclusion that we need to have a flexible architecture as well. Just like Lego, we want to have building blocks which can be combined. And it’s up to the developer to choose which bricks (= components) are required to build a feature. We based our solution on MVVM because it already has well-defined components, is easy to use, and testable.
To summarize, we are not happy about “Overhead for simple use-cases”. So we decided to define a couple of options for our architectures:
- No-Arch / View Only
Use cases without any model or business logic require no special architecture.
- MVVM “light”
| Data layer |—| ViewModel |—| View |
- Dynamic model
- No user input
- No State / State can be restored from a persisted source or Android Widget
- Navigational Actions
- User Actions (Delete, Add)
- Unit Tests (ViewModel, Repo)
- UI Tests (View)
| Data layer |—| Model |—| ViewModel |—| View |
- Dynamic model
- User input mutates the model
- Model is state-ful
- Navigational Actions
- User Actions (Delete, Add)
- User Input (Text Input, Sliders, etc.)
- Unit Tests (ViewModel, Model, Repo)
- UI Tests (View)
From our perspective, there are only two options for communication between components: RxJava and LiveData, here you are free to choose.
Hopefully what we learned can be useful and help you choose proper architecture. Happy coding!