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. Para ayudar con esto, Google ofrece Planos de Arquitectura de Android, donde Erik Hellman y yo trabajamos juntos en el ejemplo MVP & RxJava. Echemos un vistazo a cómo lo aplicamos y a los pros y los contras de este enfoque.
Aquí están los roles de cada componente:
- Model-la capa de datos. Responsable de manejar la lógica de negocio y la comunicación con las capas de red y base de datos.Vista
- – la capa de interfaz de usuario. Muestra los datos y notifica al presentador sobre las acciones del usuario.
- Presentador: recupera los datos del Modelo, aplica la lógica de la interfaz de usuario y administra el estado de la Vista, decide qué mostrar y reacciona a las notificaciones de entrada del usuario desde la Vista.
Dado que la Vista y el Presentador trabajan en estrecha colaboración, deben tener una referencia el uno al otro. Para que la unidad del Presentador se pueda probar con JUnit, la vista se abstrae y se utiliza una interfaz para ella. La relación entre el Presentador y su Vista correspondiente se define en una clase de interfaz Contract
, lo que hace que el código sea más legible y la conexión entre los dos sea más fácil de entender.
El Modelo-Vista-Presentador Patrón & RxJava en Android Planos de Arquitectura
El modelo de la muestra es un «Hacer» de la aplicación. Permite al usuario crear, leer, actualizar y eliminar tareas pendientes, así como aplicar filtros a la lista de tareas que se muestra. RxJava se utiliza para moverse del hilo principal y ser capaz de manejar operaciones asíncronas.
Modelo
El modelo funciona con las fuentes de datos remotas y locales para obtener y guardar los datos. Aquí es donde se maneja la lógica de negocio. Por ejemplo, al solicitar la lista de Task
s, el Modelo intentaría recuperarlos de la fuente de datos local. Si está vacía, consultará a la red, guardará la respuesta en la fuente de datos local y luego devolverá la lista.
La recuperación de tareas se realiza con la ayuda de RxJava:
public Observable<List<Task>> getTasks(){
...
}
El Modelo recibe como parámetros en las interfaces de constructor de las fuentes de datos locales y remotas, haciendo que el Modelo sea completamente independiente de cualquier clase de Android y, por lo tanto, fácil de probar por unidad con JUnit. Por ejemplo, para probar que getTasks
solicita datos del origen local, implementamos la siguiente prueba:
@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 funciona con el Presentador para mostrar los datos y notifica al Presentador sobre las acciones del usuario. En Actividades MVP, Fragmentos y vistas personalizadas de Android pueden ser Vistas. Nuestra elección fue usar Fragmentos.
Todas las vistas implementan la misma interfaz de vista base que permite configurar un presentador.
public interface BaseView<T> {
void setPresenter(T presenter);
}
La Vista notifica al Presentador que está lista para ser actualizada llamando al método subscribe
del Presentador en onResume
. La vista llama a presenter.unsubscribe()
en onPause
para decirle al Presentador que ya no está interesado en ser actualizado. Si la implementación de la Vista es una vista personalizada de Android, entonces el subscribe
y unsubscribe
métodos han de ser llamados onAttachedToWindow
y onDetachedFromWindow
. Las acciones de los usuarios, como los clics en los botones, activarán los métodos correspondientes en el Presentador, que es el que decide lo que debe suceder a continuación.
Las vistas se prueban con Espresso. La pantalla de estadísticas, por ejemplo, debe mostrar el número de tareas activas y completadas. La prueba que comprueba que esto se hace correctamente primero coloca algunas tareas en TaskRepository
; luego inicia StatisticsActivity
y comprueba el contenido de las vistas:
@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
El Presentador y su Vista correspondiente son creados por la Actividad. Las referencias a la Vista y al TaskRepository
– el Modelo-se dan al constructor del Presentador. En la implementación del constructor, el Presentador llamará al método setPresenter
de la vista. Esto se puede simplificar cuando se utiliza un marco de inyección de dependencias que permite la inyección de los Presentadores en las vistas correspondientes, reduciendo el acoplamiento de las clases. La implementación del TODO-MVP con Daga está cubierta en otra muestra.
Todos los presentadores implementan la misma interfaz de presentación base.
public interface BasePresenter {
void subscribe();
void unsubscribe();
}
Cuando se llama al método subscribe
, el Presentador comienza a solicitar los datos del Modelo, luego aplica la lógica de la interfaz de usuario a los datos recibidos y los establece en la Vista. Por ejemplo, en StatisticsPresenter
, todas las tareas se solicitan desde TaskRepository
; luego, las tareas recuperadas se utilizan para calcular el número de tareas activas y completadas. Estos números se utilizarán como parámetros para el método showStatistics(int numberOfActiveTasks, int numberOfCompletedTasks)
de la vista.
Una prueba unitaria para comprobar que el método showStatistics
se llama con los valores correctos es fácil de implementar. Estamos burlando el TaskRepository
y el StatisticsContract.View
y damos los objetos burlados como parámetros al constructor de un objeto StatisticsPresenter
. La implementación de prueba es:
@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);
}
El papel del método unsubscribe
es borrar todas las suscripciones del Presentador, evitando así fugas de memoria.
Además de subscribe
y unsubscribe
, cada Presentador expone otros métodos, correspondientes a las acciones del usuario en la Vista. Por ejemplo, el AddEditTaskPresenter
, agrega métodos como createTask
, que se llamarían cuando el usuario presionara el botón que crea una nueva tarea. Esto garantiza que todas las acciones del usuario, y en consecuencia toda la lógica de la interfaz de usuario, pasen por el Presentador y, por lo tanto, se puedan probar por unidad.
Desventajas del patrón Modelo-Vista-Presentador
El patrón Modelo-Vista-Presentador trae consigo una muy buena separación de preocupaciones. Si bien esto es seguro que es un profesional, al desarrollar una aplicación pequeña o un prototipo, esto puede parecer una sobrecarga. Para reducir el número de interfaces utilizadas, algunos desarrolladores eliminan la clase de interfaz Contract
y la interfaz para el Presentador.
Una de las dificultades de MVP aparece al mover la lógica de la interfaz de usuario al Presentador: ahora se convierte en una clase que todo lo sabe, con miles de líneas de código. Para resolver esto, divida el código aún más y recuerde crear clases que tengan una sola responsabilidad y que se puedan probar por unidad.
Conclusión
El patrón Modelo-Vista-Controlador tiene dos desventajas principales: en primer lugar, la Vista tiene una referencia tanto al Controlador como al Modelo; y en segundo lugar, no limita el manejo de la lógica de la interfaz de usuario a una sola clase, ya que esta responsabilidad se comparte entre el Controlador y la Vista o el Modelo. El patrón Modelo-Vista-Presentador resuelve ambos problemas al romper la conexión que la Vista tiene con el Modelo y crear una sola clase que maneja todo lo relacionado con la presentación de la Vista: el Presentador: una sola clase que es fácil de probar por unidad.
¿Y si queremos una arquitectura basada en eventos, donde la Vista reaccione a los cambios? Manténgase atento a los siguientes patrones muestreados en los Planos de Arquitectura de Android para ver cómo se puede implementar esto. Hasta entonces, lea acerca de nuestra implementación de patrón Modelo-Vista-Modelo de vista en la aplicación upday.