MVP Learning Note

The problem that MVP is trying to solve

To understand Android MVP pattern we first need to understand the problem it is trying to solve, the problem is that Android activities are closely coupled to both interface and data access mechanisms, that means we cannot test our logic independent of the Activity, and if business requirements change, as it often does, we will continue to couple untested, unknown, unproven logic within the Activity.

The goal of MVP in Android is to separate the known from the unknown. Android MVP pattern is a design approach that separates your programming logic from the Activity and the Fragment. With this approach, the Activity or the Fragment is the View, the Java class holding the logic is the Presenter. This way the Activity/Fragment no longer has to know or worry about how to make the decision to show different view base on the data loading result.(example code snippet showed below).

Model-View-Presenter

(Presenter has a 1-to-1 relation with the view)

How to implement MVP for Android | commit

Step 1 – Create the Contract

Contract describes the communication between view and presenter. It is nothing but the good old Java interface that defines the responsibility of the View and the Presenter.

Refer to Android Architecture - TasksContract.java

+public class ArticlesContract {
+
+    interface View extends BaseView<Presenter> {
+
+        void showArticles(List<Article> articleList);
+
+        void setLoadingIndicator(boolean active);
+
+        void showLoadingArticlesSucceed();
+
+        void showNoArticles();
+
+        void showLoadingArticlesError();
+
+        void showNoNetworkError();
+    }
+
+    interface Presenter extends BasePresenter {
+
+        void loadArticles();
+    }
+}

+public interface BasePresenter {
+    /**
+     * Start presenter business when its related view is ready.
+     * <p>
+     * Presenter doesn't make sense without a view, and only has a 1-to-1 relation with the view.
+     * presenter maybe need to do some initialization stuff when the view is ready.
+     * <p>
+     * That's the reason we define this method which should be invoked when the view is ready, for example,
+     * when Fragment.onViewCreated, Activity.onResume.
+     */
+    void start();
+}
  • View interface – this defines the methods that the concrete View aka Fragment will implement. This way you can proceed to create and test the Presenter without worrying about Android-specific components such as Context.
  • Presenter interface – this defines the methods that the concrete Presenter class will implement. Also known as user actions, this is where the business logic for the app is defined.

Step 2 – Implement Presenter

we need to pass the MVP-View to presenter.

+public class ArticlesPresenter implements ArticlesContract.Presenter {
+    private static final String LOG_TAG = ArticlesPresenter.class.getSimpleName();
+
+    @Inject
+    ArticlesContract.View mArticlesView;
+    // Could i put LifecycleOwner in Presenter? Does it violate the principle of MVP?
+    @Inject
+    LifecycleOwner mLifecycleOwner;
+    @Inject
+    DemoRepository mRepository;
+
+    @Inject
+    public ArticlesPresenter() {
+    }
+
+    @Override
+    public void onViewReady() {
+        loadArticles();
+    }
+
+    @Override
+    public void loadArticles() {
+        mArticlesView.setLoadingIndicator(true);
+
+        mRepository.loadArticleList().observe(mLifecycleOwner, resource -> {
+            Log.d(LOG_TAG, "loading status: " + resource.status + ", code " + resource.code);
+
+            switch (resource.status) {
+                case LOADING:
+                    break;
+
+                case SUCCESS:
+                    if (resource.data == null) {
+                        mArticlesView.showNoArticles();
+                    } else {
+                        mArticlesView.showLoadingArticlesSucceed();
+                    }
+                    break;
+
+                case ERROR:
+                    if (resource.throwable instanceof NoNetworkException) {
+                        mArticlesView.showNoNetworkError();
+                    } else {
+                        mArticlesView.showLoadingArticlesError();
+                    }
+                    break;
+            }
+
+            mArticlesView.setLoadingIndicator(false);
+            mArticlesView.showArticles(resource.data);
+        });
+    }
+}

As you can see from the code above, the presenter is the middle-man between model and view. All your presentation logic belongs to it. The presenter is responsible for querying model and updating the view with formatted data, reacting to user interactions updating the model.

The presenter must depend on the View interface defined in the Contract and not directly on the Activity: in this way, you decouple the presenter from the view implementation (and then from the Android platform) respecting the D of the SOLID principles: "Depend upon Abstractions. Do not depend upon concretions". Then we can easily unit-test presenter by creating a mock view.

