Author avatar

Pavneet Singh

MVP with Testing - Part 1 (MVP Architecture)

Pavneet Singh

  • Jul 23, 2018
  • 22 Min read
  • 11,865 Views
  • Jul 23, 2018
  • 22 Min read
  • 11,865 Views
MVP
Android

MVP Architecture

This tutorial contains information about the Android application architecture principals and how to develop an application to incorporate the MVP architecture which improves and facilitates the testing process, code re-usability, scalability, and separation of UI components from app logic.

The conventional approach in Android development follows the MVC architecture where is described as below

MVC

  • Model : Stores the data and may contain the logic.

  • View : A View is just a dummy placeholder to represent the output.

  • Controller : Acts as a manager to provide responses to users and interact with the user-interface.

An Activity or Fragment often acts as a Controller, which contains all the implementation logic for Sharedpreference, network callbacks, as well as model and database interaction while also including the activity lifecycle callback.

Eventually the code grows abruptly and becomes more rigid to maintain, scale, and refactor. MVC is so tightly coupled with Android APIs that the logic implementation cannot be tested with Unit testing or requires a great deal of effort to create a mock testing environment. This brings us to the next architecture approach, i.e. clean architecture

Clean Architecture

To separate the logic from the android APIs, A clean code architecture is adapted by the community to create layered architecture; separating model, logic, and user interface in various layers.

clean architecture

Clean Architecture Helps to Achieve Code

  • Separation of UI and logic : The UI and logic implementation are maintained separately to incorporate changes without effecting other layers means presenter should never deal with android framework related components and Views.

  • Adaptive data source implementation : The data source be changed any time in future by leveraging Repository Pattern.

  • Testability : The business logic can be tested independent of the UI, database, and any external source.

  • Re-usability : The code structure can be reused by adding the existing modules to new project.

For the detailed explanation on clean architecture, read this article.

MVP

To address the architectural issues, MVP separates the application into three layers where all user events are delegated to Presenter.

  • Model : Model is responsible for managing the data, irrespective of the source of data. The responsibilities of model are to cache data and validate the data source; either network or local data source like sqlite, files etc. A model is often implemented via Repository Pattern to abstract away the internal implementation of data source.
  • View : The only job of view is to display the content instructed by the presenter. The View interface can be implemented by Activity, Fragment, or it's subtypes. View keeps a reference to the presenter to delegate the user event to the presenter and perform UI oriented tasks like populating lists, showing dialogs, or updating content on screen.
  • Presenter : The Presenter is an intermediate layer which bridges the communication gap between Model and View. As per the user event, passed by the view, the Presenter queries the model, transforms the data, and passes the updated data to View to be displayed on the screen.

Note:

  • In most cases, Presenter and View are bound to each other in a one-to-one relationship Although one view may have more than one presenter.

  • Interfaces are defined and implemented as a contract between View and Presenter communication.

  • The View aka passive view should never update itself from the model.

  • The presenter handles all the business logic, model updating, and view updating calls.

  • To perform unit testing, the presenter should avoid any usage of Android API. Running a JVM test on a local machine takes less time because it does not requires any UI interactions(Android framework components), emulator, or physical device.

Implementing Basic MVP Patterns

The project demonstrates the work of an activity which receives and displays a list of movies from a network request (which is implemented locally for demonstration).

This is the visual representation of project structure:

description

1package com.example.pavneet_singh.mvptestingdemo.mvp.movieslist;
2
3----------------------------------------------------------------
4
5.model                  : POJO and Repository pattern
6
7.mvp.movielist          : MVP implementation for Movie List Screen
8
9.util                   : To imitate a network response and provide helpful classes
  1. Implementing Passive View : A view should be implemented as passive view. MoviesListActivity implements the MoviesListContract.View interface to expose the functionalities to other components like presenter.
