Android Architecture Patterns Part 2:Model-View-Presenter
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.
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 Task
s 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 subscribe
Methode des Moderators in onResume
aufruft. 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.