RESS - New architecture for mobile applications

Published on September 22, 2018

RESS - New architecture for mobile applications



Contrary to the provocative title, this is not a new architecture, but an attempt to translate simple and time-tested practices into the newspeech, which is spoken by the current Android-community

Introduction


Recently it has become painful to look at what is happening in the world of development for mobile platforms. Architectural astronautics is thriving, each hipster considers it his duty to come up with a new architecture, and to solve a simple task, instead of two lines, insert some fashionable frameworks.

IT sites flooded tutorials on fashionable frameworks and advanced architectures, but there is not even a best practice for REST clients for Android. Although this is one of the most frequent case studies. I want a normal approach to the development, too, went to the masses. Therefore, I am writing this article.

What are the bad existing solutions?


By and large, the problem of the new-minded MVP, VIPER and the like is exactly one, their authors do not know how to design. And their followers - all the more. And so do not understand the important and obvious things. And do the usual over-engineering .

1. Architecture should be simple


The simpler the better. The easier it is to understand, safer and more flexible. Any fool can over-complicate and make a bunch of abstractions, and to make it simple you need to think carefully.

2. Overaining is bad


To add a new level of abstraction is necessary only when the old one no longer solves problems. After adding a new level, the system should become easier to understand, and the code less . If, for example, after that you had three instead of one file, and the system became more confusing, then you made a mistake, and if in a simple way, you wrote garbage .

MVP fans, for example, in their articles themselves write in clear text that MVP stupidly leads to a significant complication of the system. And justify it by the fact that it is so flexible and easier to maintain . But, as we know from point number 1, these are mutually exclusive things.

Now about VIPER, just look, for example, at the diagram from this articles.

Scheme
image

And this is for every screen! My eyes hurt. I especially sympathize with those who, at work, have to face this against their will. I sympathize with those who introduced it myself for slightly different reasons.

New approach


Hey, I also want a trendy name . Therefore, the proposed architecture is called RESS - R equest, E vent, S creen, S torage. The letters and names are detailed so stupidly in order to get a readable word. Well, so as not to create confusion with the names already used. Well, with REST consonant.

At once I will make a reservation, this architecture for REST clients. For other types of applications, it probably will not work.



1. Storage


Data storage (in other terms Model, Repository). This class stores data and processes it (stores, loads, adds to the database, etc.), as well as all the data from the REST service first get here, parse and save here.

2. Screen


The screen of the application, in the case of Android, is your Activity. In other terms, this is the usual ViewController as in Apple's MVC.

3. Request


The class that is responsible for sending requests to the REST service, as well as receiving responses and notifying the response of the other components of the system.

4. Event


The link between the other components. For example, Request sends an event about the server response to those who subscribed to it. And Storage sends a data change event.

Further example of a simplified implementation. The code was written with assumptions and was not tested, 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();
			// ...
			// Делаем запрос и читаем ответ
			// ...
			return result;
		}
		@Override
		protected void onPostExecute(String result)
		{
			// ...
			// Парсим JSON из 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)
		{
			// ...
			// Парсим или десереализуем 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();
		// ...
		// Тут обновляем нужные вьюшки
		// ...
	}
	private void updateView2()
	{
		List<Object2> data = DataStorage.getObjects2();
		// ...
		// Тут обновляем нужные вьюшки
		// ...
	}
	@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 pressed, the required method is jerked by Request, Request sends a request to the server, processes the response and notifies DataStorage first. DataStorage parsit the answer and caches data at home. Then Request notifies the current active Screen, Screen takes data from the DataStorage and updates the UI.

The screen subscribes and unsubscribes from the slowdowns in onResume and onPause, respectively. It also updates the UI additionally in onResume. What does this give? Notifications come only in the current active Activity, no problems with processing the request in the background or by rotating the Activity. Activity will always be up to date. The notification will not reach the background activation, and when returning to the active state, the data will be taken from the DataStorage. As a result, no problems when you rotate the screen and recreate the Activity.

And for all this, there is enough default api from the Android SDK.

Questions and answers to future criticism


1. What is the profit?


Real simplicity, flexibility, maintainability, scalability and minimal dependencies. You can always complicate a certain part of the system, if you need. A lot of data? Carefully break DataStorage into several. Huge service REST API? Make a few Request. Is it too simple, cool and unfashionable? Take the eventbus. Koso look in the barbershop on HttpConnection? Well, take a retrofit. Bold Activity with a bunch of fragments? Just assume that each piece is a Screen, or break into subclasses.

2. AsyncTask is moveton, take at least Retrofit!


Yes? And what problems does it cause in this code? Memory leaks? No, here AsyncTask does not store a link to the activation, but only a link to the static method. Is the answer lost? No, the answer always comes in the Static DataStorage until the application is killed. Trying to update a pause? No, notifications come only in the active Activity.

And how does Retrofit help here? Just look here . The author took RxJava, Retrofit and still sculpts crutches to solve a problem that simply does not exist in RESS.

3. Screen is the same ViewController! It is necessary to separate logic and representation, arrr!


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 desired state for a button or text field. What are you going to share there? Say it will be easier to maintain? Maintain 3 files with 3 tons of code, instead of 1 file with 1 ton easier? OK. And if we have activations with 5 fragments? We already have 3 x (5 + 1) = 18 files.

The separation of the Controller and View in such cases simply breeds a bunch of meaningless code, it is time to admit it. 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 will write a couple of lines of code in one file.

But after all, in large projects, ViewController is growing terribly? So you have not seen large projects. Your REST client for the next site for 5 thousand lines is a small project, and 5 thousand lines are there only because there are 5 classes for each screen. Really big projects on RESS with 100+ screens and several teams of 10 people feel great. Just make a few Request and Storage. A Screen for fat screens contains an additional Screen for large UI elements, for example, the same fragments. A project on MVP of the same scale will simply choke in a heap of presenters, interfaces, activations, fragments and unobvious links. And the transition to VIPER in general will force the whole team to quit one day.

Conclusion


I hope this article will lead many developers to reconsider their views on architecture, not to produce abstractions and look at more simple and time-tested solutions.