1. Code
  2. Coding Fundamentals
  3. Design Patterns

Wie kann man Model View Presenter unter Android übernehmen?

Im vorherigen Tutorial haben wir über das Model View Presenter-Muster gesprochen, wie es auf Android angewendet wird und was seine wichtigsten Vorteile sind. In diesem Tutorial untersuchen wir das Model View Presenter-Muster detaillierter, indem wir es in einer Android-Anwendung implementieren.
Scroll to top
This post is part of a series called How to Adopt Model View Presenter on Android.
An Introduction to Model View Presenter on Android
Testing and Dependency Injection With Model View Presenter on Android

German (Deutsch) translation by Valentina (you can also view the original English article)

Im vorherigen Tutorial haben wir über das Model View Presenter-Muster gesprochen, wie es auf Android angewendet wird und was seine wichtigsten Vorteile sind. In diesem Tutorial untersuchen wir das Model View Presenter-Muster detaillierter, indem wir es in einer Android-Anwendung implementieren.

In diesem Tutorial:

  • wir erstellen eine einfache Anwendung mit dem MVP-Muster
  • wir untersuchen, wie das MVP-Muster auf Android implementiert wird
  • und wir diskutieren, wie einige Schwierigkeiten überwunden werden können, die durch die Android-Architektur verursacht werden

1. Model View Presenter

Das Model View Presenter-Muster ist ein Architekturmuster, das auf dem Model View Controller (MVC)-Muster basiert und die Trennung von Bedenken erhöht und das Testen von Einheiten erleichtert. Es werden drei Layers erstellt: Modell, Ansicht und Präsentator mit jeweils genau definierter Verantwortung.

Model View Presenter LayersModel View Presenter LayersModel View Presenter Layers

Das Modell enthält die Geschäftslogik der Anwendung. Es steuert, wie Daten erstellt, gespeichert und geändert werden. Die Ansicht ist eine passive Oberfläche, die Daten anzeigt und Benutzeraktionen an den Präsentator weiterleitet. Der Moderator fungiert als Vermittler. Es ruft Daten aus dem Modell ab und zeigt sie in der Ansicht an. Es verarbeitet auch Benutzeraktionen, die von der Ansicht weitergeleitet werden.

2. Projektplanung und -einrichtung

Wir werden eine einfache Notizenanwendung erstellen, um MVP zu veranschaulichen. Mit der App kann der Benutzer Notizen machen, diese in einer lokalen Datenbank speichern und Notizen löschen. Zur Vereinfachung verfügt die App nur über eine Aktivität.

App LayoutApp LayoutApp Layout

In diesem Tutorial konzentrieren wir uns hauptsächlich auf die Implementierung des MVP-Musters. Andere Funktionen, wie das Einrichten einer SQLite-Datenbank, das Erstellen eines DAO oder das Behandeln der Benutzerinteraktion, werden übersprungen. Wenn Sie Hilfe zu einem dieser Themen benötigen, bietet Envato Tuts+ einige großartige Tutorials zu diesen Themen.

Aktionsdiagramm und MVP-Layers

Beginnen wir mit der Erstellung einer neuen Notiz. Wenn wir diese Aktion in kleinere Operationen aufteilen, sieht der Ablauf anhand des MVP-Architekturmusters folgendermaßen aus:

  • Der Benutzer gibt eine Notiz ein und klickt auf die Schaltfläche Notiz hinzufügen.
  • Der Präsentator erstellt ein Note-Objekt mit dem vom Benutzer eingegebenen Text und fordert das Modell auf, es in die Datenbank einzufügen.
  • Das Modell fügt die Notiz in die Datenbank ein und informiert den Präsentator darüber, dass sich die Liste der Notizen geändert hat.
  • Der Präsentator löscht das Textfeld und fordert die Ansicht auf, die Liste zu aktualisieren, um die neu erstellte Notiz anzuzeigen.
MVP Action DiagramMVP Action DiagramMVP Action Diagram

MVP-Schnittstellen

Betrachten wir nun die Operationen, die erforderlich sind, um diese Aktion auszuführen, und trennen Sie sie mithilfe von MVP. Um die verschiedenen Objekte locker miteinander zu verbinden, erfolgt die Kommunikation zwischen den Layers über Schnittstellen. Wir brauchen vier Schnittstellen:

  • RequiredViewOps: Erforderlich Für Presenter verfügbare Ansichtsvorgänge
  • ProvidedPresenterOps: Vorgänge, die View zur Kommunikation mit Presenter angeboten werden
  • RequiredPresenterOps: Erforderliche Presenter-Vorgänge, die Model zur Verfügung stehen
  • ProvidedModelOps: Vorgänge, die Model zur Kommunikation mit Presenter angeboten werden
