State restoration in an Android app developed with the MVP (Model-View-Presenter) pattern can sometimes get tricky if you want to keep the architecture clean. One such example would be when a presenter needs to be made aware of the state.
Android already has a mechanism for storing and receiving objects from the Bundle
, which can be used to restore state. The presenter, on the other hand, shouldn’t depend on any Android classes. This enables better abstraction and testability with pure Java unit tests.
This topic will provide Model‑View‑Presenter (MVP) architecture of Android with various examples.
Model
In an application with a good layered architecture, this model would only be the gateway to the domain layer or business logic. See it as the provider of the data we want to display in the view.
View
The View, usually implemented by an Activity
or Fragment
, will contain a reference to the presenter. The only thing that the view will do is to call a method from the Presenter every time there is an interface action.
Presenter
The Presenter is responsible to act as the middle man between View and Model. It retrieves data from the Model and returns it formatted to the View. But unlike the typical MVC, it also decides what happens when you interact with the View.
What is truly great about MVP is that it is in fact very forgiving, and can be implemented in a variety of ways without having its principles. In this article we’ll focus solely on a specific example of implementing MVP in Android development.
Let’s assume that our screen us is a list of items (in our particular example it’s a list of notifications). In this implementation, we’re using a Contract interface, which in turn consists of two separate interfaces – View and Actions. Our View class directly implements the View interface, which performs UI-related work. In order to process all of the necessary View events (such as button clicking, etc.), the Presenter class implements the Actions interface.
public interface NotificationsContract { interface View { void progressDialog(boolean show); void showError(String message); void listItemClickAction(NotificationsAdapter.OnItemClickCallback onItemClickCallback); void loadMoreNotificationsListAction(NotificationsAdapter.OnLoadMoreListener onLoadMoreListener); void setNotificationsContent(List<Notification> dataList); NotificationsAdapter getNotificationsAdapter(); } interface Actions { void clickNotificationsListItem(Integer position); void loadMoreNotificationsList(); } }
In the code snippet below, you can see that the first thing we need to do in our fragment or activity is create and initialize View and Presenter.
public class FragmentNotifications extends BaseFragment { private BaseActivity activity; private EventBus eventBus; private NotificationsView notificationsView; private NotificationsPresenter notificationsPresenter; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); View view = inflater.inflate(R.layout.fragment_notifications, container, false); activity = (BaseActivity) getActivity(); eventBus = activity.getEventBus(); notificationsView = new NotificationsView(activity, view, eventBus); notificationsPresenter = new NotificationsPresenter(activity, notificationsView, eventBus); eventBus.register(notificationsPresenter); return view; } }
Below is the example of how your Presenter might look. It is implemented by Contract.Actions. We receive all server data using Model and transfer it to View.
public class NotificationsPresenter implements NotificationsContract.Actions { private BaseActivity activity; private NotificationsContract.View view; private EventBus eventBus; private CommonMode commonModel; public NotificationsPresenter(BaseActivity activity, NotificationsContract.View view, EventBus eventBus) { this.activity = activity; this.view = view; this.eventBus = eventBus; init(); setupActions(); } private void init() { commonModel = App.getInstance().getModelService().getCommonModel(); setupList(); } public void setupList() { //commonModel.geNotifications()... //view.setNotificationsContent(response.getContent()); } private void setupActions() { view.listItemClickAction(new NotificationsAdapter.OnItemClickCallback() { @Override public void listItemClickCallback(int position) { clickNotificationsListItem(position); } }); view.loadMoreNotificationsListAction(new NotificationsAdapter.OnLoadMoreListener() { @Override public void onLoadMore() { loadMoreNotificationsList(); } }); } @Override public void clickNotificationsListItem(Integer position) { //do some stuff } @Override public void loadMoreNotificationsList() { //load more content //commonModel.getMoreNotifications()... } }
In the following piece of code, you can see the View class, in which we work with UI. From here, we send events to Presenter for all further processing.
public class NotificationsView implements NotificationsContract.View { private BaseActivity activity; private View view; private EventBus eventBus; //... //other views public NotificationsView(BaseActivity activity, View view, EventBus eventBus) { this.activity = activity; this.eventBus = eventBus; this.view = view; init(); } private void init() { //views initialization } @Override public void progressDialog(boolean show) { if (show) { // showProgressDialog(); } else { //dismissProgressDialog(); } } @Override public void setNotificationsContent(List<Notification> dataList) { //setting adapter data } @Override public NotificationsAdapter getNotificationsAdapter() { return notificationsAdapter; } @Override public void showError(String message) { //show Toast } @Override public void listItemClickAction(NotificationsAdapter.OnItemClickCallback onItemClickCallback) { notificationsAdapter.setOnItemClickCallback(onItemClickCallback); } @Override public void loadMoreNotificationsListAction(NotificationsAdapter.OnLoadMoreListener onLoadMoreListener) { notificationsAdapter.setOnLoadMoreListener(onLoadMoreListener); } }
The next snippet presents Model. Here, we request data with the help of ApplicationAPI. We’re not going to cover the implementation of RestService here, as it’s a standard initialization of Retrofit on the base of OkHttpClient + Interceptors, Headers, etc. Also, we’re using a combination of Rx + Retrofit.
public class CommonModel { private ApplicationAPI applicationApi; public CommonModel() { applicationApi = RestService.createRestService(); } public Observable<NotificationsResponse> getNotifications() { return applicationApi.getNotifications() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); } public Observable<NotificationsResponse> getMoreNotifications(String page) { return applicationApi.getMoreNotifications(page) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); } }
Finally, the last code snippet shows us the usual API interface, one based on Retrofit + Rx.
public interface ApplicationAPI { @GET("/myapi/notifications/list/") Observable<NotificationsResponse> getNotifications(); @GET("/myapi/notifications/list/") Observable<NotificationsResponse> getMoreNotifications(@Query("page") String page); //… //other API calls }
And here you have it – one-among-many implementation of the MVP architecture. You can see that UI and presentation logic are clearly separated. By adding new features, we expand Contract, Presenter, Model (if necessary) and implement the added abilities to View. Each screen would be then divided into such components, making the architecture modular, clean and flexible.