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. Aby pomóc w tym, Google oferuje plany architektury Androida, w których Erik Hellman i ja pracowaliśmy razem nad MVP & próbka RxJava. Przyjrzyjmy się temu, jak go zastosowaliśmy oraz zaletom i wadom tego podejścia.
oto role każdego komponentu:
- Model — Warstwa danych. Odpowiedzialny za obsługę logiki biznesowej oraz komunikację z warstwami sieciowymi i bazodanowymi.
- View-warstwa interfejsu użytkownika. Wyświetla dane i powiadamia prezentera o działaniach użytkownika.
- prezenter-pobiera dane z modelu, stosuje logikę interfejsu użytkownika i zarządza stanem widoku, decyduje, co ma być wyświetlane i reaguje na powiadomienia wejściowe użytkownika z widoku.
ponieważ widok i prezenter ściśle ze sobą współpracują, muszą mieć do siebie odniesienie. Aby moduł prezentera mógł być testowany za pomocą JUnit, widok jest abstrakcyjny i używany jest dla niego interfejs. Relacja między prezenterem a odpowiadającym mu widokiem jest zdefiniowana w klasie interfejsuContract
, dzięki czemu kod jest bardziej czytelny i połączenie między nimi jest łatwiejsze do zrozumienia.
wzór modelu view-Presenter&RxJava w architekturze Android
przykładowy schemat jest aplikacją „do zrobienia”. Pozwala użytkownikowi tworzyć, czytać, aktualizować i usuwać zadania „do zrobienia”, a także stosować filtry do wyświetlonej listy zadań. RxJava służy do odsunięcia głównego wątku i obsługi operacji asynchronicznych.
Model
model współpracuje ze zdalnymi i lokalnymi źródłami danych, aby uzyskać i zapisać dane. To tutaj odbywa się logika biznesowa. Na przykład, podczas żądania listy Task
s, Model będzie próbował pobrać je z lokalnego źródła danych. Jeśli jest pusta, zapyta sieć, zapisze odpowiedź w lokalnym źródle danych, a następnie zwróci listę.
pobieranie zadań odbywa się za pomocą RxJava:
public Observable<List<Task>> getTasks(){
...
}
Model odbiera jako parametry w interfejsach konstruktora lokalnych i zdalnych źródeł danych, czyniąc Model całkowicie niezależnym od jakichkolwiek klas Androida, a tym samym łatwym do jednostkowego testowania z Junitem. Na przykład, aby sprawdzić, czy getTasks
żąda danych ze źródła lokalnego, zaimplementowaliśmy następujący test:
@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);
}
Widok
Widok współpracuje z prezenterem, aby wyświetlić dane i powiadamia prezentera o działaniach użytkownika. W działaniach MVP fragmenty i niestandardowe widoki Androida mogą być widokami. Wybraliśmy użycie fragmentów.
wszystkie widoki implementują ten sam interfejs BaseView, który umożliwia ustawienie prezentera.
public interface BaseView<T> {
void setPresenter(T presenter);
}
Widok powiadamia prezentera, że jest gotowy do aktualizacji, wywołującsubscribe
metodę prezentera wonResume
. Widok wywołuje presenter.unsubscribe()
w onPause
, aby poinformować prezentera, że nie jest już zainteresowany aktualizacją. Jeżeli implementacją widoku jest własny widok Androida, wtedy subscribe
I unsubscribe
należy wywołać metody onAttachedToWindow
I onDetachedFromWindow
. Działania użytkownika, takie jak kliknięcia przycisków, wywołają odpowiednie metody w prezenterze, które decydują o tym, co powinno się stać dalej.
widoki są testowane z Espresso. Na przykład ekran statystyk musi wyświetlać liczbę aktywnych i ukończonych zadań. Test sprawdzający, czy zostało to wykonane poprawnie, najpierw umieszcza niektóre zadania w TaskRepository
; następnie uruchamia StatisticsActivity
I sprawdza zawartość widoków:
@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()));
}
prezenter
prezenter i odpowiadający mu Widok są tworzone przez działanie. Odniesienia do widoku i doTaskRepository
– Model – są podane konstruktorowi prezentera. W implementacji konstruktora, prezenter wywołasetPresenter
metodę widoku. Można to uprościć przy użyciu struktury wstrzykiwania zależności, która umożliwia wstrzykiwanie prezenterów w odpowiednich widokach, zmniejszając łączenie klas. Implementacja ToDo-MVP ze sztyletem jest objęta inną próbką.
wszyscy Prezenterzy implementują ten sam interfejs BasePresenter.
public interface BasePresenter {
void subscribe();
void unsubscribe();
}
gdy zostanie wywołana metodasubscribe
, prezenter zaczyna żądać danych z modelu, następnie stosuje logikę interfejsu użytkownika do odebranych danych i ustawia je na widok. Na przykład wStatisticsPresenter
wszystkie zadania są wymagane zTaskRepository
– wtedy pobrane zadania są używane do obliczenia liczby aktywnych i ukończonych zadań. Te liczby będą użyte jako parametry dlashowStatistics(int numberOfActiveTasks, int numberOfCompletedTasks)
metody widoku.
test jednostkowy, aby sprawdzić, czy rzeczywiście metoda showStatistics
jest wywoływana z poprawnymi wartościami, jest łatwa do zaimplementowania. Wyśmiewamy TaskRepository
I StatisticsContract.View
I dajemy wyśmiane obiekty jako parametry konstruktorowi StatisticsPresenter
obiektu. Implementacja testowa to:
@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);
}
rolą metodyunsubscribe
jest wyczyszczenie wszystkich subskrypcji prezentera, unikając w ten sposób wycieków pamięci.
opróczsubscribe
Iunsubscribe
, każdy prezenter udostępnia inne metody, odpowiadające działaniom użytkownika w widoku. Na przykład AddEditTaskPresenter
, dodaje metody takie jak createTask
, które zostaną wywołane, gdy użytkownik naciśnie przycisk, który utworzy nowe zadanie. Gwarantuje to, że wszystkie działania użytkownika – a co za tym idzie cała logika interfejsu – przechodzą przez prezentera, a tym samym mogą być testowane jednostkowo.
wady wzorca Model-View-Presenter
wzorzec Model-View-Presenter niesie ze sobą bardzo dobrą separację problemów. Chociaż jest to na pewno profesjonalista, podczas opracowywania małej aplikacji lub prototypu może to wydawać się narzutem. Aby zmniejszyć liczbę używanych interfejsów, niektórzy programiści usuwają klasę interfejsuContract
oraz interfejs dla prezentera.
jedna z pułapek MVP pojawia się podczas przenoszenia logiki interfejsu użytkownika do prezentera: staje się ona teraz klasą wszechwiedzącą, z tysiącami linii kodu. Aby to rozwiązać, podziel Kod jeszcze bardziej i pamiętaj, aby tworzyć klasy, które mają tylko jedną odpowiedzialność i są testowalne jednostkowo.
wniosek
wzór Model-widok-kontroler ma dwie główne wady: po pierwsze, Widok ma odniesienie zarówno do kontrolera, jak i modelu; po drugie, nie ogranicza obsługi logiki interfejsu użytkownika do jednej klasy, odpowiedzialność ta jest dzielona między kontroler a Widok lub Model. Wzorzec Model-Widok-prezenter rozwiązuje oba te problemy, zrywając połączenie widoku z modelem i tworząc tylko jedną klasę, która obsługuje wszystko, co związane jest z prezentacją widoku-prezenter: jedną klasę, którą można łatwo przetestować jednostkowo.
Co jeśli chcemy architekturę opartą na zdarzeniach, w której Widok reaguje na zmiany? Wypatrujcie kolejnych wzorców próbkowanych w schematach architektury Androida, aby zobaczyć, jak można to wdrożyć. Do tego czasu przeczytaj o naszej implementacji wzorca Model-View-ViewModel w aplikacji upday.