Introducción a los componentes de arquitectura Android: go Clean!

Durante los últimos años, uno de los temas con más hype en Android ha sido la arquitectura Clean. En foros, blogs, conferencias y demás, las diferentes arquitecturas se han presentado y discutido hasta la saciedad.

En el último Google I/O, además del estupendo anuncio de Kotlin como lenguaje oficialmente soportado para Android, nos sorprendieron con una serie de librerías (en desarrollo todavía pero ya disponibles en modo Alpha) que pretenden unificar las buenas prácticas mostrando una arquitectura ejemplo a seguir en Android.

No es obligatorio seguir estas prácticas, Google mismo dice que si ya estamos usando otra arquitectura Clean, o RxJava, o cualquier otra tecnología alternativa, sigamos con ella.

En cualquier caso, es conveniente ver qué nos aporta la propuesta de Google, compararlo con nuestra arquitectura actual y tomar lo mejor de ambas. Adentrémonos en el universo Clean y veamos cómo nos puede ayudar en el desarrollo de nuestras aplicaciones.

La programación orientada a objetos debe cumplir cinco principios básicos que se conocen con el acrónimo de SOLID. Los repasamos muy rápidamente:

  • Single Responsibility: Cada unidad de código debe tener una única responsabilidad.
  • Open/Closed: Podemos extender las funcionalidades de un objeto, no modificarlas.
  • Liskov: Un objeto puede ser siempre sustituido por otro objeto subtipo
  • Interface segregation: Mejor varios interfaces específicos que uno general.
  • Dependency inversion: Un objeto debe depender de abstracciones.

Las arquitecturas CLEAN intentan cumplir con estos principios separando claramente las responsabilidades de cada capa (S): View, Presenter, Model, Data Access… a la vez que se tiende a limitar la Herencia en favor de la Composición (O,L,I). Mediante la inyección de dependencias conseguimos aislar las clases de las implementaciones concretas de sus colaboradores (D).

Con esto conseguimos un código fácil de testear ya que podemos probar cada capa o clase por separado. También será fácil de modificar: cambios en la fuente de datos que no afectarán a nuestro modelo de negocio, o un rediseño completo de la UI no supondrá modificaciones en las capas exteriores.

El diseño original de Android no pretendía ser Clean ni cumplir SOLID, se orientaba más a facilitar el desarrollo de pequeñas aplicaciones para teléfonos de escasa potencia por desarrolladores no demasiado experimentados.

En Android las aplicaciones tienen muchos puntos de entrada: Activities, Services, Content Providers y Broadcast Receivers. Estas clases son componentes con muchas responsabilidades que nos vienen dadas por el sistema, no tenemos acceso al constructor y tenemos que heredar. Estos componentes están unidos mediante Intents, que los ejecutan y permiten que colaboren con otros componentes del sistema y de otras aplicaciones.

Un error frecuente es añadir lógica o datos a estos componentes, rompiendo aún más el principio de responsabilidad única y causando problemas ya que estos componentes están bajo el control del sistema, no de nuestra aplicación, y pueden ser destruidos y/o recreados en cualquier momento causando comportamientos indeseables.

El modelo de datos se muestra mediante la UI de la aplicación, que a su vez lo modifica. Sin embargo, la UI debe ser totalmente independiente de este. El modelo debe estar definido fuera de la UI.

Esto permitirá que los ciclos de vida de los componentes gráficos, como puede ser una rotación del dispositivo que destruye y recrea la Activity, no supongan tener que solicitar los datos de nuevo o descartar los que estaban en camino.

El hecho de introducir lógica o datos en las Activities, instanciar los colaboradores dentro de la clase o mezclar la API o el modelo de datos con la lógica de la aplicación dificulta y a menudo impide testear nuestras clases.

¿Qué tiene la arquitectura Clean de novedoso?

Clean nos permite una separación de responsabilidades entre las diversas capas de la aplicación para cumplir con SOLID, planteado inicialmente por Uncle Bob en su blog, ha sido implementada de muy diversas formas: MVC, MVP, MVVM, Hexagonal