In short, make presenter framework-independent.

Step 3 – Implement the View

we need to pass the MVP-Presenter to Android View,

+public class ArticleListFragment extends DaggerFragment implements ArticleSelectListener, ArticlesContract.View {
+    @Inject
+    ArticlesContract.Presenter mPresenter;

     @Override
     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
-        loadData();
+        mPresenter.onViewReady();
     }

     private SwipeRefreshLayout.OnRefreshListener mOnRefreshListener = new SwipeRefreshLayout.OnRefreshListener() {
         @Override
         public void onRefresh() {
-            loadData();
-            mSwipeRefreshLayout.setRefreshing(false);
+            mPresenter.loadArticles();
         }
     };

-    private void loadData() {
-        mViewModel.getArticleList().observe(getActivity(), resource -> {
-            Log.d(LOG_TAG, "loading status: " + resource.status + ", code " + resource.code);
-
-            switch (resource.status) {
-                case LOADING:
-                    showMessage(R.string.status_loading);
-                    break;
-
-                case SUCCESS:
-                    showMessage(R.string.status_success);
-                    if (resource.data == null) {
-                        showMessage(R.string.status_no_response);
-                    }
-                    break;
-
-                case ERROR:
-                    if (resource.throwable instanceof NoNetworkException) {
-                        showMessage(R.string.status_no_connect);
-                    }
-                    break;
-            }
-
-            // update recycler view
-            mAdapter.update(resource.data);
-        });
-    }
-

+    @Override
+    public void showArticles(List<Article> articleList) {
+        mAdapter.update(articleList);
+    }
+
+    @Override
+    public void setLoadingIndicator(boolean active) {
+        mSwipeRefreshLayout.setRefreshing(active);
+    }
+
+    @Override
+    public void showLoadingArticlesSucceed() {
+        showMessage(R.string.status_success);
+    }
+
+    @Override
+    public void showNoArticles() {
+        showMessage(R.string.status_no_response);
+    }
+
+    @Override
+    public void showLoadingArticlesError() {
+
+    }
+
+    @Override
+    public void showNoNetworkError() {
+        showMessage(R.string.status_no_connect);
+    }
 }

As you can see from the View implementation above, View is only responsible for presenting data in a way decided by the presenter. It is not involved with any decision. The view should not handle user interaction directly where it affects the model. Instead, it should forward interactions to the presenter.

The view can be implemented by Activities, Fragments, any Android widget or anything that can do operations like showing a ProgressBar, updating a TextView, populating a RecyclerView and so on. It is better to implement the Android View with Fragment, for handling of configuration changes.

Some Guidelines

Handle Configuration Changes

The MVP pattern should not replace the way you handle configuration changes in your app. For example, refer to googlesamples/android-architecture/todo-mvp

That also means DO NOT save the state inside the presenter, or means DO NOT use Bundle.

And because the state has already been saved in Activity/Fragment, and the network results have already been cached by Repository, so we have no need to retain the presenter.

//TasksActivity.java

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Load previously saved state, if available.
        if (savedInstanceState != null) {
            TasksFilterType currentFiltering =
                    (TasksFilterType) savedInstanceState.getSerializable(CURRENT_FILTERING_KEY);
            mTasksPresenter.setFiltering(currentFiltering);
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        outState.putSerializable(CURRENT_FILTERING_KEY, mTasksPresenter.getFiltering());

        super.onSaveInstanceState(outState);
    }

Handle LifeCycle

Do not create Activity-lifecycle-style callbacks in the Presenter interface, otherwise the presenter would be coupled in particular with the Activity lifecycle.

Handle Context

Get rid of it! you shouldn’t do that in the presenter: you should access to resources in the view and to preferences in the model.

Use a dependency injection framework like Dagger 2 to keep singleton instance of the Repository where Context that is not tied to the Fragment or Activity.

Presenter naming convention

Consider this situation, when user scrolls to the end of the list, then the view should forward the action to presenter to load more. But if view changed, we have a new button named "Next Page" on the bottom, and load more only when user click this button. so in this situation, how to name the contract interface method to make it meaningful?

interface Contract.Presenter {
    loadMore();         // name1: I prefer this one
    onScrolledToEnd();  // name2
    onNextPageClick();  // name3
}

Refer