Articles

Android Architecture Patterns Part 2:Model-View-Presenter

Florina Muntenescu
Florina Muntenescu

Follow

Nov 2, 2016 · 6 min read

It’s about time we developers start thinking about how we can apply good architecture patterns in our Android apps. Um dabei zu helfen, bietet Google Android Architecture Blueprints an, bei denen Erik Hellman und ich gemeinsam am MVP & RxJava-Beispiel gearbeitet haben. Schauen wir uns an, wie wir es angewendet haben und welche Vor- und Nachteile dieser Ansatz hat.

Hier sind die Rollen jeder Komponente:

  • Modell — die Datenschicht. Verantwortlich für den Umgang mit der Geschäftslogik und die Kommunikation mit den Netzwerk- und Datenbankschichten.
  • Ansicht – die UI-Ebene. Zeigt die Daten an und benachrichtigt den Moderator über Benutzeraktionen.
  • Presenter – ruft die Daten aus dem Modell ab, wendet die UI-Logik an und verwaltet den Status der Ansicht, entscheidet, was angezeigt werden soll, und reagiert auf Benutzereingabebenachrichtigungen aus der Ansicht.

Da die Ansicht und der Präsentator eng zusammenarbeiten, müssen sie einen Verweis aufeinander haben. Um die Präsentationseinheit mit JUnit testbar zu machen, wird die Ansicht abstrahiert und eine Schnittstelle dafür verwendet. Die Beziehung zwischen dem Präsentator und der entsprechenden Ansicht ist in einer Contract Schnittstellenklasse definiert, wodurch der Code lesbarer und die Verbindung zwischen den beiden leichter verständlich wird.

Model-View-Presenter-Klassenstruktur

Das Model-View-Presenter-Muster & RxJava in Android Architecture Blueprints

Das Blueprint-Beispiel ist eine „To Do“ -Anwendung. Es ermöglicht einem Benutzer, „To Do“ -Aufgaben zu erstellen, zu lesen, zu aktualisieren und zu löschen sowie Filter auf die angezeigte Aufgabenliste anzuwenden. RxJava wird verwendet, um den Hauptthread zu verlassen und asynchrone Vorgänge verarbeiten zu können.

Modell

Das Modell arbeitet mit den entfernten und lokalen Datenquellen zusammen, um die Daten abzurufen und zu speichern. Hier wird die Geschäftslogik gehandhabt. Wenn beispielsweise die Liste der Tasks angefordert wird, versucht das Modell, sie aus der lokalen Datenquelle abzurufen. Wenn es leer ist, fragt es das Netzwerk ab, speichert die Antwort in der lokalen Datenquelle und gibt dann die Liste zurück.

Das Abrufen von Aufgaben erfolgt mit Hilfe von RxJava:

public Observable<List<Task>> getTasks(){ 
...
}

Das Modell erhält als Parameter in den Konstruktorschnittstellen der lokalen und entfernten Datenquellen, wodurch das Modell völlig unabhängig von irgendwelchen Android-Klassen ist und somit einfach mit JUnit zu testen ist. Um beispielsweise zu testen, dass getTasks Daten von der lokalen Quelle anfordert, haben wir den folgenden Test implementiert:

@Mock 
private TasksDataSource mTasksRemoteDataSource;
@Mock
private TasksDataSource mTasksLocalDataSource;
...
@Test
public void getTasks_requestsAllTasksFromLocalDataSource() {
// Given that the local data source has data available
setTasksAvailable(mTasksLocalDataSource, TASKS);
// And the remote data source does not have any data available
setTasksNotAvailable(mTasksRemoteDataSource);
// When tasks are requested from the tasks repository
TestSubscriber<List<Task>> testSubscriber =
new TestSubscriber<>();
mTasksRepository.getTasks().subscribe(testSubscriber); // Then tasks are loaded from the local data source
verify(mTasksLocalDataSource).getTasks();
testSubscriber.assertValue(TASKS);
}

View

Die Ansicht zeigt die Daten mit dem Presenter an und benachrichtigt den Presenter über die Aktionen des Benutzers. In MVP-Aktivitäten können Fragmente und benutzerdefinierte Android-Ansichten Ansichten sein. Unsere Wahl war, Fragmente zu verwenden.

Alle Ansichten implementieren dieselbe BaseView-Schnittstelle, die das Festlegen eines Präsentators ermöglicht.

public interface BaseView<T> {
void setPresenter(T presenter);
}

Die Ansicht benachrichtigt den Moderator, dass sie bereit ist, aktualisiert zu werden, indem sie die subscribeMethode des Moderators in onResumeaufruft. Die Ansicht ruft presenter.unsubscribe() in onPause auf, um dem Moderator mitzuteilen, dass er nicht mehr an einer Aktualisierung interessiert ist. Wenn die Implementierung der Ansicht eine benutzerdefinierte Android-Ansicht ist, müssen die Methoden subscribe und unsubscribe aufgerufen werden onAttachedToWindow und onDetachedFromWindow. Benutzeraktionen wie Schaltflächenklicks lösen entsprechende Methoden im Presenter aus, die entscheiden, was als nächstes passieren soll.

Die Ansichten werden mit Espresso getestet. Der Statistikbildschirm muss beispielsweise die Anzahl der aktiven und abgeschlossenen Aufgaben anzeigen. Der Test, der überprüft, ob dies korrekt durchgeführt wird, legt zuerst einige Aufgaben in die TaskRepository; startet dann die StatisticsActivity und überprüft den Inhalt der Ansichten:

