Hoy hablarémos sobre Arquitectura android y como repensamos MVP en Android. Cuando hablamos de arquitectura de software, en Apiumhub siempre tenemos en mente los siguientes objetivos:

  • Aumentar testeabilidad, expresividad y reusabilidad.
  • Reducir mocking/stubbing, acoplamiento y costes de desarrollo.

Establecidos estos objetivos, priorizamos las distintas soluciones que pensamos siguiendo las 4 reglas del diseño simple, las cuales son, ordenadas por prioridad descendente:

  • Escribir código que pase los tests
  • Que nuestro código revele intencion y elimine duplicidad
  • Una arquitectura con el mínimo número de elementos

Teniendo siempre en mente estos dos pilares, podemos empezar a plantearnos qué arquitectura nos beneficiaría a la hora de desarrollar aplicaciones móviles.

 

Repensando MVP en Android

 

(M)VB – Model-View-Backend

Antes de entrar a valorar las arquitecturas más usadas en la industria y cuál es nuestra aproximación a las mismas, nos gustaría recalcar que uno de los recursos más necesarios a la hora de desarrollar es el pragmatismo. Esto, aplicado al mundo de la arquitectura quiere decir que hemos de ser conscientes en cada momento de qué producto estamos desarrollando y las necesidades del mismo, en la forma en que si, por ejemplo, tenemos una aplicación que consulta datos de un servidor y los muestra en pantalla, sin tener una base de datos, transformaciones complejas sobre estos datos, concatenado de llamadas, componentes reusables, etc… lo más sencillo es usar una arquitectura que utilice las menos capas y componentes posibles, que lea del servidor y pinte en pantalla; una arquitectura Model-View-Backend.

Model-View-Backend

 

MVP – Model-View-Presenter

Cuando hablamos de arquitectura mobile, el punto de partida suele ser el patrón MVP. Hace unos años y antes de la llegada del MVVM, MVP en Android fue el patrón estándar en la industria para construir la capa de presentación de nuestra aplicación.

Este patrón nació a raíz de la necesidad de solucionar el problema que constituía el tener Activities que eran God Objects, donde residía toda la lógica de la aplicación, las llamadas de red, el almacenamiento en BD/shared preferences, etc… 

No queremos profundizar mucho en los detalles de MVP en Android, ya que es bastante popular y conocida por todos, por lo que sólo comentaremos las ventajas e inconvenientes que encontramos:

 

Ventajas de MVP en Android:

  • Vista desacoplada del resto de componentes
  • Lógica de presentación testeable de forma unitaria
  • Vistas y presenters reusables

Desventajas de MVP en Android:

  • Acoplamiento bidireccional. Esto es, el presenter conoce a la vista y la vista al presenter. Además el presenter tiene dependencias con los servicios.
  • Al existir este acoplamiento, para testear el presenter tenemos que mockear los servicios y la vista.
  • Un presenter tiene dependencias con N servicios, lo que nos complica el testing del mismo ya que cada servicio que añadamos implica más mocking en los tests.
  • Existe tendencia a almacenar estados intermedios en el presenter (por ejemplo, el estado de un formulario), lo que nos lleva a dificultades a la hora de testear este presenter ya que hemos de configurar estos estados para cubrir todos los posibles casos.

 

MVPBinder

Como hemos visto en el MVP clásico, la mayor desventaja del MVP clásico es el aumento de la dificultad para testear un presenter conforme este va creciendo; por ello decidimos evolucionar el mismo hasta lograr desacoplarlo de los servicios.

MVPBinder

Para esto creamos una clase “Binder”, la cual es una subclase del presenter que nos permite enlazar eventos del servicio con una respuesta en la vista, y así no tener dependencias con los servicios en el presenter, tal que para testearlo no tenemos que mockear los mismos. Un ejemplo de una clase Binder sería así:

class Binder(view, service): Presenter(view) {
  init {
    bindSearch { query ->
      service.search(query,
        this::onGetRepositoryListNext,
        this::onGetRepositoryListError
      )
	…
  }
}

El método bindSearch está definido en el presenter y se define como:

fun bindSearch(func: (String) -> Unit)

Así mismo, los métodos onGetRepositoryListNext y onGetRepositoryListError también se definen en el presenter y son estos:

protected fun onGetRepositoryListNext(list) {
  view.hideLoading() 
  if (list.isEmpty()) {
    view.showEmptyData()
  } else {
    view.showData(list)
  }
}
protected fun onGetRepositoryListError(error) {
  view.hideLoading()
  when (error) {
    is ConnectException -> view.showNetworkError()
    else -> view.showOtherError()
  }
}

Adoptada esta arquitectura, estas son las ventajas y desventajas que nos encontramos:

Ventajas