MVP InterfacesMVP InterfacesMVP Interfaces

3. Implementierung von MVP unter Android

Nachdem wir eine Vorstellung davon haben, wie die verschiedenen Methoden organisiert werden sollen, können wir mit der Erstellung unserer App beginnen. Wir vereinfachen die Implementierung, indem wir uns nur auf die Aktion konzentrieren, um eine neue Notiz hinzuzufügen. Die Quelldateien dieses Tutorials sind auf GitHub verfügbar.

Wir verwenden nur eine Activity mit einem Layout, das Folgendes umfasst:

  • EditText für neue Notizen
  • Button zum Hinzufügen einer Notiz
  • RecyclerView, um alle Notizen aufzulisten
  • zwei TextView-Elemente und eine Button in einem RecyclerView-Halter

Schnittstellen

Beginnen wir mit der Erstellung der Schnittstellen. Um alles organisiert zu halten, platzieren wir die Schnittstellen in einem Halter. Auch in diesem Beispiel konzentrieren wir uns auf die Aktion zum Hinzufügen einer neuen Notiz.

1
public interface MVP_Main {
2
    /**

3
     * Required View methods available to Presenter.

4
     * A passive layer, responsible to show data

5
     * and receive user interactions

6
     */
7
    interface RequiredViewOps {
8
    	// View operations permitted to Presenter

9
		Context getAppContext();
10
        	Context getActivityContext();
11
		void notifyItemInserted(int layoutPosition);
12
        	void notifyItemRangeChanged(int positionStart, int itemCount);
13
    }
14
15
    /**

16
     * Operations offered to View to communicate with Presenter.

17
     * Processes user interactions, sends data requests to Model, etc.

18
     */
19
    interface ProvidedPresenterOps {
20
       	// Presenter operations permitted to View

21
		void clickNewNote(EditText editText);
22
            // setting up recycler adapter

23
            int getNotesCount();
24
            NotesViewHolder createViewHolder(ViewGroup parent, int viewType);
25
            void bindViewHolder(NotesViewHolder holder, int position);
26
    }
27
28
    /**

29
     * Required Presenter methods available to Model.

30
     */
31
    interface RequiredPresenterOps {
32
       	// Presenter operations permitted to Model

33
		Context getAppContext();
34
        	Context getActivityContext();
35
    }
36
37
    /**

38
     * Operations offered to Model to communicate with Presenter

39
     * Handles all data business logic.

40
     */
41
    interface ProvidedModelOps {
42
        	// Model operations permitted to Presenter

43
            int getNotesCount();
44
            Note getNote(int position);
45
		int insertNote(Note note);
46
        	boolean loadData();
47
    }
48
}

View Layer

Jetzt ist es Zeit, die Layers Modell, Ansicht und Präsentator zu erstellen. Da MainActivity als Ansicht fungiert, sollte die RequiredViewOps-Schnittstelle implementiert werden.

1
public class MainActivity
2
        extends AppCompatActivity
3
    implements View.OnClickListener, MVP_Main.RequiredViewOps {
4
5
    private MVP_Main.ProvidedPresenterOps mPresenter;
6
    private EditText mTextNewNote;
7
    private ListNotes mListAdapter;
8
9
    @Override
10
    public void onClick(View v) {
11
        switch (v.getId()) {
12
            case R.id.fab:{
13
                // Adds a new note

14
                mPresenter.clickNewNote(mTextNewNote);
15
            }
16
        }
17
    }
18
    @Override
19
    public Context getActivityContext() {
20
        return this;
21
    }
22
23
    @Override
24
    public Context getAppContext() {
25
        return getApplicationContext();
26
    }
27
    // Notify the RecyclerAdapter that a new item was inserted

28
    @Override
29
    public void notifyItemInserted(int adapterPos) {
30
        mListAdapter.notifyItemInserted(adapterPos);
31
    }
32
    // notify the RecyclerAdapter that items has changed

33
    @Override
34
    public void notifyItemRangeChanged(int positionStart, int itemCount){
35
        mListAdapter.notifyItemRangeChanged(positionStart, itemCount);
36
    }
37
    // notify the RecyclerAdapter that data set has changed

38
    @Override
39
    public void notifyDataSetChanged() {
40
        mListAdapter.notifyDataSetChanged();
41
    }
42
    // Recycler adapter

43
    // This class could have their own Presenter, but for the sake of

44
    // simplicity, will use only one Presenter.

45
    // The adapter is passive and all the processing occurs 

46
    // in the Presenter layer.

47
    private class ListNotes extends RecyclerView.Adapter<NotesViewHolder>       
48
    {
49
        @Override
50
        public int getItemCount() {
51
            return mPresenter.getNotesCount();
52
        }
53
54
        @Override
55
        public NotesViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
56
            return mPresenter.createViewHolder(parent, viewType);
57
        }
58
59
        @Override
60
        public void onBindViewHolder(NotesViewHolder holder, int position) {
61
            mPresenter.bindViewHolder(holder, position);
62
        }
63
    }
