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. Per aiutare con questo, Google offre progetti di architettura Android, in cui Erik Hellman e io abbiamo lavorato insieme sul campione MVP& RxJava. Diamo un’occhiata a come l’abbiamo applicato e ai pro e ai contro di questo approccio.
Ecco i ruoli di ogni componente:
- Modello — il livello dati. Responsabile della gestione della logica di business e della comunicazione con i livelli di rete e database.
- Vista-il livello dell’interfaccia utente. Visualizza i dati e notifica al Presentatore le azioni dell’utente.
- Presenter-recupera i dati dal modello, applica la logica dell’interfaccia utente e gestisce lo stato della vista, decide cosa visualizzare e reagisce alle notifiche di input dell’utente dalla vista.
Poiché la vista e il Presentatore lavorano a stretto contatto, devono avere un riferimento l’uno all’altro. Per rendere l’unità Presentatore testabile con JUnit, la vista viene astratta e viene utilizzata un’interfaccia. La relazione tra il Presentatore e la sua vista corrispondente è definita in una classe di interfaccia Contract
, rendendo il codice più leggibile e la connessione tra i due più facile da capire.
Il Model-View-Presenter & RxJava in Architettura Android i Modelli
Il programma di esempio è un ”Fare” applicazione. Consente all’utente di creare, leggere, aggiornare ed eliminare le attività “Da fare”, nonché applicare filtri all’elenco visualizzato delle attività. RxJava viene utilizzato per spostarsi dal thread principale ed essere in grado di gestire operazioni asincrone.
Modello
Il modello funziona con le origini dati remote e locali per ottenere e salvare i dati. Questo è dove viene gestita la logica di business. Ad esempio, quando si richiede l’elenco di Task
s, il modello tenta di recuperarli dall’origine dati locale. Se è vuoto, interrogherà la rete, salverà la risposta nell’origine dati locale e restituirà l’elenco.
Il recupero delle attività viene eseguito con l’aiuto di RxJava:
public Observable<List<Task>> getTasks(){
...
}
Il modello riceve come parametri nelle interfacce del costruttore delle origini dati locali e remote, rendendo il Modello completamente indipendente da qualsiasi classe Android e quindi facile da unit test con JUnit. Ad esempio, per verificare che getTasks
richieda dati dalla sorgente locale, abbiamo implementato il seguente 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);
}
View
La Vista funziona con il Presentatore per visualizzare i dati e notifica al Presentatore le azioni dell’utente. Nelle attività MVP, i frammenti e le viste personalizzate di Android possono essere Viste. La nostra scelta era di usare Frammenti.
Tutte le viste implementano la stessa interfaccia BaseView che consente di impostare un Presentatore.
public interface BaseView<T> {
void setPresenter(T presenter);
}
La vista notifica al Presentatore che è pronto per essere aggiornato chiamando il metodosubscribe
del Presentatore inonResume
. La vista chiamapresenter.unsubscribe()
inonPause
per dire al Presentatore che non è più interessato all’aggiornamento. Se l’implementazione della Vista è una vista personalizzata Android, i metodi subscribe
e unsubscribe
devono essere chiamati su onAttachedToWindow
e onDetachedFromWindow
. Le azioni dell’utente, come i clic sui pulsanti, attiveranno i metodi corrispondenti nel Presentatore, essendo questo quello che decide cosa dovrebbe accadere dopo.
Le viste vengono testate con Espresso. La schermata statistiche, ad esempio, deve visualizzare il numero di attività attive e completate. Il test che verifica che ciò sia fatto correttamente prima inserisce alcune attività nelTaskRepository
; quindi avvia ilStatisticsActivity
e controlla il contenuto delle viste:
@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
Il Presentatore e la sua vista corrispondente vengono creati dall’Attività. I riferimenti alla vista e alTaskRepository
– il modello – sono dati al costruttore del Presentatore. Nell’implementazione del costruttore, il Presentatore chiamerà il metodosetPresenter
della Vista. Questo può essere semplificato quando si utilizza un framework di iniezione delle dipendenze che consente l’iniezione dei Presentatori nelle viste corrispondenti, riducendo l’accoppiamento delle classi. L’implementazione del ToDo-MVP con Dagger è coperta in un altro campione.
Tutti i Presentatori implementano la stessa interfaccia BasePresenter.
public interface BasePresenter {
void subscribe();
void unsubscribe();
}
Quando viene chiamato il metodo subscribe
, il Presentatore inizia a richiedere i dati dal modello, quindi applica la logica dell’interfaccia utente ai dati ricevuti e lo imposta alla Vista. Ad esempio, inStatisticsPresenter
, tutte le attività sono richieste daTaskRepository
– quindi le attività recuperate vengono utilizzate per calcolare il numero di attività attive e completate. Questi numeri verranno utilizzati come parametri per il metodoshowStatistics(int numberOfActiveTasks, int numberOfCompletedTasks)
della Vista.
Un test unitario per verificare che il metodo showStatistics
sia chiamato con i valori corretti è facile da implementare. Stiamo prendendo in giro il TaskRepository
e ilStatisticsContract.View
e dare gli oggetti derisi come parametri al costruttore di unStatisticsPresenter
oggetto. L’implementazione del test è:
@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);
}
Il ruolo del metodo unsubscribe
è quello di cancellare tutte le sottoscrizioni del Presentatore, evitando così perdite di memoria.
Oltre a subscribe
e unsubscribe
, ogni Presentatore espone altri metodi, corrispondenti alle azioni dell’utente nella Vista. Ad esempio, AddEditTaskPresenter
, aggiunge metodi come createTask
, che verrebbe chiamato quando l’utente preme il pulsante che crea una nuova attività. Ciò garantisce che tutte le azioni dell’utente – e di conseguenza tutta la logica dell’interfaccia utente-passino attraverso il Presentatore e quindi possano essere testate in unità.
Svantaggi del modello Model-View-Presenter
Il modello Model-View-Presenter porta con sé un’ottima separazione delle preoccupazioni. Mentre questo è sicuramente un professionista, quando si sviluppa una piccola app o un prototipo, questo può sembrare un sovraccarico. Per ridurre il numero di interfacce utilizzate, alcuni sviluppatori rimuovono la classe di interfacciaContract
e l’interfaccia per il Presentatore.
Una delle insidie di MVP appare quando si sposta la logica dell’interfaccia utente al Presentatore: questa diventa ora una classe onnisciente, con migliaia di righe di codice. Per risolvere questo problema, dividi ancora di più il codice e ricorda di creare classi che hanno una sola responsabilità e sono testabili in unità.
Conclusione
Il modello Model-View-Controller presenta due svantaggi principali: in primo luogo, la Vista ha un riferimento sia al Controller che al Modello; e in secondo luogo, non limita la gestione della logica dell’interfaccia utente a una singola classe, questa responsabilità è condivisa tra il Controller e la Vista o il modello. Il modello Model-View-Presenter risolve entrambi questi problemi interrompendo la connessione che la Vista ha con il modello e creando una sola classe che gestisce tutto ciò che riguarda la presentazione della Vista — il Presentatore: una singola classe che è facile da testare.
Cosa succede se vogliamo un’architettura basata su eventi, in cui la vista reagisce alle modifiche? Restate sintonizzati per i prossimi modelli campionati nei progetti di architettura Android per vedere come questo può essere implementato. Fino ad allora, leggi la nostra implementazione del modello Model-View-ViewModel nell’app upday.