From the translator: - I have long been interested in how to make the code of Android applications cleaner, and this is probably the first article, after which I didn’t have any thoughts: "Why is this all here?" and "Did he ever try to use it in his life?" Therefore, I decided to translate, maybe someone else would be useful .
Write Hello World is always easy. The code looks simple and straightforward, and it seems that the SDK is very adapted to your needs. But if you have experience writing more complex Android applications, you know that this is not the case with working code. You can spend hours trying to understand why your shopping cart is not updated after changing the orientation of your phone if WiFi is not available. You assume that the solution to the problem may be to add another
if
in the 457-line
onCreate()
method of your activation - somewhere between the code that fixes the crash on Samsung 4.1 on board, and the one that shows the coupon on $ 5 on the user's birthday. Well, there is a better way.
In Remind, we roll out new functions every two weeks, and in order to maintain this speed and high quality of the product, we need a way to keep the code simple, maintained, divided
. "decoupled", in the sense of loose coupling, and tested. Using the MVP architectural pattern allows us to do this and focus on the most significant part of our code - our business logic.
MVP , or Model-View-Presenter, is one of several patterns that contributes to the division of responsibility when implementing a user interface. In each of these patterns, the roles of the layers are slightly different. The purpose of this article is not to describe the differences between the patterns, but to show how this can be applied on the android (by analogy with modern UI frameworks such as
Rails and
iOS ), and how your application will benefit from this.
')
An example of code that illustrates most of the approaches described below can be found here:
https://github.com/remind101/android-arch-sampleOld School Android
The division of responsibility, which is implied by the Android framework, looks like this:
A model can be any POJO, a
View is an XML markup, and a fragment (or initially activated) acts as a
Controller / Presenter . In theory, this works quite well, but as soon as your application grows, a lot of code related to the Presentation appears in the Controller. This is because not so much can be done with XML, so that all data binding (data-binding), animation, input processing, etc., is performed in the fragment, along with business logic.
Everything becomes even worse when complex interface elements are placed in lists or grids (GridView / GridLayout means, in general, "grid elements"). Now the adapter is responsible not only to store the view and controller code for all these elements, but also to manage them as a collection. Since all these elements are tightly coupled, they become very difficult to maintain and even harder to test.
Enter the Model-View-Presenter
MVP gives us the opportunity to highlight all that boring low-level Android code that is needed to display our interface and interact with it, in the
View , and the higher-level business logic of what our application should do, evict to
Presenter .
To achieve this on an android, you need to consider the activation or a fragment as a presentation layer, and provide a lightweight presenter in order to control the presentation. The most important thing is to determine the responsibility of each layer, and standardize the interface between them. Here is a general description of the division, which works very well with us:
The presentation (activation or fragment) is responsible for:
- Creating a copy of the presenter and the mechanism of its connection / disconnection;
- Alert the presenter about important life cycle events for him;
- A message to the presenter about the input events;
- Placing the views and connecting them to the data;
- Animations;
- Event tracking;
- Switch to other screens.
The presenter is responsible for:
- Loading models;
- Saving the reference to the model and view state;
- Formatting what should be displayed on the screen, and instructing the view to display it;
- Interaction with repositories (database, network, etc.) ( approx. Lane. Repository is a pattern, just in case);
- Determining what to do when input events are received from the view.
Here is an example of what the interface between a presentation and a presenter could be:
interface MessageView {
There are a couple of interesting points to consider about this interface:
- Methods for updating the presentation should be simple and aimed at a single element. This is better than having one
setMessage(Message message)
method that will update everything, since the formatting of what needs to be displayed should be the responsibility of the presenter. For example, in the future you will want to display "you" instead of the username if the current user is the author of the message, and this is part of the business logic. - Presenter's life cycle event methods are simple and should not reflect the true (over complicated) system life cycle. You are not required to process any of these. But if you want the presenter to perform some actions at different stages of this cycle, you can process as much in it as you see fit.
- Input events at the presenter must remain high level. For example, if you want to define a complex gesture, for example, a three-finger swipe, this and other events should be determined by the presentation.
- You can pay attention to the
MessagePresenter.onAuthorClicked()
and MessageView.goToAuthorProfile()
methods. The presentation implementation will likely have a clicker that will call this method of presenter, and that in turn will call goToAuthorProfile()
. Is it not necessary to remove all this and go to the author's profile directly from the click of the miner. Not! Deciding whether to go to a user profile when clicking on his name is part of your business logic, and the presenter is responsible for this.
As it turned out in practice, if the code of your presenter contains the code of the Android framework, and not just pure Java, you are probably doing something wrong. And accordingly, if your presentations need a reference to a model, apparently, you are also doing something wrong.
As soon as the question of tests arises,
most of the code that you need to test will be in the presenter . What is cool is that this code does not need Android to run, since it only has links to the presentation interface, and not to its implementation in the context of Android. This means that you can simply get up the presentation interface and write clean JUnit tests for business logic that check the correctness of calling methods on the wet presentation.
This is how our tests look now.
What about lists?
Until now, we assumed that our ideas are activations and fragments, but in reality they can be anything. We did quite well with the lists, having a
ViewHolder that implements the view interface (both
RecyclerView.ViewHolder
, and the usual old ViewHolder for use in conjunction with a ListView). In the adapter, you just need the basic logic for handling the attachment / detachment of presenters (an example of all this is in the git repository).
If you look at an example of a screen containing a list of messages, download progress, and an empty view, the division of responsibility will be as follows:
- The list presenter is responsible for loading messages and displaying list / progress view / empty stub display logic;
- The fragment is responsible for implementing the logic for displaying the list of views / progress / stub and switching to other screens;
- The adapter matches the presenters with their ViewHolder;
- The message presenter is responsible for the business logic of the individual message;
- ViewHolder is responsible for displaying the message.
All of these components are loosely coupled and can be tested separately from each other.
Moreover, if you have a message list screen and a detail screen, you can reuse the same message presenter and just have two different implementations of the presentation interface (in the ViewHolder and the snippet). This saves your DRY code (
approx. Lane - “Don't Repeat Yourself”, or “Do not repeat”, who does not know).
Similarly, a view interface can implement custom views. This allows you to use MVP in custom widgets to reuse it in different parts of the application, or simply break complex interfaces into simpler blocks.
MVP and configuration changes
If you’ve been writing for Android for some time, you know how much pain the support for changing orientation and configuration delivers:
- Fragment / activation should be able to restore their state. Every time when working with a fragment, you should ask yourself how this thing should act when changing orientation, what should be placed in the saveInstanceState bundle, etc.
- Long operations in background threads are very difficult to do correctly. One of the most popular errors is to keep the link to the fragment / activation in the background thread, since it needs to update the UI after the work is completed. This leads to a memory leak (and, probably, a drop in the application due to an increase in memory consumption), as well as to the fact that the new activation will never receive a callback and, accordingly, will not update the UI.
Proper use of MVP can resolve this issue without having to think about it at all. Since presenters do not have a strong reference to the current UI, they are very lightweight and can be restored when the orientation changes! Since the presenter stores the link to the model and the state of the view, it can restore the desired state of the view after the orientation change. Here is a rough description of what happens when the screen is rotated, if this pattern is used:
- Initially, the activation was created (let's call it the "first instance");
- A new presenter is being created;
- Presenter is attached to the activit;
- The user clicks the "Download" button;
- The presenter runs a long operation;
- Orientation changes;
- Presenter decouples from the first copy of the activit
- There are no more references to the first copy of the activation, and now it is available to the garbage collector;
- Presenter saved, background operation continues;
- A second copy of the activation is created;
- The second copy of the activity is tied to the presenter.
- Loading is completed;
- Presenter updates the presentation (second copy of the activation).
How to save fragments between changes of orientation can be seen in the repository in the class
PresenterManager
.
Total
Yes, this is the end. I hope it turned out to demonstrate how sharing responsibility like MVP will help you write supported and tested code.
Summarizing:
- Separate your business logic by putting it into a naked java-object of the presenter;
- Spend time on a clean interface between your presenters and presentations;
- Let your activations, snippets and custom views implement the presentation interface;
- For lists, the view interface must implement the ViewHolder;
- Test your presenters thoroughly;
- Save presenters when changing orientation.
The implementation of the above can be found in
the ArchExample repository .
There are also many libraries that can help you use this approach, for example,
Mosby ,
Flow and Mortar , or
Nucleus . I advise you to consider them.