Pluralsight Logo
Author avatar

Pavneet Singh

Author badge Author

MVP with Testing - Part 1 (MVP Architecture)

Pavneet Singh

Author BadgeAuthor
  • Jul 23, 2018
  • 22 Min read
  • 521 Views
  • Jul 23, 2018
  • 22 Min read
  • 521 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

1
2
3
4
5
6
7
8
9
package com.example.pavneet_singh.mvptestingdemo.mvp.movieslist;

----------------------------------------------------------------

.model                  : POJO and Repository pattern

.mvp.movielist          : MVP implementation for Movie List Screen

.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
2
// passive view
public class MoviesListActivity extends AppCompatActivity implements MoviesListContract.View{

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
1
public final class MoviePresenter implements MoviesListContract.Presenter  {

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface MoviesListContract {
   // implemented by MoviesListActivity to provide concrete implementation
   interface View {
       void showProgress(); // display progress bar

       void hideProgress(); // hide progress bar

       void showMovieList(List<Movie> movies); // receive response to display

       void showLoadingError(String errMsg); // display error
   }

   // implemented by MoviesPresenter to handle user event
   interface Presenter{
       void loadMoviewList();

       void dropView();
   }

   // implemented by MoviePresenter to receive response from asynchronous processes
   interface OnResponseCallback{
       void onResponse(List<Movie> movies);

       void onError(String errMsg);
   }
}

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public final class MoviePresenter implements MoviesListContract.Presenter  {
  // to keep reference to view
  private MoviesListContract.View view;

  // Repository pattern, mclient holds reference to concrete data retrieval implementation

  private MovieRepo mclient;

  public MoviePresenter(MoviesListContract.View view,MovieRepo client) {
    this.view = view;

    mclient = client;
  }

  @Override
  public void dropView() {
      view = null;
  }

  // would be triggered by MovieListActivity
  @Override
  public void loadMoviewList() {
      view.showProgress();

      mclient.getMovieList(callback);

      // required for espresso UI testing

      // to wait till response occurred

      EspressoTestingIdlingResource.increment();
  }

  // callback mechanism , onResponse will be triggered with response

  // by simulatemovieclient(or your network or database process) and pass the response to view
  private final OnResponseCallback callback = new OnResponseCallback() {
      @Override
      public void onResponse(List<Movie> movies) {

          view.showMovieList(movies);

          view.hideProgress();

          EspressoTestingIdlingResource.decrement();
      }

      @Override
      public void onError(String errMsg) {

          view.hideProgress();

          view.showLoadingError(errMsg);

          EspressoTestingIdlingResource.decrement();
      }
  };
}

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
public class MoviesListActivity extends AppCompatActivity implements MoviesListContract.View{
    private RecyclerView recyclerView;

    private MoviesAdapter moviesAdapter;

    private SwipeRefreshLayout swipeLayout;

    private MoviesListContract.Presenter presenter;

    private TextView tv_empty_msg;



    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_movie_list);

        initViews();
    }



    private void initViews(){

        presenter = new MoviePresenter(this, new SimulateMovieClient());

        recyclerView = (RecyclerView) findViewById(R.id.movies_recycler_list); // list

        tv_empty_msg = (TextView)findViewById(R.id.swipe_msg_tv); // empty message

        recyclerView.setLayoutManager(new LinearLayoutManager(this)); // for vertical liner list

        moviesAdapter = new MoviesAdapter(this);

        recyclerView.setAdapter(moviesAdapter);

        swipeLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_container);

        swipeLayout.setOnRefreshListener(listener);

        swipeLayout.setColorSchemeColors( // colors for progress dialog

                ContextCompat.getColor(MoviesListActivity.this, R.color.colorPrimary),

                ContextCompat.getColor(MoviesListActivity.this, R.color.colorAccent),

                ContextCompat.getColor(MoviesListActivity.this, android.R.color.holo_green_light)

        );

    }



    private final OnRefreshListener listener = new OnRefreshListener() {

        @Override

        public void onRefresh() {

            presenter.loadMoviewList();

        }

    };



    // for future, to show progress

    @Override

    public void showProgress() {

        swipeLayout.setRefreshing(true);

    }



    @Override

    public void hideProgress() {

        swipeLayout.setRefreshing(false);

    }



    // toggle the visibility of empty textview or list

    // display list only when response it not empty

    private void showORHideListView(boolean flag){

        if (flag){

            tv_empty_msg.setVisibility(View.GONE);

            recyclerView.setVisibility(View.VISIBLE);

        }else {

            tv_empty_msg.setVisibility(View.VISIBLE);

            recyclerView.setVisibility(View.GONE);

        }

    }



    @Override

    public void showMovieList(List<Movie> movies) {

        if (!movies.isEmpty()){

            moviesAdapter.setList(movies);

            showORHideListView(true);

        }

    }



    @Override

    public void showLoadingError(String errMsg) {

        hideProgressAndShowErr(errMsg);

        showORHideListView(false);

    }



    private void hideProgressAndShowErr(String msg){

        tv_empty_msg.setVisibility(View.VISIBLE);

        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();

        showORHideListView(false);

    }



    @Override

    protected void onDestroy() {

        super.onDestroy();

        presenter.dropView();

    }

}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout

    xmlns:android="http://schemas.android.com/apk/res/android"

    android:id="@+id/tasksContainer"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:scrollbars="vertical">



    <TextView

        android:text="@string/No_Data_MSG"

        android:gravity="center|center_horizontal"

        android:id="@+id/swipe_msg_tv"

        android:layout_width="match_parent"

        android:layout_height="match_parent" />



    <android.support.v4.widget.SwipeRefreshLayout

        android:id="@+id/swipe_container"

        android:layout_width="match_parent"

        android:layout_height="match_parent" >



        <android.support.v7.widget.RecyclerView

            android:id="@+id/movies_recycler_list"

            android:visibility="gone"

            android:layout_width="match_parent"

            android:layout_height="wrap_content"/>



    </android.support.v4.widget.SwipeRefreshLayout>