@Before 
public void setup() {
// Given some tasks
TasksRepository.destroyInstance();
TasksRepository repository = Injection.provideTasksRepository( InstrumentationRegistry.getContext()); repository.saveTask(new Task("Title1", "", false));
repository.saveTask(new Task("Title2", "", true)); // Lazily start the Activity from the ActivityTestRule
Intent startIntent = new Intent();
mStatisticsActivityTestRule.launchActivity(startIntent);
}@Test
public void Tasks_ShowsNonEmptyMessage() throws Exception {
// Check that the active and completed tasks text is displayed
Context context = InstrumentationRegistry.getTargetContext();
String expectedActiveTaskText = context
.getString(R.string.statistics_active_tasks);
String expectedCompletedTaskText = context
.getString(R.string.statistics_completed_tasks); onView(withText(containsString(expectedActiveTaskText)))
.check(matches(isDisplayed()));
onView(withText(containsString(expectedCompletedTaskText)))
.check(matches(isDisplayed()));
}

Presenter

Der Presenter und seine entsprechende Ansicht werden von der Aktivität erstellt. Verweise auf die Ansicht und auf das TaskRepository – das Modell – werden dem Konstruktor des Präsentators übergeben. Bei der Implementierung des Konstruktors ruft der Präsentator die setPresenter -Methode der Ansicht auf. Dies kann vereinfacht werden, wenn ein Abhängigkeitsinjektionsframework verwendet wird, das die Injektion der Präsentatoren in die entsprechenden Ansichten ermöglicht, wodurch die Kopplung der Klassen verringert wird. Die Implementierung des ToDo-MVP mit Dagger wird in einem anderen Beispiel behandelt.

Alle Moderatoren implementieren die gleiche BasePresenter-Schnittstelle.

public interface BasePresenter {
void subscribe();
void unsubscribe();
}

Wenn die subscribe -Methode aufgerufen wird, fordert der Presenter die Daten vom Modell an, wendet dann die UI-Logik auf die empfangenen Daten an und setzt sie auf die Ansicht. Zum Beispiel werden in StatisticsPresenter alle Aufgaben von TaskRepository angefordert – dann werden die abgerufenen Aufgaben verwendet, um die Anzahl der aktiven und abgeschlossenen Aufgaben zu berechnen. Diese Nummern werden als Parameter für die showStatistics(int numberOfActiveTasks, int numberOfCompletedTasks) -Methode der Ansicht verwendet.

Ein Unit-Test, um zu überprüfen, ob tatsächlich die showStatistics -Methode mit den richtigen Werten aufgerufen wird, ist einfach zu implementieren. Wir verspotten die TaskRepository und die StatisticsContract.View und geben die verspotteten Objekte als Parameter an den Konstruktor eines StatisticsPresenter Objekts weiter. Die Testimplementierung lautet:

@Test 
public void loadNonEmptyTasksFromRepository_CallViewToDisplay() {
// Given an initialized StatisticsPresenter with
// 1 active and 2 completed tasks
setTasksAvailable(TASKS); // When loading of Tasks is requested
mStatisticsPresenter.subscribe(); // Then the correct data is passed on to the view
verify(mStatisticsView).showStatistics(1, 2);
}

Die Rolle der unsubscribe -Methode besteht darin, alle Abonnements des Präsentators zu löschen und so Speicherlecks zu vermeiden.

Abgesehen von subscribe und unsubscribe stellt jeder Presenter andere Methoden zur Verfügung, die den Benutzeraktionen in der Ansicht entsprechen. Zum Beispiel fügt das AddEditTaskPresenter Methoden wie createTask hinzu, die aufgerufen werden, wenn der Benutzer die Schaltfläche drückt, die eine neue Aufgabe erstellt. Dies stellt sicher, dass alle Benutzeraktionen – und folglich die gesamte UI-Logik – den Presenter durchlaufen und dadurch Unit-getestet werden können.

Nachteile des Model-View-Presenter-Musters

Das Model-View-Presenter-Muster bringt eine sehr gute Trennung von Bedenken mit sich. Während dies sicher ein Profi ist, kann dies bei der Entwicklung einer kleinen App oder eines Prototyps wie ein Overhead erscheinen. Um die Anzahl der verwendeten Schnittstellen zu verringern, entfernen einige Entwickler die Contract Schnittstellenklasse und die Schnittstelle für den Presenter.

Eine der Fallstricke von MVP tritt auf, wenn die UI-Logik in den Presenter verschoben wird: Dies wird jetzt zu einer allwissenden Klasse mit Tausenden von Codezeilen. Um dies zu lösen, teilen Sie den Code noch mehr auf und denken Sie daran, Klassen zu erstellen, die nur eine Verantwortung haben und komponententestbar sind.

Fazit

Das Model-View-Controller-Muster hat zwei Hauptnachteile: Erstens hat die Ansicht einen Verweis sowohl auf den Controller als auch auf das Modell; und zweitens beschränkt es die Handhabung der UI-Logik nicht auf eine einzige Klasse, wobei diese Verantwortung zwischen dem Controller und der Ansicht oder dem Modell geteilt wird. Das Model-View-Presenter-Muster löst diese beiden Probleme, indem es die Verbindung der Ansicht mit dem Modell unterbricht und nur eine Klasse erstellt, die alles behandelt, was mit der Präsentation der Ansicht zu tun hat — den Presenter: eine einzelne Klasse, die einfach zu testen ist.

Was ist, wenn wir eine ereignisbasierte Architektur wollen, bei der die Ansicht auf Änderungen reagiert? Bleiben Sie dran für die nächsten Muster in den Android Architecture Blueprints, um zu sehen, wie dies implementiert werden kann. Lesen Sie bis dahin mehr über unsere Model-View-ViewModel-Musterimplementierung in der upday-App.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.