📜 ⬆️ ⬇️

Android application architecture

Our journey from standard Activity and AsyncTasks to modern MVP architecture using RxJava.



The project code should be divided into independent modules that work with each other as a well-oiled mechanism - a photo of Chester Alvarez .

The ecosystem of development tools for Android is developing very quickly. Every week someone creates new tools, updates existing libraries, writes new articles, or makes reports. If you go on vacation for a month, then by the time you return, the latest version of the Support Library and / or Google Play Services will already be published.

I have been developing Android applications at ribot for the past three years, and all this time, the architecture of our applications, and the technologies we use, have been constantly evolving and improving. This article will take you through the way we have walked, showing the lessons we have learned, the mistakes we have made, and the reasoning that led to all these architectural changes.
')

Good old times


Back in 2012, the structure of our projects looked very simple. We did not have any libraries to work with the network, and AsyncTask was still our friend. The diagram below shows the approximate architecture of those solutions:


The code was divided into two levels: the data layer (data layer), which was responsible for receiving / storing data received through both the REST API and various local storages, and the view layer (responsible for processing and displaying data).

APIProvider provides methods that allow activations and fragments to interact with the REST API. These methods use URLConnection and AsyncTask to execute the request in the background thread, and then deliver the results to the activation via callback functions. CacheProvider works the same CacheProvider : there are methods that retrieve data from SharedPreferences or SQLite, and there are callback functions that return results.

Problems


The main problem with this approach is that the level of representation has too much responsibility. Let's imagine a simple script in which an application should load a list of posts from a blog, cache them into SQLite, and then display them in a ListView . Activity should do the following:

  1. Call the APIProvider#loadPosts(Callback) method.
  2. Wait for the onSuccess() method call in the transferred Callback , and then call CacheProvider#savePosts(Callback) .
  3. Wait for the onSuccess() method call in the transferred Callback , and then display the data in the ListView .
  4. Separately handle two possible errors that may occur in both the APIProvider and the CacheProvider .

And this is another simple example. In real life, it may happen that the API returns the data not in the form in which our view level expects it, which means the Activity will have to somehow transform and / or filter the data before it can work with it. Or, for example, loadPosts() will take an argument that you need to get from somewhere (for example, an email address that we will request through the Play Services SDK). Surely the SDK will return the address asynchronously, via a callback function, which means we now have three levels of nesting callback functions. If we continue to wind down more and more complexity, we end up with what is called a callback hell.

Sum up:


New architecture with RxJava


We have used the above approach for two years. During this time, we made several changes that lessened the pain and suffering from the problems described. For example, we added several auxiliary classes, and carried some logic into them to offload activations and fragments, and also we started using Volley in the APIProvider . Despite these changes, the code was still difficult to test, and the callback-hell periodically broke through here and there.

The situation began to change in 2014, when we read several articles on RxJava . We tried it on several pilot projects, and realized that a solution to the problem of nested callback functions was found. If you are not familiar with reactive programming, then we recommend reading this introduction here. In short, RxJava allows you to manage your data through asynchronous streams (for example, a translator: in this case, streams are streams, not to be confused with threads - streams of execution), and provides many operators that can be applied to streams to transform, filter, or combine data as you need.

Taking into account all the bumps that we have filled over the past two years, we began to think over the architecture of the new application, and came to the following:


The code is still divided into two levels: the data layer contains the DataManager and a set of helper classes, the presentation layer consists of Android SDK classes, such as Activity , Fragment , ViewGroup , and so on.

The helper classes (the third column in the diagram) have very limited areas of responsibility, and implement them in a consistent manner. For example, most projects have classes for accessing the REST API, reading data from the database, or interacting with third-party SDKs. Different applications will have a different set of helper classes, but the following will be the most commonly used:


Many public methods of helper classes return RxJava Observables .

DataManager is a central part of the new architecture. It makes extensive use of RxJava operators in order to combine, filter, and transform data received from helpers. The task of the DataManager is to release activations and fragments from the work of “combing” the data - it will perform all the necessary transformations within itself and give out the data ready for display.

