Our Android app had grown at a rapid pace, and more growth was on the way. Our UI widgets were defined in the same classes as our API response handlers. Some of our activities were over two thousand lines of code. Activities and Fragments that had originally seemed short, succinct, and well organized had grown into an unrecognizable thicket of Java. We needed to tame this wild growth before our code base became another spaghetti code behemoth. Enter MVP.
As anyone involved in the Android development community is likely aware, the Model View Presenter (MVP) architecture has gained a lot of traction in the past few years (and so has it’s sibling MVVM). Without going into yet another explanation of what MVP is or another discussion of the merits of different architectural paradigms, it’s worth mentioning why we chose MVP. Well, it’s not because we thought it was the greatest architectural paradigm of all time. The important thing for us about MVP isn’t dividing code into a strict hierarchy of models, views, and presenters. What’s important is consistently organized, readable, modular, and maintainable code. Organized code is easier to read at a high level. Consistently organizing code by functionality (e.g. defining business objects or view logic in specific places or packages) makes the task of locating a feature trivial and cuts down on the amount of time developers spend searching for unfamiliar bits of functionality. Interfaces help to achieve all three of these goals; and when all communication between a widget and the logic that controls it passes through an interface, it becomes much simpler to identify the effect that a UI event has on the program’s state.
Our legal team would probably burst into flames if we released our code base (or maybe turn into lobsters, I don’t really know). To prevent the rise of the flaming lobster people, this post will be discussing an MVP proof of concept that our team developed to illustrate how we could implement MVP in our app. The sample app can be found here https://github.com/HotwireDotCom/mvp_starter_kit as part of our MVP Starter Kit.
At a high level, the MVP app allows users to search Wikipedia for article extracts (the first section of any Wikipedia article). It has a search page with an autocomplete field that remembers the user’s past searches, a details page that shows a list of the results from the Wikimedia API that match the user’s search query, and a page that displays the article extract. This involves the following technical features:
Retrieve data from a real REST API
Save data to a local database
Read data from a local database
Navigation between screens
Decoupling the app’s logic layer from all Android SDK classes, making the app testable with JUnit instead of an instrumentation framework
The app uses the following external libraries:
Retrofit for making API calls
RxJava for asynchronous event handling
Activeandroid for ORM
What are these strange things called navigators?
Several of the pages in our production app can transition forward to more than one other page. Technically this means that some Activities in our app display one of several Fragments or launch multiple Activities depending on what the user does. This is navigation behavior. Navigators were created for two reasons. First, creating an interface describing all of a page’s navigation behavior helps to organize the code. It’s much easier to figure out where and why a page is launched when you know that launching that page requires calling one specific method. Second, navigation should be controlled by the applications presentation layer. In order to keep Android SDK classes out of the presentation layer (thereby keeping the presentation logic JUnit testable), presenters need an interface for calling into a page’s base activity. Navigator interfaces make this possible.
How It All Fits Together
The Home-to-Results Flow
The Results-to-Details Flow
Both paths follow a similar structure, and one that has worked well in our production app. There are several key points to note in the diagrams and in the sample app:
Whenever the user causes a UI event, all of the event handling logic is done in the presenter.
When a new View loads and its Presenter needs data, that Presenter is responsible for requesting data.
Presenters are the only modules that request data from the DataAccessLayer and receive their responses.
API calls are treated as Observables, with the Presenters instantiating Subscribers to those Observables.
Data is not passed from one Activity to another. Instead, it is stored in the DataAccessLayer and read when it is needed. While it is not used for deep linking in the sample app, this pattern lends itself well to deep linking.
Presenters control an application’s page flow and are the only modules that call Navigators.
In Android, all that is needed to start a new Activity or Fragment is a reference to an Activity. Rather than spreading out the code for launching Activities and Fragments, it’s all located in Activities and is always accessed through a Navigator method.
It’s been over a year since our team began a page-by-page implementation of MVP across out app. Our original strategy was to decouple the views from their presentation logic by rewriting pages (no copy and pasting lest we create copy pasta) and to write a single Data Access Layer for our app at a later date in time. This phased implementation worked well and allowed everyone on our team to get experience implementing MVP on at least on page. The only problem worth noting about our implementation was trying to write only one presenter for each page. Simple pages with a small number of more-or-less standard UI widgets lend themselves well to having only a single presenter. Pages with many complex, interrelated UI widgets do not work well with only a single presenter. This is because the presenters end up doing too much work trying to handle both the presentation logic of each view and the logic for handling the interactions between each view. While a detailed discussion of the solution to this problem is outside the scope of this post, the simple solution is splitting complex pages into separate modules.