Table of Contents
Today we will talk about Android Architecture and will rethink the use of MVP in Android. When we talk about software architecture, in Apiumhub we always have in mind the following objectives:
- Improve testability, expressiveness and reusability.
- Reduce mocking / stubbing, linkage and development costs.
Establishing these objectives, we prioritize the different solutions that follow 4 rules of the simple design, which are mentioned by descending priority:
- Write code that passes the tests
- Code reveals intentions
- Eliminates duplicity
- An architecture with the minimum number of elements
With these two pillars in mind, we can begin to consider what architecture would benefit us when developing mobile applications.
Rethinking MVP in Android
(M)VB – Model-View-Backend
Before starting to evaluate the most used architectures in the industry and discuss our approach, we would like to emphasize that one of the most important thing to take into account when developing is pragmatism.
This, applied to the world of architecture means that we must be aware at all times of what kind of product we are developing and what are the needs, in the way that if, for example, we have an application that queries data from a server and that shows it on screen, without having a database, complex transformations on this data, concatenated calls, reusable components, etc … the simplest thing would be to use an architecture that uses the least number of layers and components, that read from the server and paint on the screen ; a Model-View-Backend architecture.
MVP – Model-View-Presenter
When we talk about mobile architecture, the starting point is usually the MVP pattern. A few years ago and before the arrival of the MVVM, MVP in Android was the industry standard for building the application layer of our application.
MVP in Android was born as a result of the need to solve the problem that constituted having Activities that were God Objects, where all the logic of the application reside, the network calls, the storage in BD / shared preferences, etc …
We do not want to delve much into the details of this architecture, as it is quite popular and known to all, so we will only discuss the MVP in Android advantages and disadvantages:
MVP in Android Advantages:
- Decoupled view of the other components
- Presentation logic unit-wise
- Reusable views and presenters
MVP in Android Disadvantages:
- Bidirectional coupling. The presenter knows the view and the view knows the presenter. In addition, the presenter has dependencies with the services.
- When this coupling exists, to test the presenter we have to mock the services and the view.
- A presenter has dependencies with N services, which complicates the testing of it since each service that we add implies more mocking in the tests.
- There is a tendency to store intermediate states in the presenter (for example, the state of a form), which leads to difficulties when testing this presenter since we have to configure these states to cover all possible cases.
MVPBinder
As we have seen in the classic MVP, the biggest disadvantage of the classic MVP is the increase of the difficulty to test a presenter as it grows; that is why we decided to evolve it until we can decouple it from services.
For this we created a class “Binder”, which is a subclass of the presenter that allows us to link service events with a response in the view, and thus avoid having dependencies with the services in the presenter, so that to test it we do not have to mock the same ones. An example of a Binder class would look like this:
class Binder(view, service): Presenter(view) {
init {
bindSearch { query ->
service.search(query,
this::onGetRepositoryListNext,
this::onGetRepositoryListError
)
…
}
}
The bindSearch method is defined in the presenter and is defined as:
fun bindSearch(func: (String) -> Unit)
Likewise, the onGetRepositoryListNext and onGetRepositoryListError methods are also defined in the presenter and are these:
protected fun onGetRepositoryListNext(list) {
view.hideLoading()
if (list.isEmpty()) {
view.showEmptyData()
} else {
view.showData(list)
}
}
protected fun onGetRepositoryListError(error) {
view.hideLoading()
when (error) {
is ConnectException -> view.showNetworkError()
else -> view.showOtherError()
}
}
Adopting this architecture, these are the advantages and disadvantages that we find:
Advantages
- Decoupling between the presenter and the services. What facilitates the reusability of the presenter.
- It is not necessary to mock / stubbear the service. What facilitates the testing of the presenter.
Disadvantages
- We continue to maintain a dependency between the view and the presenter, so we have to mock the view to test the presenter (although we only do verifications and do not define behavior for it)
- If we have more than one service, our Binder will have those N services as a dependency and may grow indefinitely.
- There is still a presenter in which we can store intermediate states, which complicates the testing of it.
MVB – Model-View-Binder
After the adoption of the presenter with the binder we realized that the same investment of dependencies that we made between the presenter and the services, could be done between the presenter and the view (s). In this way we would have a completely decoupled view of the presenter, as well as a presenter decoupled from both the view and the services. The sole responsibility of this presenter (from here on we will call him Binder) will be to link the events that issue the view with calls to the service, and the events that the service issues with the methods of the view. A Binder of these characteristics would be like this:
class Binder(view, service) {
init {
view.bindSearch(service::search)
service.bindData(view::showData)
service.bindEmptyData(view::showEmpty)
service.bindNetworkError(view::showNetworkError)
}
}
In this way, we could have N binders where each one links a view-service pair, so that we could have for example a Binder that links the events of the view that we want to track in an analytical service with a service that does the same, and have another Binder to obtain data from that same view. In this way the Binder to track events (along with the interfaces of the view and the service) would be reusable in all the views that we want and we will eliminate code duplication.
The events that a view / service emits as well as the information it receives would be defined in an interface such that:
interface RepositoryListView {
fun bindSearch(func: (String) -> Unit)
fun showLoading()
fun hideLoading()
...
}
interface RepositoryListService {
fun search(query: String)
fun bindData(func: (List) -> Unit)
fun bindEmptyData(func: () -> Unit)
...
}
This architecture makes it much easier for us to test our presentation layer, since it completely eliminates the dependencies with views and services (the only dependency is on the Binder, and testing it would be redundant, since it is little more than a configuration file) and for both eliminates the need to mock them.
It doesn’t only eliminate the mocking, but the way to test both our services and our views is to verify that, for an event of entry X, a certain value is emitted through the output event Y. In this way we have a Unified form to test a large part of our application, no matter how complex the services or the views are.
In addition, we eliminate the temptation to have intermediate states in intermediate layers (like a Presenter or a ViewModel), since we can only store them in the view (information that concerns only the view) or we will have to take it to the outer layers of our application (for example a cache in memory) and access them through a repository.
Thanks to this architecture, we solve the problems that we saw in the previous ones, having only one disadvantage: the complexity of the services has increased with respect to the previous ones; a disadvantage that we mitigate intensively testing our services.
Next steps:
- We are working on a DSL that defines input and output events and, through adapters, we can work with RxJava, Corrutinas / canales, etc …
- Delegates that allow us to define these events and their entry and exit points
- (Perhaps) An annotation system that allows us to binde these events without the need to write us the corresponding code
Side note: Many of the code used to write this article can be found on this repository.
Don’t forget to subscribe to our monthly newsletter to receive the latest news about mobile architecture and MVP in Android in particular!
Author
-
Android developer with over 8 years of experience on the platform. Passionate about software architecture, best practices, design patterns & teamwork.
View all posts