In this tutorial, we explore the Model View Presenter pattern in more detail by implementing it in an Android application.
In this tutorial:
- we build a simple application using the MVP pattern
- we explore how to implement the MVP pattern on Android
- and we discuss how to overcome some difficulties caused by Android's architecture
1. Model View Presenter
The Model View Presenter pattern is an architectural pattern based on the Model View Controller (MVC) pattern that increases the separation of concerns and facilitates unit testing. It creates three layers, Model, View, andPresenter, each with a well defined responsibility.
The Model holds the business logic of the application. It controls how data is created, stored, and modified. The View is a passive interface that displays data and routes user actions to the Presenter. The Presenter acts as the middleman. It retrieves data from the Model and shows it in the View. It also processes user actions forwarded by the View.
2. Project Planning & Setup
We are going to build a simple notes application to illustrate MVP. The app allows the user to take notes, save them in a local database, and delete notes. To make it simple, the app will have only one Activity.
In this tutorial, we concentrate primarily on the implementation of the MVP pattern. Other functions, such as setting up a SQLite database, constructing aDAO, or handling user interaction, are skipped. If you need help with any of these topics, Envato Tuts+ has some great tutorials about these topics.
Action Diagram and MVP Layers
Let's start with the creation of a new note. If we break this action into smaller operations, then this is what the flow would look like using the MVP architectural pattern:
- The user types a note and clicks the add note button.
- The Presenter creates a
Note
object with the text entered by the user and asks the Model to insert it in the database. - The Model inserts the note in the database and informs the Presenter that the list of notes has changed.
- The Presenter clears the text field and asks the View to refresh its list to show the newly created note.
Let's now consider the operations needed to achieve this action and separate them using MVP. To keep the various objects loosely coupled, the communication between the layers takes places by using interfaces. We need four interfaces:
RequiredViewOps
: required View operations available to PresenterProvidedPresenterOps
: operations offered to View for communication with PresenterRequiredPresenterOps
: required Presenter operations available to ModelProvidedModelOps
: operations offered to Model to communicate with Presenter
3. Implementing MVP on Android
Now that we have an idea of how the various methods should be organized, we can start creating our app. We simplify the implementation by only focusing on the action to add a new note. The source files of this tutorial are available on GitHub.
We use only one
Activity
with a layout that includes:EditText
for new notesButton
to add a noteRecyclerView
to list all notes- two
TextView
elements and aButton
inside aRecyclerView
holder
Interfaces
Let’s begin by creating the interfaces. To keep everything organized, we place the interfaces within a holder. Again, in this example we focus on the action to add a new note.
01
02
03
04
05
06
07
08
09
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
| public interface MVP_Main { /** * Required View methods available to Presenter. * A passive layer, responsible to show data * and receive user interactions */ interface RequiredViewOps { // View operations permitted to Presenter Context getAppContext(); Context getActivityContext(); void notifyItemInserted( int layoutPosition); void notifyItemRangeChanged( int positionStart, int itemCount); } /** * Operations offered to View to communicate with Presenter. * Processes user interactions, sends data requests to Model, etc. */ interface ProvidedPresenterOps { // Presenter operations permitted to View void clickNewNote(EditText editText); // setting up recycler adapter int getNotesCount(); NotesViewHolder createViewHolder(ViewGroup parent, int viewType); void bindViewHolder(NotesViewHolder holder, int position); } /** * Required Presenter methods available to Model. */ interface RequiredPresenterOps { // Presenter operations permitted to Model Context getAppContext(); Context getActivityContext(); } /** * Operations offered to Model to communicate with Presenter * Handles all data business logic. */ interface ProvidedModelOps { // Model operations permitted to Presenter int getNotesCount(); Note getNote( int position); int insertNote(Note note); boolean loadData(); } } |
View Layer
It's now time to create the Model, View, and Presenter layers. Since
MainActivity
will act as the View, it should implement the RequiredViewOps
interface.
01
02
03
04
05
06
07
08
09
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
| public class MainActivity extends AppCompatActivity implements View.OnClickListener, MVP_Main.RequiredViewOps { private MVP_Main.ProvidedPresenterOps mPresenter; private EditText mTextNewNote; private ListNotes mListAdapter; @Override public void onClick(View v) { switch (v.getId()) { case R.id.fab:{ // Adds a new note mPresenter.clickNewNote(mTextNewNote); } } } @Override public Context getActivityContext() { return this ; } @Override public Context getAppContext() { return getApplicationContext(); } // Notify the RecyclerAdapter that a new item was inserted @Override public void notifyItemInserted( int adapterPos) { mListAdapter.notifyItemInserted(adapterPos); } // notify the RecyclerAdapter that items has changed @Override public void notifyItemRangeChanged( int positionStart, int itemCount){ mListAdapter.notifyItemRangeChanged(positionStart, itemCount); } // notify the RecyclerAdapter that data set has changed @Override public void notifyDataSetChanged() { mListAdapter.notifyDataSetChanged(); } // Recycler adapter // This class could have their own Presenter, but for the sake of // simplicity, will use only one Presenter. // The adapter is passive and all the processing occurs // in the Presenter layer. private class ListNotes extends RecyclerView.Adapter<NotesViewHolder> { @Override public int getItemCount() { return mPresenter.getNotesCount(); } @Override public NotesViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return mPresenter.createViewHolder(parent, viewType); } @Override public void onBindViewHolder(NotesViewHolder holder, int position) { mPresenter.bindViewHolder(holder, position); } } } |
Presenter Layer
The Presenter is the middleman and needs to implement two interfaces:
ProvidedPresenterOps
to allow calls from the ViewRequiredPresenterOps
to receive results from the Model
Pay special attention to the View layer reference. We need to use a
WeakReference<MVP_Main.RequiredViewOps>
sinceMainActivity
could be destroyed at any time and we want to avoid memory leaks. Also, the Model layer hasn't been set up yet. We do that later when we connect the MVP layers together.
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
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
| public class MainPresenter implements MVP_Main.ProvidedPresenterOps, MVP_Main.RequiredPresenterOps { // View reference. We use as a WeakReference // because the Activity could be destroyed at any time // and we don't want to create a memory leak private WeakReference<MVP_Main.RequiredViewOps> mView; // Model reference private MVP_Main.ProvidedModelOps mModel; /** * Presenter Constructor * @param view MainActivity */ public MainPresenter(MVP_Main.RequiredViewOps view) { mView = new WeakReference<>(view); } /** * Return the View reference. * Throw an exception if the View is unavailable. */ private MVP_Main.RequiredViewOps getView() throws NullPointerException{ if ( mView != null ) return mView.get(); else throw new NullPointerException( "View is unavailable" ); } /** * Retrieves total Notes count from Model * @return Notes list size */ @Override public int getNotesCount() { return mModel.getNotesCount(); } /** * Creates the RecyclerView holder and setup its view * @param parent Recycler viewGroup * @param viewType Holder type * @return Recycler ViewHolder */ @Override public NotesViewHolder createViewHolder(ViewGroup parent, int viewType) { NotesViewHolder viewHolder; LayoutInflater inflater = LayoutInflater.from(parent.getContext()); View viewTaskRow = inflater.inflate(R.layout.holder_notes, parent, false ); viewHolder = new NotesViewHolder(viewTaskRow); return viewHolder; } /** * Binds ViewHolder with RecyclerView * @param holder Holder to bind * @param position Position on Recycler adapter */ @Override public void bindViewHolder( final NotesViewHolder holder, int position) { final Note note = mModel.getNote(position); holder.text.setText( note.getText() ); holder.date.setText( note.getDate() ); holder.btnDelete.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { clickDeleteNote(note, holder.getAdapterPosition(), holder.getLayoutPosition()); } }); } /** * @return Application context */ @Override public Context getAppContext() { try { return getView().getAppContext(); } catch (NullPointerException e) { return null ; } } /** * @return Activity context */ @Override public Context getActivityContext() { try { return getView().getActivityContext(); } catch (NullPointerException e) { return null ; } } /** * Called by View when user clicks on new Note button. * Creates a Note with text typed by the user and asks * Model to insert it in DB. * @param editText EditText with text typed by user */ @Override public void clickNewNote( final EditText editText) { getView().showProgress(); final String noteText = editText.getText().toString(); if ( !noteText.isEmpty() ) { new AsyncTask<Void, Void, Integer>() { @Override protected Integer doInBackground(Void... params) { // Inserts note in Model, returning adapter position return mModel.insertNote(makeNote(noteText)); } @Override protected void onPostExecute(Integer adapterPosition) { try { if (adapterPosition > - 1 ) { // Note inserted getView().clearEditText(); getView().notifyItemInserted(adapterPosition + 1 ); getView().notifyItemRangeChanged(adapterPosition, mModel.getNotesCount()); } else { // Informs about error getView().hideProgress(); getView().showToast(makeToast( "Error creating note [" + noteText + "]" )); } } catch (NullPointerException e) { e.printStackTrace(); } } }.execute(); } else { try { getView().showToast(makeToast( "Cannot add a blank note!" )); } catch (NullPointerException e) { e.printStackTrace(); } } } /** * Creates a Note object with given text * @param noteText String with Note text * @return A Note object */ public Note makeNote(String noteText) { Note note = new Note(); note.setText( noteText ); note.setDate(getDate()); return note; } } |
Comments
Post a Comment