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
Model : Stores the data and may contain the logic.
View : A View
is just a dummy placeholder to represent the output.
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
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.
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.
For the detailed explanation on clean architecture, read this article.
To address the architectural issues, MVP separates the application into three layers where all user events are delegated to Presenter.
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.
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:
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
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{
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.
MoviesListContract.Presenter
1public 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.
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}
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.
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}
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}
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>
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}
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>
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}
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}
Final visual output :
This concludes the introductory implementation of MVP and the complete code structure is available at github repository to play with.
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.
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.