</RelativeLayout>

This is the adapter code to create list items

MoviesAdapter.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
public class MoviesAdapter extends RecyclerView.Adapter<MoviesAdapter.MovieHolder> {



    private final Context context;

    private final List<Movie> movStrings = new ArrayList<>();

    private static final String TAG = "MoviesAdapter";



    public MoviesAdapter(Context context) {

        this.context = context;

    }



    @Override

    public MovieHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        View view = LayoutInflater.from(context).inflate(R.layout.item_movie_model,parent,false);

        return new MovieHolder(view);

    }



    @Override

    public void onBindViewHolder(MovieHolder holder, final int position) {

        Log.e(TAG, "onBindViewHolder: "+position);

        holder.movieTitle.setText(movStrings.get(position).getTitle());

        holder.date.setText(Utility.convertMinutesToDuration(movStrings.get(position).getDurationinMinutes()));

        holder.rating.setText(Double.toString(movStrings.get(position).getRating()));

        holder.moviesLayout.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View v) {

                Toast.makeText(context, movStrings.get(position).toString(), Toast.LENGTH_SHORT).show();

            }

        });

    }



    public void setList(List<Movie> list){

        movStrings.clear();

        movStrings.addAll(list);

        notifyDataSetChanged();

        Log.e(TAG, "onNext: "+movStrings.size() );

    }





    @Override

    public int getItemCount() {

        return movStrings.size();

    }



    public static class MovieHolder extends RecyclerView.ViewHolder {

        TextView movieTitle;

        TextView date;

        TextView view;

        TextView rating;

        LinearLayout moviesLayout;



        public MovieHolder(View v) {

            super(v);

            moviesLayout = (LinearLayout) v.findViewById(R.id.movies_layout);

            movieTitle = (TextView) v.findViewById(R.id.title);

            date = (TextView) v.findViewById(R.id.date);

            rating = (TextView) v.findViewById(R.id.rating);

        }

    }

}

The layout item file item_movie_model.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:tools="http://schemas.android.com/tools"

    android:id="@+id/movies_layout"

    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    android:background="?android:attr/selectableItemBackground"

    android:gravity="center_vertical"

    android:minHeight="72dp"

    android:orientation="horizontal"

    android:padding="16dp">



    <LinearLayout

        android:layout_width="0dp"

        android:layout_height="wrap_content"

        android:layout_weight="1"

        android:orientation="vertical">



        <TextView



            android:id="@+id/title"

            tools:text="Avengers"

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:layout_gravity="top"

            android:paddingRight="16dp"

            android:textStyle="bold"

            android:textColor="@android:color/black"

            android:textSize="16sp" />





        <TextView

            android:id="@+id/date"

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:maxLines="1"

            android:paddingRight="16dp"

            android:textColor="@color/greyLight" />



    </LinearLayout>



    <LinearLayout

        android:layout_width="wrap_content"

        android:layout_height="35dp"

        android:orientation="horizontal">



        <ImageView

            android:id="@+id/rating_image"

            android:layout_width="15dp"

            android:layout_height="15dp"

            android:scaleType="centerCrop"

            android:src="@drawable/star"

            android:tint="@color/colorAccent" />





        <TextView

            android:id="@+id/rating"

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:layout_marginLeft="8dp"

            tools:text="5.0"

            android:layout_marginStart="8dp" />

    </LinearLayout>



</LinearLayout>

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

MovieRepo.java

1
2
3
4
5
public interface MovieRepo {

    void getMovieList(MoviesListContract.OnResponseCallback callback);

}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public final class SimulateMovieClient implements MovieRepo{



    static final Random RANDOM = new Random();





    public void getMovieList(final MoviesListContract.OnResponseCallback callback)  {

        // To imitate network request delay

        new Handler().postDelayed(new Runnable() {

            @Override

            public void run() {

                ArrayList<Movie> movies = new ArrayList<>();

                try {

                    movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "IT", Utility.convertStringToDate("2017-10-8"), 7.6, 127, Type.HORROR));

                    // add more Movie object in list

                    callback.onResponse(movies);

                } catch (ParseException e) {

                    callback.onError("Error from network");

                }

            }

        }, 1500);



    }

}

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.

6