64
}

Presenter-Layer

Der Moderator ist der Mittelsmann und muss zwei Schnittstellen implementieren:

  • ProvidedPresenterOps, um Anrufe aus der Ansicht zuzulassen
  • RequiredPresenterOps, um Ergebnisse vom Modell zu erhalten

Achten Sie besonders auf die Ansichtslayerreferenz. Wir müssen eine WeakReference<MVP_Main.RequiredViewOps> verwenden, da MainActivity jederzeit zerstört werden kann und wir Speicherlecks vermeiden möchten. Außerdem wurde die Modellebene noch nicht eingerichtet. Das machen wir später, wenn wir die MVP-Layers miteinander verbinden.

1
public class MainPresenter implements MVP_Main.ProvidedPresenterOps, MVP_Main.RequiredPresenterOps {
2
3
    // View reference. We use as a WeakReference

4
    // because the Activity could be destroyed at any time

5
    // and we don't want to create a memory leak

6
    private WeakReference<MVP_Main.RequiredViewOps> mView;
7
    // Model reference

8
    private MVP_Main.ProvidedModelOps mModel;
9
10
    /**

11
     * Presenter Constructor

12
     * @param view  MainActivity

13
     */
14
    public MainPresenter(MVP_Main.RequiredViewOps view) {
15
        mView = new WeakReference<>(view);
16
    }
17
18
    /**

19
     * Return the View reference.

20
     * Throw an exception if the View is unavailable.

21
     */
22
    private MVP_Main.RequiredViewOps getView() throws NullPointerException{
23
        if ( mView != null )
24
            return mView.get();
25
        else
26
            throw new NullPointerException("View is unavailable");
27
    }
28
29
    /**

30
     * Retrieves total Notes count from Model

31
     * @return  Notes list size

32
     */
33
    @Override
34
    public int getNotesCount() {
35
        return mModel.getNotesCount();
36
    }
37
38
    /**

39
     * Creates the RecyclerView holder and setup its view

40
     * @param parent    Recycler viewGroup

41
     * @param viewType  Holder type

42
     * @return          Recycler ViewHolder

43
     */
44
    @Override
45
    public NotesViewHolder createViewHolder(ViewGroup parent, int viewType) {
46
        NotesViewHolder viewHolder;
47
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
48
49
        View viewTaskRow = inflater.inflate(R.layout.holder_notes, parent, false);
50
        viewHolder = new NotesViewHolder(viewTaskRow);
51
52
        return viewHolder;
53
    }
54
55
    /**

56
     * Binds ViewHolder with RecyclerView

57
     * @param holder    Holder to bind

58
     * @param position  Position on Recycler adapter

59
     */
60
    @Override
61
    public void bindViewHolder(final NotesViewHolder holder, int position) {
62
        final Note note = mModel.getNote(position);
63
        holder.text.setText( note.getText() );
64
        holder.date.setText( note.getDate() );
65
        holder.btnDelete.setOnClickListener(new View.OnClickListener() {
66
            @Override
67
            public void onClick(View v) {
68
                clickDeleteNote(note, holder.getAdapterPosition(), holder.getLayoutPosition());
69
            }
70
        });
71
72
    }
73
74
    /**

75
     * @return  Application context

76
     */
77
    @Override
78
    public Context getAppContext() {
79
        try {
80
            return getView().getAppContext();
81
        } catch (NullPointerException e) {
82
            return null;
83
        }
84
    }
85
86
    /**

87
     * @return  Activity context

88
     */
89
    @Override
90
    public Context getActivityContext() {
91
        try {
92
            return getView().getActivityContext();
93
        } catch (NullPointerException e) {
94
            return null;
95
        }
96
    }
97
98
    /**

99
     * Called by View when user clicks on new Note button.

100
     * Creates a Note with text typed by the user and asks

101
     * Model to insert it in DB.

102
     * @param editText  EditText with text typed by user

103
     */
104
    @Override
105
    public void clickNewNote(final EditText editText) {
106
        getView().showProgress();
107
        final String noteText = editText.getText().toString();
108
        if ( !noteText.isEmpty() ) {
109
            new AsyncTask<Void, Void, Integer>() {
110
                @Override
111
                protected Integer doInBackground(Void... params) {
112
                    // Inserts note in Model, returning adapter position

113
                    return mModel.insertNote(makeNote(noteText));
114
                }
115
116
                @Override
117
                protected void onPostExecute(Integer adapterPosition) {
118
                    try {
119
                        if (adapterPosition > -1) {
120
                            // Note inserted

121
                            getView().clearEditText();
122
                            getView().notifyItemInserted(adapterPosition + 1);
123
                            getView().notifyItemRangeChanged(adapterPosition, mModel.getNotesCount());
124
                        } else {
125
                            // Informs about error

126
                            getView().hideProgress();
127
                            getView().showToast(makeToast("Error creating note [" + noteText + "]"));
128
                        }
129
                    } catch (NullPointerException e) {
130
                        e.printStackTrace();
131
                    }
132
                }
133
            }.execute();
134
        } else {
135
            try {
136
                getView().showToast(makeToast("Cannot add a blank note!"));
137
            } catch (NullPointerException e) {
138
                e.printStackTrace();
139
            }
140
        }
141
    }
142
143
    /**

144
     * Creates a Note object with given text

145
     * @param noteText  String with Note text

146
     * @return  A Note object

147
     */
148
    public Note makeNote(String noteText) {
149
        Note note = new Note();
150
        note.setText( noteText );
151
        note.setDate(getDate());
152
        return note;
153
154
    }
155
}

