Architectures of Android applications

A pragmatic approach to choosing one

After the first couple of months of development most Android beginners start thinking about how to keep the code of their applications maintainable. From the Web and their experienced colleagues they learn about a number of architectural approaches. One question, however, remains unanswered and might puzzle not only novices: “How do I choose an architecture from the abundance?”

In this article I provide a brief overview of several popular architectures and share my thoughts on how they could be compared based on some significant criteria. I also describe a perspective on the available architectures that the comparison leads me to and some resources that could be useful for further exploration of the field.


Architectures

In this part I describe the following architectural patterns: Forms & Controls, MVC, MVP, MVVM and MVI. For each of them I describe the list of entities and their relationships.

1. Forms & Controls

Although the approach is not usually referred to with this name, it is quite common and familiar. The name was suggested by Martin Fowler in GUI Architectures in order to describe how people write code in most GUI environments if they only rely on the platform abstractions.

In all such environments there is always an abstraction of a screen which could be generally called Form and a set of UI elements, like buttons, spinners, checkboxes, etc., which could be called Controls.

A Form defines the layout of the Controls hierarchy and contains behaviour logic of the screen:

In Android the notion of Form is represented by Activity component. Indeed, it allows a developer to set the layout with setContentView(int) and put the rest of the logic in the Activity class as well: reaction to the user’s gestures, clicks and inputs, persisting data, interaction with remote APIs, etc.

So, whenever you write an application using only a bunch of Activities you follow an architectural pattern :)

2. Model View Controller

This and all subsequent approaches have a common entity layer — Model. Model is focused on the application logic in its most abstract form and does everything but dealing with UI. It is presentation-agnostic, i.e. Model classes do not use anything platform-specific.

In contrast, the other two entities are responsible for the UI-part of the application. Every UI element is conceptually split onto a Controller and a View.

The Controller part of a widget listens to the user’s gestures, clicks and inputs and does appropriate interactions with Model in response to them. E.g. if the user clicked a download button, then the button’s Controller would react to the click and ask the Model to download necessary data.

The View part is only responsible for presenting the state of Model to the user.

Model has to provide an observable state for View and keep it in a consistent state for correct presentation.

3. Model View Presenter

In this approach the scale of presentation logic is not a separate widget as in MVC, but the whole widget hierarchy of a screen.

A View in MVP provides a simple interface for getting and setting widget states in such hierarchy. It does not know anything about Model.

All user inputs are delegated to a Presenter which interacts with Model in response to them, just like Controller in MVC. However, unlike the latter, Presenter sets the state of UI by means of the View’s interface. It also waits for the results of the operations it initiates in Model if they result in a UI change.

4. Model View ViewModel

Like in MVP, View is the layout of a screen. The difference is that now it observes ViewModel’s state and changes UI accordingly.

Similar to Presenter in MVP, ViewModel is handed off all user inputs and reacts to them by interacting with Model. In MVVM, however, ViewModel doesn’t interact with View directly. It simply maintains an observable state for View, like Model in MVC.

5. Model View Intent

You might think that in this architecture an Intent is an intermediary between View and Model. In fact, the intermediary is called Presenter again, but it did not deserve a letter in the abbreviation.

You could also assume that the approach implies a peculiar usage of Android Intents which are used for communication between Activities, Services, etc. which is also far from being truth.

Actually, Intents is just a name for user inputs (clicks, gestures, value changes in text fields, etc.) View provides an interface which allows Presenter to register itself as an observer of the Intents. As usual, it reacts to the inputs by interacting with Model, but what is new in the approach is the way in which UI updates happen.

As Presenter observes Intents and interacts with Model it generates a sequence of ViewStates which are supposed to be rendered by View. A ViewState is an immutable POJO that is sufficient to describe an instant state of a screen.


Comparative Analysis

Once we have considered a variety of architectures, it is the time to understand essential distinctions between them.

1. Criteria

Let us compare the described architectural patterns based on the following list of characteristics:

  • Abidance to Single Responsibility Principle. Classes violating the principle tend to be long and hard to read which makes SRP an important criterion.
  • Presence of classes with complex mutable state. These are evil for two primary reasons: 1) states require additional efforts to persist them at some points in Activity’s lifecycle; 2) a multitude of fields, like isRefreshing, isCheckBoxEnabled, etc., makes a developer to read and keep in mind many places in code in order to build a coherent image of what is happening in the class. In this regard, the criterion contributes to what could be called architecture complexity.
  • Testability. By this I mean the possibility to write non-instrumented unit tests, i.e. which could be run without a device or an emulator. Platform classes, like Activity or Service, cannot be run in this way because they require a genuine instance of Context for their work. But running non-instrumented tests is faster and that’s why I consider it to be an advantage when an architecture encourages developers to put more logic in platform-agnostic entities.
  • Auxiliary libraries. Some architectures require developers to use a library for practically viable implementation. The library becomes an additional requirement for any developer that would like to join a team using the architecture. That could be considered as another component of architecture complexity.

2. Comparison Results

The table given below puts together categorical estimates for the architectures under consideration based on the criteria from the previous paragraph.

SRP column contains only two categories because all architectures either don’t have classes abiding to SRP (as F&C where the only entity is Form; Controls are usually provided by the platform) or have at least one entity violating the principle. The cases correspond to the categories “bad” and “normal” respectively. Category “good” would correspond to a pattern where all entities have only one responsibility. It turned out that there is no such pattern.

In MV* architectures SRP violation follows two patterns:

  1. An entity performs a role an intermediary or Model and maintains an observable state for View. (Model in MVC and ViewModel in MVVM.)
  2. An intermediary is reacting to user inputs by interacting with Model and manages UI state. (Presenter in MVP and MVI.)

Testability is said to be “good” if the given architecture has platform independent entities. Otherwise it is given “bad” category.

By complexity in the table I mean the combination of two factors: presence of classes with complex mutable state and necessity to learn additional libraries.

Every architecture has at least one source of complexity:

  • Complex mutable state: Form in F&C, Model in MVC, Presenter in MVP and ViewModel in MVVM.
  • Auxiliary libraries: MVC and MVVM require a mean for data binding (Android Data Binding Library or RxJava); MVI requires a good implementation of Observer pattern with powerful operators for manipulating observable streams of data (currently only RxJava).

An approach with one source of complexity is considered to be “normal” and “bad” if it has both sources.


Conclusions

By looking at the results table one can notice three groups of architectures with similar distribution of categorical estimates:

  1. F&C. Lack of testability and harsh disregard of SRP makes this approach a bad choice for real life projects. However, due to relative simplicity it is a suitable pattern for experiments and test projects.
  2. MVC and MVVM. The architectures could be used in large projects: both can be covered with unit tests and provide some convenient entities with a more or less focused purpose. But at the same time they are relatively complex in the above mentioned sense: in both developers have the bother to deal with states and have to know a data binding library.
  3. MVP and MVI. While having advantages of the previous group, the architectures can be caracterized as relatively simple. MVP does not require you to learn any additional tools and MVI allows to get rid of complex states.

P.S.

Some of the statements and conclusions in this article might seem arguable. Nevertheless, it demonstrates which kind of analysis one could carry out in order to make a sensible choice of an architectural pattern for an Android project. Don’t stick to buzzwords and don’t trust to “authoritative opinions”. Think for yourself and stay critical.

Resources

Like what you read? Give Maxim Blumental a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.