  • Desacoplamiento entre el presenter y los servicios. Lo que nos facilita la reusabilidad del presenter.
  • No es necesario mockear/stubbear el servicio. Lo que nos facilita el testing del presenter.

Desventajas

  • Seguimos manteniendo una dependencia entre la vista y el presenter, por lo que tenemos que mockear la vista para testear el presenter (aunque únicamente hagamos verificaciones y no definamos comportamiento para la misma)
  • Si tenemos más de un servicio, nuestro Binder tendrá esos N servicios como dependencia y podrá crecer de forma indeterminada.
  • Sigue existiendo un presenter en el cual podemos almacenar estados intermedios, lo que nos complica el testing del mismo.

Apium Academy

 

MVB – Model-View-Binder

MVB - Model-View-Binder

 

Tras la adopción del presenter con el binder nos dimos cuenta que la misma inversión de dependencias que hicimos entre el presenter y los servicios, se podía hacer entre el presenter y la(s) vista(s). De esta forma tendríamos una vista totalmente desacoplada del presenter, así como un presenter desacoplado tanto de la vista como de los servicios. La única responsabilidad de este presenter (a partir de aquí pasaremos a llamarle Binder) será la de enlazar los eventos que emita la vista con llamadas al servicio, y los eventos que emita el servicio con los métodos de la vista. Un Binder de estas características sería tal que:

class Binder(view, service) {
  init {
    view.bindSearch(service::search)
    service.bindData(view::showData)
    service.bindEmptyData(view::showEmpty)
    service.bindNetworkError(view::showNetworkError)
  }
}


De esta forma, podríamos tener N binders donde cada uno enlaza un par vista-servicio, tal que podríamos tener por ejemplo un Binder que nos enlazara los eventos de la vista que queramos trackear en un servicio de analítica con un servicio que haga lo propio, y tener otro Binder para obtener datos de esa misma vista. De esta forma el Binder para trackear eventos (junto con las interfaces de la vista y el servicio) sería reutilizable en todas las vistas que queramos y eliminaremos mucha duplicidad del código.

Los eventos que emite una vista/servicio así como la información que recibe se definiría en una interfaz tal que: 

interface RepositoryListView {
 fun bindSearch(func: (String) -> Unit)

 fun showLoading()
 fun hideLoading()
 ...
}
interface RepositoryListService {
 fun search(query: String)

 fun bindData(func: (List) -> Unit)
 fun bindEmptyData(func: () -> Unit)
 ...
}

Esta arquitectura nos facilita mucho el testing de nuestra capa de presentación, ya que elimina totalmente las dependencias con vistas y servicios (la única dependencia la tiene el Binder, y testearlo sería redundante, ya que es poco más que un fichero de configuración) y por tanto elimina la necesidad de mocking de los mismos.

No sólo elimina el mocking, si no que la forma de testear tanto nuestros servicios como nuestras vistas es comprobar que, para un evento de entrada X, se emite un valor cierto valor a través del evento de salida Y. De esta forma tenemos una forma unificada de testear gran parte de nuestra aplicación, no importa cómo de complejos sean los servicios o las vistas.

Además, eliminamos la tentación de tener estados intermedios en capas intermedias (como un Presenter o un ViewModel), ya que únicamente podremos bien almacenarlos en la vista (información que concierne exclusivamente a la vista) o bien tendremos que llevarla a las capas más externas de nuestra aplicación (por ejemplo una caché en memoria) y acceder a ellas a través de un repositorio.

Gracias a esta arquitectura resolvemos los problemas que nos presentaban las anteriores, teniendo como única desventaja que la complejidad de los servicios ha aumentado respecto a las anteriores; desventaja que mitigamos testeando intensivamente nuestros servicios.

 

Next steps:

  • Estamos trabajando en un DSL que defina eventos de entrada y salida y que, a través de adaptadores, podamos trabajar con RxJava, Corrutinas/canales, etc…
  • Delegados que nos permitan definir estos eventos y sus puntos de entrada y salida
  • (Quizás)Un sistema de anotaciones que nos permita bindear estos eventos sin la necesidad de tener que escribir nosotros el código correspondiente

Side note: Many of the code used to write this article can be found on this repository

Suscríbete a nuestro newsletter para estar al día de MVP en Android y desarrollo movil en general!