RESS – New Architecture For Mobile Applications
Introduction
Recently it has become painful to look at what is happening in the world of development for mobile platforms. Architectural astronautics is thriving, every hipster considers it his duty to come up with a new architecture, and to solve a simple problem, instead of two lines, insert a few fashionable frameworks.
Ayishnye sites flooded tutorials on fashionable frameworks and over-architected architectures, but there is even no best practice for REST-clients for Android. Although this is one of the most frequent application cases. We want a normal approach to development, too, went to the masses. Therefore we are writing this article.
Then the existing solutions are bad
By and large, the problem of new-fangled MVP, VIPER and the like is exactly one, their authors do not know how to design. And their followers – even more so. And so they do not understand important and obvious things. And they are engaged in ordinary over-engineering.
1. The architecture should be simple
The simpler, the better. The easier it is for understanding, more reliable and more flexible. Any fool can make a lot of abstractions and make a bunch of abstractions, and to do it simply – you need to think carefully.
2. Over-engineering is bad
Add a new level of abstraction only when the old one does not solve the problems. After adding a new level, the system should become easier to understand and the code less. If, for example, after that, you have three instead of one file, and the system becomes more confused, then you made a mistake, and if in a simple way, you wrote garbage.
Fans of MVP, for example, write in their articles in clear text that MVP stupidly leads to a significant complication of the system. And justify it so that it is so flexible and easier to maintain. But, as we know from item number 1, these are mutually exclusive things.
Now about VIPER, just look, for example, at the scheme from this article.
The scheme
And this is for every screen! Our eyes hurt. We especially sympathize with those who at work with this have to face not on their own. For those who have implemented it themselves, we sympathize for a few other reasons.
A new approach
Hey, we want a fashion name too. Therefore, the proposed architecture is called RESS – Request, Event, Screen, Storage. The letters and names are picked up so stupidly for the word to be read. Well and not to create confusion with already used names. Well, with REST consonant.
At once we will make a reservation, this architecture for REST-clients. For other types of applications, it probably will not work.
1. Storage
The data warehouse (in other terms, Model, Repository). This class stores data and processes it (stores it, loads it, puts it into a database, etc. as well as all data from the REST service first get here, parsed and saved here.
2. Screen
The application screen, in the case of Android, is your Activity. In other terms, this is the usual ViewController as in MVC from Apple.
3. Request
The class that is responsible for sending requests to the REST service, as well as receiving responses and notifying about the response of the remaining components of the system.
4. Event
The link between the remaining components. For example, the Request sends an event about the response of the server, to those who subscribed to it. Storage sends a data change event.
Here is an example of a simplified implementation. The code is written with assumptions and was not checked, so there may be syntax errors and typos.
Request
public class Request { public interface RequestListener { default void onApiMethod1 (Json answer) {} default void onApiMethod2 (Json answer) {} } private static class RequestTask extends AsyncTask <Void, Void, String> { public RequestTask (String methodName) { this.methodName = methodName; } private String methodName; @Override protected String doInBackground (Void ... params) { URL url = new URL (Request.serverUrl + "/" + methodName); HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection (); // ... // Do the request and read the answer // ... return result; } @Override protected void onPostExecute (String result) { // ... // Parsim JSON from result // ... Requestr.onHandleAnswer (methodName, json); } } private static String serverUrl = "myserver.com"; private static List <OnCompleteListener> listeners = new ArrayList <> (); private static void onHandleAnswer (String methodName, Json json) { for (RequestListener listener: listeners) { if (methodName.equals ("api / method1")) listener.onApiMethod1 (json); else if (methodName.equals ("api / method2")) listener.onApiMethod2 (json); } } private static void makeRequest (String methodName) { new RequestTask (methodName) .executeOnExecutor (AsyncTask.THREAD_POOL_EXECUTOR); } public static void registerListener (RequestListener listener) { listeners.add (listener); } public static void unregisterListener (RequestListener listener) { listeners.remove (listener); } public static void apiMethod1 () { makeRequest ("api / method1"); } public static void onApiMethod2 () { makeRequest ("api / method2"); } }
Storage
public class DataStorage { public interface DataListener { default void onData1Changed () {} default void onData2Changed () {} } private static MyObject1 myObject1 = null; private static List <MyObject2> myObjects2 = new ArrayList <> (); public static void registerListener (DataListener listener) { listeners.add (listener); } public static void unregisterListener (DataListener listener) { listeners.remove (listener); } public static User getMyObject1 () { return myObject1; } public static List <MyObject2> getMyObjects2 () { return myObjects2; } public static Request.RequestListener listener = new Request.RequestListener () { private T fromJson <T> (Json answer) { // ... // Parsim or deserialize JSON // ... return objectT; } @Override public void onApiMethod1 (Json answer) { myObject1 = fromJson (answer); for (RequestListener listener: listeners) listener.data1Changed (); } @Override public void onApiMethod2 (Json answer) { myObject2 = fromJson (myObjects2); for (RequestListener listener: listeners) listener.data2Changed (); } };}; }
Screen
public class MyActivity extends Activity implements DataStorage.DataListener { private Button button1; private Button button2; @Override protected void onCreate (Bundle savedInstanceState) { super.onCreate (savedInstanceState); button1.setOnClickListener ((View) -> { Request.apiMethod1 (); }); button2.setOnClickListener ((View) -> { Request.apiMethod2 (); }); updateViews (); } @Override protected void onPause () { super.onPause (); DataStorage.unregisterListener (this); } @Override protected void onResume () { super.onResume (); DataStorage.registerListener (this); updateViews (); } private void updateViews () { updateView1 (); updateView2 (); } private void updateView1 () { Object1 data = DataStorage.getObject1 (); // ... // Here we update the required views // ... } private void updateView2 () { List <Object2> data = DataStorage.getObjects2 (); // ... // Here we update the required views // ... } @Override public void onData1Changed () { updateView1 (); } @Override public void onData2Changed () { updateView2 (); } }
App
public class MyApp extends Application { @Override public void onCreate () { super.onCreate (); Request.registerListener (DataStorage.listener); } }
The same scheme, but in terms of RESS, for understanding
It works like this: When a button is clicked, the requested method is jerked at the Request, Request sends a request to the server, processes the response and notifies the data storage first. DataStorage parse the response and cache the data in itself. Then Request notifies the current active Screen, Screen takes data from the data storage and updates the UI.
Screen subscribes and unsubscribes from the modem onResume and onPause, respectively. And also updates the UI in addition to onResume. What does it give? Notifications come only to the currently active Activity, no problems with processing the request in the background or turning Activity. Activity will always be up to date. Prior to the background activity, the notification will not reach, and upon returning to the active state, the data will be taken from the data storage. As a result, there are no problems with the rotation of the screen and the re-creation of the Activity.
And for all these missing defaults from the Android SDK.
Questions and answers to future criticism
1. What profit?
Real simplicity, flexibility, maintainability, scalability and minimum dependencies. You can always complicate a certain part of the system if you need to. A lot of data? Gently break the DataStorage into several. A huge REST API for the service? Do a few Request. Can they be too simple, slim and unfashionable? Take the EventBus. Are they looking askance in a barbershop on HttpConnection? Well, take Retrofit. Bold Activity with a bunch of fragments? Just think that each fragment is Screen, or break it into subclasses.
2. AsyncTask is a mauve, take at least Retrofit!
Yes? And what problems does it cause in this code? Memory leaks? No, here AsyncTask does not store links to activations, but only a link to the static method. The answer is lost? No, the answer always comes in the static data storage, until the application is killed. Trying to update the activates on a pause? No, notifications only come in the active Activity.
And how will Retrofit help? Just look here. The author took RxJava, Retrofit and still sculpts crutches to solve a problem that RESS simply does not have.
3. The screen is the same ViewController! You need to separate the logic and the view.
Drop this mantra already. A typical client for a REST service is one large view for the server part. All your business logic is to set the required state for the button or text box. What are you going to share there? Speak so it will be easier to maintain? Support 3 files with 3 tons of code, instead of 1 file with 1 ton is easier? OK. And if we have activated with 5 fragments? We already have 3 x (5 + 1) = 18 files.
Separation on the Controller and View in such cases simply breeds a bunch of meaningless code, it’s time to admit it already. Adding functionality to a project with MVP is especially fun: do you want to add a button handler? Ok, correct Presenter, Activity, and View-interface. In RESS for this, I’ll write a couple lines of code in one file.
But in large projects, ViewController horribly grows? So you did not see big projects. Your REST client for the next site for 5,000 lines is a small project, and 5,000 lines are there only because each screen has 5 classes. Really great projects on RESS with 100+ screens and several teams of 10 people feel great. Just do a few Request and Storage. A Screen for fat screens contains additional Screen for large UI elements, for example, the same fragments. The project on MVP of the same scale will simply choke in the heap of presenters, interfaces, activations, fragments, and non-obvious links. And the transition to VIPER, in general, will make the whole team quit one day.
Conclusion
We hope this article will encourage many developers to reconsider their views on architecture, not to produce abstractions and look at simpler and time-tested solutions