Aun cuando cada una de estas implementaciones estuviera bien resuelta, su diversidad ha creado otro problema: cada desarrollador creaba su versión, no siempre correctamente implementada y que debía ser enseñada al resto del equipo, que podía estar o no de acuerdo.

Unificar la manera de hacer las cosas solo puede redundar en ventajas para el ecosistema Android y para los desarrolladores.

4 aspectos claves

Para solucionar estos problemas, Google propone una arquitectura CLEAN basada en cuatro aspectos:

  1. Gestión del ciclo de vida: mediante la implementación de un Interface por parte de Activities y Fragments (LifecycleOwner) estas clases pueden informar de su ciclo de vida (Lifecycle) a otras clases que pueden actuar en consecuencia mediante métodos anotados. Actualmente estas clases con soporte de Lifecycle se proporcionan como ejemplos (LifecycleActivity y LifecycleFragment) en la librería de arquitectura, pero en la release final la librería de compatibilidad soportará directamente Lifecycle.
  2. ViewModel: las implementaciones de este interface contienen los datos necesarios para mostrar la view. Estos ViewModel se instancian a través de la clase ViewModelProvider, que se asegura de que sobreviven a cambios de configuración de la Activity. La responsabilidad de conseguir y almacenar los datos recae en el ViewModel, no en la Activity.
  3. LiveData: es una clase de almacenamiento de datos que permite que estos sean observados. Se diferencia de un Observable en que es consciente del ciclo de vida de la Activity evitando, por ejemplo, mandarle datos cuando esta no está en primer plano.
  4. Room: las aplicaciones móviles deben soportar redes de baja calidad e incluso permitir su uso mientras estamos desconectados de la red. Para ello se utiliza el almacenamiento local, pero desafortunadamente la implementación de la base de datos SQLite que Android nos proporciona exige escribir mucho código boilerplate y es muy dada a errores de sentencias SQL en tiempo de ejecución.

¿Cómo queda la arquitectura?

Mediante el uso de las herramientas que nos proporciona la librería de arquitectura podemos construir diferentes implementaciones, garantizando que todas ellas proporcionan una separación de los componentes de la aplicación y respetan su ciclo de vida.

La propuesta de Google es una arquitectura Model-View-ViewModel básica, pero podríamos añadir un presenter, Interactor, use cases…

En el OnCreate la Activity solicita una instancia del ViewModel al ViewModelprovider:

public void onCreate(Bundle savedInstanceState) {
       MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
       model.getMessages().observe(this, messages -> {
           // update UI
       });
   }

Pasando this al ViewModelprovider este sabe para qué vista es el ViewModel y devolverá siempre la misma instancia, por lo que los datos podrán sobrevivir a cambios de configuración en la Activity.

Por supuesto, podemos destruir esa instancia si lo deseamos y se destruye automáticamente cuando la Activity termina normalmente.

Cuando se llama a model.getMessages() lo que se devuelve es un ViewData que podemos observar y pintar la UI cada vez que haya un cambio en los datos. Si los datos no están disponibles, el ViewModel se encarga de cargarlos desde el repositorio:

public class MyViewModel extends ViewModel {
   private MutableLiveData<List<Message>> messages;
   public LiveData<List<Message>> getMessages() {
       if (messages == null) {
           messages = new MutableLiveData<List<Messages>>();
           // launch async operation to fetch messages        
       }
       return messages;
   }
}

Las peticiones se hacen a través de un repositorio que nos devolverá primero una copia desde la base de datos Room, y si es necesario solicitará los datos al backend.

El POJO Message estará anotado como @Entity, lo que le convierte en una Row de la base de datos:

@Entity
class Message {
   @PrimaryKey
   public int id;
   public String messageText;
}

Y el DAO de acceso a los mensajes se anotará con @Dao. Las sentencias SQL de los DAO se comprobarán en tiempo de compilación evitando errores difíciles de detectar:

@Dao
public interface MyDao {
   @Query("SELECT * FROM messages")
   public Message[] loadAllMessages();
}

Un aspecto importante en el que insiste Google es en que respetemos el concepto de Single source of truth u origen único de la información. El provider siempre devolverá la información de la base de datos.

Dependiendo de los criterios programados (caducidad, datos crecientes, etc.) si es necesario solicitará más datos al backend. Cuando estos lleguen no se pasarán a la Activity, sino que se introducirán en la base de datos y será esta mediante el LiveData observado, la que los pase a la vista.

Como excepción, si esta no estuviera en primer plano, LiveData esperaría a que lo estuviera para pasarle los datos, o si hubiera sido destruida, LiveData borraría ese observer de su lista.

El motivo de proceder de esta manera es que llamadas distintas al API pueden devolver respuestas que contengan el mismo dato, por ejemplo el nick del remitente de los mensajes, que puede cambiar entre llamadas. Si mostráramos ese dato, directamente podríamos tener inconsistencias entre pantallas.

Otros componentes

Google también aprovecha la documentación de estas librerías para animarnos a utilizar otras buenas prácticas, proponiendo que utilicemos Retrofit para el acceso a API REST, cuando hasta ahora solo hablaban de Volley. Retrofit, aún no siendo de Google, se ha convertido en el estándar de facto para el acceso al backend desde Android.

Con respecto al manejo de dependencias, por primera vez Google nos hace recomendaciones: Para usuarios nóveles propone usar Service Locator, pero para desarrolladores más avanzados recomienda encarecidamente el uso de inyección de dependencias mediante Dagger 2, una librería inicialmente desarrollada por Square y que actualmente mantiene la misma Google.

Cómo testear con arquitectura Clean

La separación de los componentes de la app nos permite testear de manera sencilla cada uno de ellos.

Test unitarios

  1. ViewModel se puede testear mockeando el Repository.
  2. Repository se puede testear mockeando el WebService de Retrofit y el DAO de Room.
  3. El DAO también se puede probar usando Junit, ya que Room permite que se le pase una implementación de SupportSQLiteOpenHelper.
  4. El Webservice se puede probar usando MockWebServer para simular las respuestas de backend.

Espresso

La UI y la interacción de usuario la probaremos mediante Espresso. Como la UI solo habla con el ViewMode, será suficiente con mockear este.

¡Estad atentos!

Aquí solo hemos dado un repaso muy por encima de lo que Google ha hecho, os recomiendo leeros la información y realizar los codelabs incluidos en la documentación de Google.

A los que ya utilizáis arquitecturas CLEAN seguro que os proporciona ideas nuevas e incluso puede que cambiéis algunas de vuestras prácticas por las de Google.

Para los que aún no lo hacéis, sin duda es el momento: no solo ahorraréis tiempo y escribiréis mejor código y no dudéis en que los que vengan detrás a leer vuestro legacy code os lo agradecerán.

Mi opinión personal es que está muy bien trabajada, y dado que aún está en fase alpha, Google está recibiendo mucho feedback de los developers, así que podemos esperar que mejore en próximas releases. Unificar la manera de hacer las cosas solo puede redundar en ventajas para el ecosistema Android y para los desarrolladores.

Podéis también mirar una implementación ejemplo de esta arquitectura que hemos hecho en Paradigma. Se trata de un lector RSS básico escrito en Kotlin usando las librerías aquí comentadas.

Miguel Sesma es principalmente desarrollador y formador Android desde 2010, apasionado de la calidad del software y comprometido con las arquitecturas clean y la introducción de buenas prácticas en el desarrollo de movilidad. Sus intereses van desde los lenguajes modernos como Kotlin y Swift aplicados al desarrollo móvil, programación funcional, reactiva, hasta investigación de nuevas tecnologías como los ordenadores cuánticos. Anteriormente ha trabajado Network and Database administrator, desarrollador C++ y desarrollo de IoT.

Ver toda la actividad de Miguel Sesma

Escribe un comentario