1// passive view
2public class MoviesListActivity extends AppCompatActivity implements MoviesListContract.View{
java

Presenter will hold a reference to view and invoke methods exposed by View interface.The process to fetch the data from the data source, will be handled by the presenter. This passive view approach simplifies the testing phase with the help of mocking object.

  1. Android API Free Presenter : The presenter handles events from the UI and uses the interface callback mechanism to communicate with long background threads and processes. The presenter implements the MoviesListContract.Presenter
1public final class MoviePresenter implements MoviesListContract.Presenter  {
java

The key point to make a testable presenter is to avoid usage of android API because often the common android API like Sharedpreference, database requires context which cannot be acquired without the involvement of activity lifecycle so to conduct the local junit tests via JVM on IDE the mockito framework is used to create dummy object.

  1. A Complete Contract for View and Presenter : The View and Presenter interfaces are combined into one single interface, known as Contract, which will be used to establish a relationship between the concrete implementations of both View and Presenter.

MoviesListContract.java

1public interface MoviesListContract {
2   // implemented by MoviesListActivity to provide concrete implementation
3   interface View {
4       void showProgress(); // display progress bar
5
6       void hideProgress(); // hide progress bar
7
8       void showMovieList(List<Movie> movies); // receive response to display
9
10       void showLoadingError(String errMsg); // display error
11   }
12
13   // implemented by MoviesPresenter to handle user event
14   interface Presenter{
15       void loadMoviewList();
16
17       void dropView();
18   }
19
20   // implemented by MoviePresenter to receive response from asynchronous processes
21   interface OnResponseCallback{
22       void onResponse(List<Movie> movies);
23
24       void onError(String errMsg);
25   }
26}
java

The View interface simply indicates the UI behavior and a concrete implementation of Presenter will manage the state of the UI via invoking the methods of View.

A presenter has more user action specific methods to perform than the result oriented operation on data. Presenter also use OnResponseCallback interfaces to establish a communication link with background tasks and process.

The reason to keep Presenter and View in a single interface is

  • View is also a class in Android. So, with this we can name our interface as View without creating any confusion.

  • Both View and Presenter functionalities are available at a single place which enhances the clarity.

  • Presenter and View, both have specific behavior to serve a single entity (activity or fragment) so it makes more sense to keep the same things together.

Important Point While Making a Presenter

  • Don't create a lifecycle in presenter, otherwise your presenter will be tightly connected with the Android component where most have their own differences. This makes the presenter more dependent and highly rigid to modification.
  • Introducing a method in presenter which accepts the view reference requires null checks at many places, so avoid it if you can and instead use constructor.
  • Use cache a mechanism like LRUCache to retain the data, keep the presenter stateless, and let it recreate with the creation of an activity or fragment.
  1. View and Presenter in Connectivity : MoviePresenter will receive the reference of View, i.e. MoviesListActivity, via constructor along with the concrete implementation of MovieRepo interface which will contain the logic to retrieve data from a data source (network, local database, files etc).

Below is the concrete implementation of presenter i.e. MoviePresenter.java:

1public final class MoviePresenter implements MoviesListContract.Presenter  {
2  // to keep reference to view
3  private MoviesListContract.View view;
4
5  // Repository pattern, mclient holds reference to concrete data retrieval implementation
6
7  private MovieRepo mclient;
8
9  public MoviePresenter(MoviesListContract.View view,MovieRepo client) {
10    this.view = view;
11
12    mclient = client;
13  }
14
15  @Override
16  public void dropView() {
17      view = null;
18  }
19
20  // would be triggered by MovieListActivity
21  @Override
22  public void loadMoviewList() {
23      view.showProgress();
24
25      mclient.getMovieList(callback);
26
27      // required for espresso UI testing
28
29      // to wait till response occurred
30
31      EspressoTestingIdlingResource.increment();
32  }
33
34  // callback mechanism , onResponse will be triggered with response
35
36  // by simulatemovieclient(or your network or database process) and pass the response to view
37  private final OnResponseCallback callback = new OnResponseCallback() {
38      @Override
39      public void onResponse(List<Movie> movies) {
40
41          view.showMovieList(movies);
42
43          view.hideProgress();
44
45          EspressoTestingIdlingResource.decrement();
46      }
47
48      @Override
49      public void onError(String errMsg) {
50
51          view.hideProgress();
52
53          view.showLoadingError(errMsg);
54
55          EspressoTestingIdlingResource.decrement();
56      }
57  };
58}
java

loadMoviewList method would be triggered by MoviesListActivity when the user performs the action (swipe down on screen) then a progress bar will be displayed on screen until a response is received and delivered to activity by view.showMovieList(movies); method to display list.

1public class MoviesListActivity extends AppCompatActivity implements MoviesListContract.View{
2    private RecyclerView recyclerView;
3
4    private MoviesAdapter moviesAdapter;
5
6    private SwipeRefreshLayout swipeLayout;
7
8    private MoviesListContract.Presenter presenter;
9
10    private TextView tv_empty_msg;
11
12
13
14    @Override
15
16    protected void onCreate(Bundle savedInstanceState) {
17
18        super.onCreate(savedInstanceState);
19
20        setContentView(R.layout.activity_movie_list);
21
22        initViews();
23    }
24
25
26
27    private void initViews(){
28
29        presenter = new MoviePresenter(this, new SimulateMovieClient());
30
31        recyclerView = (RecyclerView) findViewById(R.id.movies_recycler_list); // list
32
33        tv_empty_msg = (TextView)findViewById(R.id.swipe_msg_tv); // empty message
34
35        recyclerView.setLayoutManager(new LinearLayoutManager(this)); // for vertical liner list
36
37        moviesAdapter = new MoviesAdapter(this);
38
39        recyclerView.setAdapter(moviesAdapter);
40
41        swipeLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_container);
42
43        swipeLayout.setOnRefreshListener(listener);
44
45        swipeLayout.setColorSchemeColors( // colors for progress dialog
46
47                ContextCompat.getColor(MoviesListActivity.this, R.color.colorPrimary),
48
49                ContextCompat.getColor(MoviesListActivity.this, R.color.colorAccent),
50
51                ContextCompat.getColor(MoviesListActivity.this, android.R.color.holo_green_light)
52
53        );
54
55    }
56
57
58
59    private final OnRefreshListener listener = new OnRefreshListener() {
60
61        @Override
62
63        public void onRefresh() {
64
65            presenter.loadMoviewList();
66
67        }
68
69    };
70
71
72
73    // for future, to show progress
74
75    @Override
76
77    public void showProgress() {
78
79        swipeLayout.setRefreshing(true);
80
81    }
82
83
84
85    @Override
86
87    public void hideProgress() {
88
89        swipeLayout.setRefreshing(false);
90
91    }
92
93
94
95    // toggle the visibility of empty textview or list
96
97    // display list only when response it not empty
98
99    private void showORHideListView(boolean flag){
100
101        if (flag){
102
103            tv_empty_msg.setVisibility(View.GONE);
104
105            recyclerView.setVisibility(View.VISIBLE);
106
107        }else {
108
109            tv_empty_msg.setVisibility(View.VISIBLE);
110
111            recyclerView.setVisibility(View.GONE);
112
113        }
114
115    }
116
117
118
119    @Override
120
121    public void showMovieList(List<Movie> movies) {
122
123        if (!movies.isEmpty()){
124
125            moviesAdapter.setList(movies);
126
127            showORHideListView(true);
128
129        }
130
131    }
132
133
134
135    @Override
136
137    public void showLoadingError(String errMsg) {
138
139        hideProgressAndShowErr(errMsg);
140
141        showORHideListView(false);
142
143    }
144
145
146
147    private void hideProgressAndShowErr(String msg){
148
149        tv_empty_msg.setVisibility(View.VISIBLE);
150
151        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
152
153        showORHideListView(false);
154
155    }
156
157
158
159    @Override
160
161    protected void onDestroy() {
162
163        super.onDestroy();
164
165        presenter.dropView();
166
167    }
168
169}
java

The above implementation of View creates a reference to the presenter and passes its reference using this along with the SimulateMovieClient instance, containing data retrieval logic.

activity_movie_list.xml

1<?xml version="1.0" encoding="utf-8"?>
2
3<RelativeLayout
4
5    xmlns:android="http://schemas.android.com/apk/res/android"
6
7    android:id="@+id/tasksContainer"
8
9    android:layout_width="match_parent"
10
11    android:layout_height="match_parent"
12
13    android:scrollbars="vertical">
14
15
16
17    <TextView
18
19        android:text="@string/No_Data_MSG"
20
21        android:gravity="center|center_horizontal"
22
23        android:id="@+id/swipe_msg_tv"
24
25        android:layout_width="match_parent"
26
27        android:layout_height="match_parent" />
28
29
30
31    <android.support.v4.widget.SwipeRefreshLayout
32
33        android:id="@+id/swipe_container"
34
35        android:layout_width="match_parent"
36
37        android:layout_height="match_parent" >
38
39
40
41        <android.support.v7.widget.RecyclerView
42
43            android:id="@+id/movies_recycler_list"
44
45            android:visibility="gone"
46
47            android:layout_width="match_parent"
48
49            android:layout_height="wrap_content"/>
50
51
52
53    </android.support.v4.widget.SwipeRefreshLayout>
54
55</RelativeLayout>
xml

This is the adapter code to create list items

MoviesAdapter.java:

1public class MoviesAdapter extends RecyclerView.Adapter<MoviesAdapter.MovieHolder> {
2
3
4
5    private final Context context;
6
7    private final List<Movie> movStrings = new ArrayList<>();
8
9    private static final String TAG = "MoviesAdapter";
10
11
12
13    public MoviesAdapter(Context context) {
14
15        this.context = context;
16
17    }
18
19
20
21    @Override
22
23    public MovieHolder onCreateViewHolder(ViewGroup parent, int viewType) {
24
25        View view = LayoutInflater.from(context).inflate(R.layout.item_movie_model,parent,false);
26
27        return new MovieHolder(view);
28
29    }
30
31
32
33    @Override
34
35    public void onBindViewHolder(MovieHolder holder, final int position) {
36
37        Log.e(TAG, "onBindViewHolder: "+position);
38
39        holder.movieTitle.setText(movStrings.get(position).getTitle());
40
41        holder.date.setText(Utility.convertMinutesToDuration(movStrings.get(position).getDurationinMinutes()));
42
43        holder.rating.setText(Double.toString(movStrings.get(position).getRating()));
44
45        holder.moviesLayout.setOnClickListener(new View.OnClickListener() {
46
47            @Override
48
49            public void onClick(View v) {
50
51                Toast.makeText(context, movStrings.get(position).toString(), Toast.LENGTH_SHORT).show();
52
53            }
54
55        });
56
57    }
58
59
60
61    public void setList(List<Movie> list){
62
63        movStrings.clear();
64
65        movStrings.addAll(list);
66
67        notifyDataSetChanged();
68
69        Log.e(TAG, "onNext: "+movStrings.size() );
70
71    }
72
73
74
75
76
77    @Override
78
79    public int getItemCount() {
80
81        return movStrings.size();
82
83    }
84
85
86
87    public static class MovieHolder extends RecyclerView.ViewHolder {
88
89        TextView movieTitle;
90
91        TextView date;
92
93        TextView view;
94
95        TextView rating;
96
97        LinearLayout moviesLayout;
98
99
100
101        public MovieHolder(View v) {
102
103            super(v);
104
105            moviesLayout = (LinearLayout) v.findViewById(R.id.movies_layout);
106
107            movieTitle = (TextView) v.findViewById(R.id.title);
108
109            date = (TextView) v.findViewById(R.id.date);
110
111            rating = (TextView) v.findViewById(R.id.rating);
112
113        }
114
115    }
116
117}
java

The layout item file item_movie_model.xml:

1<?xml version="1.0" encoding="utf-8"?>
2
3<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
4
5    xmlns:tools="http://schemas.android.com/tools"
6
7    android:id="@+id/movies_layout"
8
9    android:layout_width="match_parent"
10
11    android:layout_height="wrap_content"
12
13    android:background="?android:attr/selectableItemBackground"
14
15    android:gravity="center_vertical"
16
17    android:minHeight="72dp"
18
19    android:orientation="horizontal"
20
21    android:padding="16dp">
22
23
24
25    <LinearLayout
26
27        android:layout_width="0dp"
28
29        android:layout_height="wrap_content"
30
31        android:layout_weight="1"
32
33        android:orientation="vertical">
34
35
36
37        <TextView
38
39
40
41            android:id="@+id/title"
42
43            tools:text="Avengers"
44
45            android:layout_width="wrap_content"
46
47            android:layout_height="wrap_content"
48
49            android:layout_gravity="top"
50
51            android:paddingRight="16dp"
52
53            android:textStyle="bold"
54
55            android:textColor="@android:color/black"
56
57            android:textSize="16sp" />
58
59
60
61
62
63        <TextView
64
65            android:id="@+id/date"
66
67            android:layout_width="wrap_content"
68
69            android:layout_height="wrap_content"
70
71            android:maxLines="1"
72
73            android:paddingRight="16dp"
74
75            android:textColor="@color/greyLight" />
76
77
78
79    </LinearLayout>
80
81
82
83    <LinearLayout
84
85        android:layout_width="wrap_content"
86
87        android:layout_height="35dp"
88
89        android:orientation="horizontal">
90
91
92
93        <ImageView
94
95            android:id="@+id/rating_image"
96
97            android:layout_width="15dp"
98
99            android:layout_height="15dp"
100
101            android:scaleType="centerCrop"
102
103            android:src="@drawable/star"
104
105            android:tint="@color/colorAccent" />
106
107
108
109
110
111        <TextView
112
113            android:id="@+id/rating"
114
115            android:layout_width="wrap_content"
116
117            android:layout_height="wrap_content"
118
119            android:layout_marginLeft="8dp"
120
121            tools:text="5.0"
122
123            android:layout_marginStart="8dp" />
124
125    </LinearLayout>
126
127
128
129</LinearLayout>
xml

MovieRepo is an interface which is implemented by SimulateMovieClient to imitate the background thread behavior to retrieve data as shown below.

MovieRepo.java

1public interface MovieRepo {
2
3    void getMovieList(MoviesListContract.OnResponseCallback callback);
4
5}
java

SimulateMovieClient contains a thread implementation which will give the response with the delay of 1500 milliseconds.

1public final class SimulateMovieClient implements MovieRepo{
2
3
4
5    static final Random RANDOM = new Random();
6
7
8
9
10
11    public void getMovieList(final MoviesListContract.OnResponseCallback callback)  {
12
13        // To imitate network request delay
14
15        new Handler().postDelayed(new Runnable() {
16
17            @Override
18
19            public void run() {
20
21                ArrayList<Movie> movies = new ArrayList<>();
22
23                try {
24
25                    movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "IT", Utility.convertStringToDate("2017-10-8"), 7.6, 127, Type.HORROR));
26
27                    // add more Movie object in list
28
29                    callback.onResponse(movies);
30
31                } catch (ParseException e) {
32
33                    callback.onError("Error from network");
34
35                }
36
37            }
38
39        }, 1500);
40
41
42
43    }
44
45}
java

Final visual output :

mvp demo

This concludes the introductory implementation of MVP and the complete code structure is available at github repository to play with.

Conclusion

MVP provides remarkable support for testing, scalability, and SOLID principals. One downside can be the boilerplate code but eventually it's worth your time to create complex applications and decoupled modules.

References

Dagger 2 : To supply the dependencies to modules like presenter rather than receiving it from constructor. This makes our module more adaptive to future changes.

RxJava : For reactive programming

Android Architecture Blueprints : Google repository to demonstrate working of various Android architectures.

Must read Testing MVP Architecture to implement specific tests for MVP architecture.


I hope that testing will now help you to be a stellar programmer, you can share your love by giving this guide a thumbs up.