Modell Layer

Die Modell Layer ist für die Verwaltung der Geschäftslogik verantwortlich. Es enthält eine ArrayList mit den zur Datenbank hinzugefügten Notizen, eine DAO-Referenz zum Ausführen von Datenbankoperationen und eine Referenz zum Presenter.

1
public class MainModel implements MVP_Main.ProvidedModelOps {
2
3
    // Presenter reference

4
    private MVP_Main.RequiredPresenterOps mPresenter;
5
    private DAO mDAO;
6
    // Recycler data

7
    public ArrayList<Note> mNotes;
8
9
    /**

10
     * Main constructor, called by Activity during MVP setup

11
     * @param presenter Presenter instance

12
     */
13
    public MainModel(MVP_Main.RequiredPresenterOps presenter) {
14
        this.mPresenter = presenter;
15
        mDAO = new DAO( mPresenter.getAppContext() );
16
    }
17
18
     /**

19
     * Inserts a note on DB

20
     * @param note  Note to insert

21
     * @return      Note's position on ArrayList

22
     */
23
    @Override
24
    public int insertNote(Note note) {
25
        Note insertedNote = mDAO.insertNote(note);
26
        if ( insertedNote != null ) {
27
            loadData();
28
            return getNotePosition(insertedNote);
29
        }
30
        return -1;
31
    }
32
33
     /**

34
     * Loads all Data, getting notes from DB

35
     * @return  true with success

36
     */
37
    @Override
38
    public boolean loadData() {
39
        mNotes = mDAO.getAllNotes();
40
        return mNotes != null;
41
    }
42
     /**

43
     * Gets a specific note from notes list using its array position

44
     * @param position    Array position

45
     * @return            Note from list

46
     */
47
    @Override
48
    public Note getNote(int position) {
49
        return mNotes.get(position);
50
    }
51
     
52
    /**

53
     * Get ArrayList size

54
     * @return  ArrayList size

55
     */
56
    @Override
57
    public int getNotesCount() {
58
        if ( mNotes != null )
59
            return mNotes.size();
60
        return 0;
61
    }
62
}

4. Alles zusammenbinden

Wenn die MVP-Layers vorhanden sind, müssen wir sie instanziieren und die erforderlichen Referenzen einfügen. Bevor wir dies tun, müssen wir einige Probleme ansprechen, die in direktem Zusammenhang mit Android stehen.

Instanziieren der Layers

Da Android die Instanziierung einer Activity nicht zulässt, wird die Ansichtslayer für uns instanziiert. Wir sind für die Instanziierung der Layers Presenter und Model verantwortlich. Leider kann das Instanziieren dieser Layers außerhalb der Activity problematisch sein.