The code below shows what a method from the DataManager might look like. It works as follows:

  1. Uploads a list of posts via Retrofit.
  2. Caches data in a local database through DatabaseHelper .
  3. Filters posts, selecting those that were published today, as the presentation level should only display them.

 public Observable<Post> loadTodayPosts() { return mRetrofitService.loadPosts() .concatMap(new Func1<List<Post>, Observable<Post>>() { @Override public Observable<Post> call(List<Post> apiPosts) { return mDatabaseHelper.savePosts(apiPosts); } }) .filter(new Func1<Post, Boolean>() { @Override public Boolean call(Post post) { return isToday(post.date); } }); } 

View-level components will simply call this method and subscribe to the Observable returned to them. Once the subscription is completed, posts returned by the received Observable can be added to the Adapter to display them in a RecyclerView or something similar.

The last element of this architecture is the event bus . Event bus allows us to run messages about certain events occurring at the data level, and components that are at the presentation level can subscribe to these messages. For example, the signOut() method in DataManager can trigger a message indicating that the corresponding Observable completed its work, and then the activators subscribed to this event can redraw their interface to show that the user has logged out.

How is this approach better?



And what problems remain?




We try Model View Presenter


Over the past year, separate architectural patterns have begun to gain popularity in the Android community, as MVP , or MVVM . After researching these patterns in a test project , as well as a separate article , we found that MVP can bring significant changes to the architecture of our projects. Since we have already divided the code into two levels (data and presentation), the introduction of MVP looked natural. We just needed to add a new level of presenters, and transfer part of the code from the views to it.


The data level remains unchanged, but now it is called a model to match the name of the corresponding level from the MVP.

Presenters are responsible for loading data from the model and calling the appropriate methods at the presentation level when the data is loaded. Presenters subscribe to the Observables returned by the DataManager . Therefore, they should work with such entities as subscriptions and schedulers . Moreover, they can analyze errors that occur, or apply additional operators to data streams, if necessary. For example, if we need to filter some data, and this filter most likely will not be used anywhere else, it makes sense to bring this filter to the presenter level, and not to the DataManager .

Below is one of the methods that can be at the presenter level. Here comes the subscription to Observable , returned by the dataManager.loadTodayPosts() method, which we defined in the previous section.

 public void loadTodayPosts() { mMvpView.showProgressIndicator(true); mSubscription = mDataManager.loadTodayPosts().toList() .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(new Subscriber<List<Post>>() { @Override public void onCompleted() { mMvpView.showProgressIndicator(false); } @Override public void onError(Throwable e) { mMvpView.showProgressIndicator(false); mMvpView.showError(); } @Override public void onNext(List<Post> postsList) { mMvpView.showPosts(postsList); } }); } 

mMvpView is the mMvpView -level component that the presenter works with. Usually it will be Activity , Fragment or ViewGroup .

As in the previous architecture, the presentation layer contains standard components from the Android SDK. The difference is that now these components do not subscribe directly to Observables . Instead, they implement the MvpView interface, and provide a list of clear and understandable methods, such as showError() or showProgressIndicator() . Presentation-level components are also responsible for handling user interaction (for example, push events), and calling the appropriate methods in a presenter. For example, if we have a button that loads a list of posts, our Activity will have to OnClickListener the presenter.loadTodayPosts() method in OnClickListener .

If you want to take a look at a working example, you can take a look at our repository on Github . Well, if you wanted more, you can see our recommendations for building architecture .

How is this approach better?



And what problems remain?





It is important to mention that the approach I described is not ideal. In general, it would be naive to believe that there is somewhere the very unique and unique architecture that will take and solve all your problems once and for all. Android's ecosystem will continue to evolve at high speeds, and we will have to keep up to date, exploring, reading and experimenting. What for? To continue making great Android apps.

I hope you enjoyed my article and found it useful. If so, do not forget to click on the Recommend button (comment of the translator: go to the original article, and click on the heart button at the end of the article). Also, I would like to hear your thoughts on our current approach.

Source: https://habr.com/ru/post/278815/


All Articles