El patrón MVP (Model View Presenter) es un derivado del conocido MVC (Model View Controller), y uno de los patrones más populares para organizar la capa de presentación en las aplicaciones de Android.
Este artículo se publicó lo publiqué por primera vez en 2014 en mi blog en inglés, y ha sido el más popular desde entonces. Así que he decidido traducirlo al español para que lo tengas aquí también.
Desde entonces se han producido cambios importantes en los patrones de arquitectura, como MVVM con architecture components, pero MVP sigue siendo válido y es una opción a tener en cuenta.
¿Qué es MVP?
El patrón MVP permite separar la capa de presentación de la lógica para que todo sobre cómo funciona la interfaz de usuario sea independiente de cómo lo representamos en la pantalla. Idealmente, el patrón MVP lograría que la misma lógica pudiera tener vistas completamente diferentes e intercambiables.
Lo primero que hay que aclarar es que MVP no es una arquitectura en sí misma, solo es responsable de la capa de presentación. Varias personas me han rebatido esto, por lo que quiero explicarlo un poco más a fondo.
Puedes ver en muchos sitios que MVP se define como un patrón de arquitectura porque puede convertirse en parte de la arquitectura de su aplicación, pero no consideres que solo porque estás usando MVP, tu arquitectura está completa. MVP solo modela la capa de presentación, pero el resto de capas aún requerirá una buena arquitectura si quieres una aplicación flexible y escalable.
Un ejemplo de una arquitectura completa podría ser Clean Architecture, aunque hay muchas otras opciones.
En cualquier caso, siempre es mejor usarlo para su arquitectura que no usarlo en absoluto.
¿Por qué usar MVP?
En Android, tenemos un problema derivado del hecho de que las actividades de Android están estrechamente relacionadas con la UI y los mecanismos de acceso a datos. Podemos encontrar ejemplos extremos como CursorAdapter, que mezcla adaptadores, que son parte de la vista, con cursores, algo que debería relegarse a las profundidades de la capa de acceso a datos.
Para que una aplicación sea fácilmente extensible y fácil de mantener, necesitamos definir capas bien separadas. ¿Qué haremos mañana si, en lugar de recuperar los mismos datos de una base de datos, necesitamos hacerlo desde un servicio web? Tendríamos que rehacer toda nuestra visión.
MVP hace que las vistas sean independientes de nuestra fuente de datos. Dividimos la aplicación en al menos tres capas diferentes, lo que nos permite probarlas de forma independiente. Con MVP sacamos la mayor parte de la lógica de las actividades para poder probarla sin usar tests de instrumentación.
Cómo implementar MVP para Android
Bueno, aquí es donde todo comienza a volverse más difuso. Hay muchas variaciones de MVP y todos pueden ajustar el patrón a sus necesidades y la forma en que se sienten más cómodos. Varía dependiendo básicamente del número de responsabilidades que delegamos al presentador.
¿Es la vista responsable de habilitar o deshabilitar una barra de progreso, o debe ser realizada por el presentador? ¿Y quién decide qué acciones deben mostrarse en la ActionBar? Ahí es donde comienzan las decisiones difíciles. Mostraré cómo trabajo habitualmente, pero quiero que lo tomes con espíritu crítico, porque no hay una forma “estándar” de implementarlo.
Para representar lo que cuento aquí, he implementado un ejemplo muy simple que puedes encontrar en mi Github con una pantalla de inicio de sesión y una pantalla principal. Por razones de simplicidad, el código en el artículo está en Kotlin, pero también puede verificar el código en Java 8 en el repositorio.
El modelo
En una aplicación con una arquitectura por capas completa, este modelo solo sería la puerta de entrada a la capa de dominio o la lógica empresarial. Si estuviéramos usando la clean architecture de Uncle Bob, el modelo probablemente sería un interactor que implementa un caso de uso. Pero para el propósito de este artículo, es suficiente verlo como el proveedor de los datos que queremos mostrar en la vista.
Si echas un ojo al código, verás que he creado dos mocks de interactors con delays artificiales para simular peticiones a un servidor. Esta es la estructura de uno de estos interactors:
class LoginInteractor { ... fun login(username: String, password: String, listener: OnLoginFinishedListener) { // Mock login. I'm creating a handler to delay the answer a couple of seconds postDelayed(2000) { when { username.isEmpty() -> listener.onUsernameError() password.isEmpty() -> listener.onPasswordError() else -> listener.onSuccess() } } } }
Es una función simple que recibe el nombre de usuario y la contraseña, y realiza algunas validaciones.
The View
La vista, generalmente implementada por una Activity (puede ser un Fragment, una View… dependiendo de cómo esté estructurada la aplicación), contendrá una referencia al presentador. El presentador será idealmente provisto por un inyector de dependencias como Dagger, pero en caso de que no use algo como esto, esta vista será la responsable de crear el objeto presentador. Lo único que hará la vista es llamar a un método de presentador cada vez que haya una acción del usuario (por ejemplo, hacer clic en un botón).
Como el presentador debe ser independiente de la vista, utiliza una interfaz que debe ser implementada. Aquí está la interfaz que usa el ejemplo:
interface LoginView { fun showProgress() fun hideProgress() fun setUsernameError() fun setPasswordError() fun navigateToHome() }
Tiene algunos métodos de utilidad para mostrar y ocultar el progreso, mostrar errores, navegar a la siguiente pantalla… Como se mencionó anteriormente, hay muchas maneras de hacer esto, pero prefiero mostrar la más simple.
Entonces, la actividad puede implementar esos métodos. Aquí te muestro algunos, para que tengas una idea:
class LoginActivity : AppCompatActivity(), LoginView { ... override fun showProgress() { progress.visibility = View.VISIBLE } override fun hideProgress() { progress.visibility = View.GONE } override fun setUsernameError() { username.error = getString(R.string.username_error) } }
Pero si recuerdas, también te dije que la vista usa el presentador para notificar sobre las interacciones del usuario. Así es como se usa:
class LoginActivity : AppCompatActivity(), LoginView { private val presenter = LoginPresenter(this, LoginInteractor()) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) button.setOnClickListener { validateCredentials() } } private fun validateCredentials() { presenter.validateCredentials(username.text.toString(), password.text.toString()) } override fun onDestroy() { presenter.onDestroy() super.onDestroy() } ... }
El Presenter se define como una property para la actividad, y cuando se hace clic en el botón, llama a validateCredentials()
, que notificará al Presenter.
Lo mismo sucede con onDestroy()
. Más adelante veremos por qué se necesita notificar en ese caso.
El Presenter
El Presenter es responsable de actuar como intermediario entre la vista y el modelo. Recupera datos del modelo y los devuelve formateados a la vista.
Además, a diferencia del MVC típico, decide qué sucede cuando se interactúa con la vista. Por lo tanto, tendrá un método para cada posible acción que el usuario pueda hacer. Lo vimos en la vista, pero aquí está la implementación:
class LoginPresenter(var loginView: LoginView?, val loginInteractor: LoginInteractor) : LoginInteractor.OnLoginFinishedListener { fun validateCredentials(username: String, password: String) { loginView?.showProgress() loginInteractor.login(username, password, this) } ... }
MVP tiene algunos riesgos, y lo más importante de lo que nos solemos olvidar es que el presentador está conectado a la vista para siempre. Y la vista es una actividad, lo que significa que:
- Podemos producir un leak de la actividad si ejecutamos tareas largas en segundo plano
- Podemos intentar actualizar una activity que ya haya muerto
Para el primer punto, si puedes asegurarte de que tus tareas en segundo plano finalicen en un período de tiempo razonable, no le daría muchos vueltas. Producir un leak de una actividad durante 5-10 segundos no hará que tu aplicación sea mucho peor, y las soluciones para esto suelen ser complejas.
El segundo punto es más preocupante. Imagina que envías una solicitud a un servidor que tarda 10 segundos, pero el usuario cierra la actividad después de 5 segundos. Cuando se llama al callback y se actualiza la IU, lanzará un error porque la actividad está finalizando.
Para resolver esto, llamamos al método onDestroy()
que limpia la vista:
fun onDestroy() { loginView = null }
De esta forma evitamos llamar a una vista que esté en un estado inconsistente.
Conclusión
Separar la interfaz de la lógica en Android no es fácil, pero el patrón MVP hace que sea más sencillo evitar que nuestras actividades terminen degradando en clases muy acopladas que consisten en cientos o incluso miles de líneas. En aplicaciones grandes, es esencial organizar bien nuestro código. De lo contrario, se hace imposible mantenerlas y extenderlas.
Recuerda que tienes el repositorio, donde puedes ver el código tanto en Kotlin como en Java
De patrones de presentación, arquitecturas, testing… hablamos a fondo en mi programa Architect Coders. Así que si te interesa, puedes unirte para la próxima edición.
Hola primero muchas gracias por compartir tus conocimientos y puntos de vista envío publicaciones como está.
Entiendo que por fines explicativos los ejemplos nunca son complejos, pero por eso es que tengo dudas que nunca he logrado resolver, por ejemplo:
1)En un login sólo se validan 2 datos que son fáciles de pasar al presentador y luego al modelo, pero ¿Y si se tienen que validar los 10 botones y 5 campos que muestra la UI? ¿No debería existir una clase que encapsule los valores? Y si se crea una clase para ello, ¿Quién debería encargarse de crearla y actualizarla si la vista no supone tener mayor responsabilidad?
2) En muchos blogs he leído que el presentador no debería depender del tipo de componentes visuales que se usen y por otro lado debe manejar toda la lógica de interacción de la UI, pero ¿Cómo es eso posible?, ¿La interfaz del presentador debería tener un método para cada evento de la UI, de tal modo que la implementación de cada listener use los métodos del presentador? Porque algunos componentes usan sus propios valores o estados por ejemplo ScrollState, MotionEvent, InputFilters y todo eso depende de los componentes que se usen, lo cual me obliga a escribir la lógica en la vista en lugar de comprometer el presentador por si después cambio los componentes, pero entonces la vista ya no es sencilla y es más bien parecida a un MVC
3) ¿Por qué se refieren a reglas de negocio como los POJO’s? Para mí las reglas de negocio son validaciones de los datos o estados del modelo, eso siempre me confunde cuando se dice que en el modelo se usan las reglas de negocio porque entonces agrego lógica para validar campos en esa capa, pero resulta que son los dtos, entonces dónde se validan y convierten los datos, ¿En el presentador? ¿No quedará un monstruo muy grande si todo lo hace el presentador?
De ante mano muchas gracias!
Uff, esto es largo y depende de muchas cosas, pero haré lo que pueda por contestarlo:
1) Para eso se suele usar un modelo que representa el formulario, efectivamente. Lo ideal sería que lo actualizara el presenter, pero es un engorro. Aquí al ser tan solo un cambio de estado, se puede dejar que lo haga la vista y notifique al presenter con el modelo cambiado.
2) En eso la línea es muy fina, y cada uno toma sus propias decisiones. Yo prefiero hacer un poco más agnóstico el presenter, y dejar que la vista interprete el modelo, siempre y cuando sean interpretaciones sencillas.
3) Las conversiones de datos se hacen en la capa que corresponda en una arquitectura completa (en este ejemplo no se llega a ese punto). Si el presenter necesitara hacer alguna conversión de datos (que es posible, porque puedes estar preparando los datos para mostrarlos en la UI), se encargará un data mapper de hacerlo, no el presenter en sí. Yo personalmente cuando hablo de POJOs o de Entities, me refiero a entidades que representan un modelo. Normalmente son clases que solo tienen estado pero que no realizan ninguna operación.
El tema con las arquitecturas es que no hay reglas escritas en piedra, cada uno las estructura e interpreta como considera. Así que de cada persona que leas algo, te llevarás probablemente una idea bastante diferente.