Es wird empfohlen, eine Form der Abhängigkeitsinjektion zu verwenden, um dies zu erreichen. Da unser Ziel darin besteht, uns auf die MVP-Implementierung zu konzentrieren, werden wir einen einfacheren Ansatz verfolgen. Dies ist nicht der beste verfügbare Ansatz, aber am einfachsten zu verstehen. Wir werden später in dieser Serie über MVP und Abhängigkeitsinjektion sprechen.

  • instanziieren Sie den Präsentator und das Modell in der Aktivität mithilfe lokaler Variablen
  • richten Sie RequiredViewOps und ProvidedModelOps im Presenter ein
  • richten Sie RequiredPresenterOps im Modell ein
  • speichern Sie ProvidedPresenterOps als Referenz für die Ansicht
1
/**

2
* Setup Model View Presenter pattern

3
*/
4
private void setupMVP() {
5
     // Create the Presenter

6
     MainPresenter presenter = new MainPresenter(this);
7
     // Create the Model

8
     MainModel model = new MainModel(presenter);
9
     // Set Presenter model

10
     presenter.setModel(model);
11
     // Set the Presenter as a interface

12
     mPresenter = presenter;
13
}

Umgang mit Konfigurationsänderungen

Eine andere Sache, die wir berücksichtigen sollten, ist der Lebenszyklus der Aktivität. Die Activity von Android kann jederzeit zerstört werden, und die Layers Präsentator und Modell können auch damit zerstört werden. Wir müssen dies beheben, indem wir eine Art Zustandsmaschine verwenden, um den Zustand während Konfigurationsänderungen zu speichern. Wir sollten auch die anderen Layers über den Status der Aktivität informieren.

Um dies zu erreichen, verwenden wir eine separate Klasse, StateMaintainer, die ein Fragment enthält, das seinen Status beibehält und dieses Fragment zum Speichern und Abrufen unserer Objekte verwendet. Sie können sich die Implementierung dieser Klasse in den Quelldateien dieses Tutorials ansehen.

Wir müssen dem Präsentator und dem Modell eine onDestroy-Methode hinzufügen, um sie über den aktuellen Status der Aktivität zu informieren. Wir müssen dem Präsentator auch eine setView-Methode hinzufügen, die für den Empfang einer neuen Ansichtsreferenz der neu erstellten Aktivität verantwortlich ist.

View LifecycleView LifecycleView Lifecycle
1
public class MainActivity
2
        extends AppCompatActivity
3
    implements View.OnClickListener, MVP_Main.RequiredViewOps
4
{
5
    // …

6
    private void setupMVP() {
7
        // Check if StateMaintainer has been created

8
        if (mStateMaintainer.firstTimeIn()) {
9
            // Create the Presenter

10
            MainPresenter presenter = new MainPresenter(this);
11
            // Create the Model

12
            MainModel model = new MainModel(presenter);
13
            // Set Presenter model

14
            presenter.setModel(model);
15
            // Add Presenter and Model to StateMaintainer

16
            mStateMaintainer.put(presenter);
17
            mStateMaintainer.put(model);
18
19
            // Set the Presenter as a interface

20
            // To limit the communication with it

21
            mPresenter = presenter;
22
23
        }
24
        // get the Presenter from StateMaintainer

25
        else {
26
            // Get the Presenter

27
            mPresenter = mStateMaintainer.get(MainPresenter.class.getName());
28
            // Updated the View in Presenter

29
            mPresenter.setView(this);
30
        }
31
    }
32
    // …

33
}

Abschluss

Das MVP-Muster kann einige Probleme lösen, die durch die Standardarchitektur von Android verursacht werden. Es macht Ihren Code einfach zu warten und zu testen. Die Einführung von MVP mag zunächst schwierig aussehen, aber sobald Sie die Logik dahinter verstanden haben, ist der gesamte Prozess unkompliziert.

Sie können jetzt Ihre eigene MVP-Bibliothek erstellen oder eine bereits verfügbare Lösung wie Mosby oder simple-mvp verwenden. Sie sollten jetzt besser verstehen, was diese Bibliotheken hinter den Kulissen tun.

Wir sind fast am Ende unserer MVP-Reise. Im dritten und letzten Teil dieser Serie werden wir dem Mix Unit-Tests hinzufügen und unseren Code anpassen, um die Abhängigkeitsinjektion mit Dagger zu verwenden. Ich hoffe Sie dort zu sehen.