Arquitectura de Componentes en Android

Carlos Leonardo Camilo Vargas Huamán
OrbisMobile
Published in
11 min readMay 22, 2018
The Final Android Architecture Components

INTRODUCCIÓN

Si revisamos un poco los Fundamentos de una Aplicación en Android, recordaremos que una aplicación móvil está compuesta de componentes principales tales como: actividades, fragmentos, servicios, proveedores de contenido(content providers) y receptores de emisión(broadcast receivers). Justamente por esta razón la estructura de una aplicación móvil es mucho más compleja que la de una aplicación web que en su mayoría solo tiene un punto de entrada desde el escritorio.

Entonces una aplicación Android escrita apropiadamente necesita ser muy flexible ya que el usuario constantemente crea su propio flujo a través de diferentes aplicaciones, tareas y cambia sus flujos de navegación. Por ejemplo, compartir una foto desde una aplicación conlleva a muchos escenarios; la aplicación emite un intent de cámara, el sistema operativo abre una aplicación de cámara y al mismo tiempo esta nueva aplicación puede abrir a su vez otras aplicaciones como file chooser, etc. El usuario puede volver nuevamente a la aplicación principal y a su vez ser interrumpido por una llamada telefónica y luego de contestar podría finalmente compartir la imagen.

Todo esto en Android sigue siendo muy normal y tu aplicación debe manejar estos flujos correctamente porque recuerda que los dispositivos móviles están restringidos por recursos, en cualquier momento el sistema operativo puede eliminar algunas aplicaciones para poder dar espacio a otras.

En conclusión, los componentes de una aplicación, tienen su propio ciclo de vida, pueden ser lanzados individualmente, destruidos en cualquier momento por el usuario o sistema y lamentablemente no están bajo nuestro control. Por esta razón nunca debes almacenar ninguna data de aplicación o estado en los componentes de la aplicación. ¿CÓMO ESTRUCTURO MI APLICACIÓN ENTONCES?

PRINCIPIOS ARQUITECTURALES

El primer principio importante es la Separación de Intereses: Un error común es escribir toda nuestra lógica en una Actividad o Fragmento. Si tu código no interactúa con la UI pues no debería ir en estas clases. Además tener código lo más simple posible en estas clases evitará muchos problemas relacionados con el ciclo de vida. Lo mejor es minimizar las dependencias que tengamos en estas clases para proporcionar una sólida experiencia de usuario.

El segundo principio importante es manejar tu UI desde un modelo preferiblemente un modelo persistente. ¿Y porque la persistencia es ideal?

  • Tus usuarios no perderán su información incluso si el SO destruye tu aplicación para liberar recursos.
  • Tu aplicación seguirá trabajando incluso si la conexión de red está lenta o desconectada.

Los modelos son responsables de manejar los datos, son independientes de la vista y de los componentes, por lo tanto están aislados de problemas relaciones con el ciclo de vida de los componentes.

Usar clases de modelos con una responsabilidad bien definida para el manejo de datos permitirá que estas sean testeable y tu aplicación consistente.

¿QUE ES LA ARQUITECTURA DE COMPONENTES?

La arquitectura de componentes se presentó en respuesta a diversos problemas comunes que tenían los desarrolladores, tales como manejos del ciclo de vida en actividades, tener diversas formas de estructurar una aplicación y todo aquello que producía que el desarrollo en Android sea lento.

La arquitectura de componentes es un conjunto de librerías de Android para poder estructurar tu aplicación de una manera en que esta sea robusta, testeable y mantenible.

Logo que representa la Arquitectura de Componentes.

Pues bien ahora veremos los diversos componentes de arquitectura tales como Room, Lifecycle, LiveData, ViewModel, veremos también una clase especial llamada Repositorio y clases muy importantes para la administración de los datos como Dao y NetworkResource.

Room

Es una librería de mapeo de objetos SQL bastante robusta y toma cuidado de la data local persistente para la aplicación.

Ventajas de usar Room:

  • Olvídate de SQLiteOpenHelper, con Room te asegurarás de no tener código acumulado, o en inglés (Less Boilerplate) porque Room mapea los registro de tu base de datos y los convierte a objetos como tus entidades y vice versa. Por lo tanto olvídate de los famosos ContentValues y Cursores.
  • Algo asombroso de Room es que te permite validar tus consultas SQLite (“SQLite queries”) en tiempo de compilación, entonces si te equivocas en tu consulta te brindará un mensaje de ayuda, por lo tanto sabrás cómo solucionarlo fácilmente.
  • Finalmente Room tiene un buen soporte de observación para trabajar con LiveData y RxJava.

Componentes de Room

  • Anotaciones: Room te permite escribir menos líneas de código justamente porque genera código android SQLite internamente y esto lo hace posible gracias a las anotaciones. De esta forma nos brinda una simple API para la base de datos.
  • @Entity: Define la estructura de una tabla en tu base de datos.
  • @Dao: Representa el Objeto de Acceso a la Base de Datos, y es la interfaz que define todas las operaciones de lectura y escritura que tu app necesitará.
  • @Database: Esta anotación es usada para crear una nueva base de datos o adquirir una conexión a una RoomDatabase preexistente. Así mismo se coloca en el objeto de base de datos.

Pues bien ahora si profundizemos en el código:

Creamos nuestra entidad en el archivo UserEntity.kt

@Entity
data class UserEntity(@PrimaryKey(autoGenerate = true) val id: Int, val name: String)

Creamos nuestra interface UserDao.kt que contiene un CRUD básico.

@Dao
interface UserDao {
@Query("SELECT * FROM UserEntity WHERE id = :userId")
fun getUser(userId: Int): LiveData<UserEntity>

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUser(userEntity: UserEntity)

@Update
fun updateUser(userEntity: UserEntity)

@Delete
fun deleteUser(userEntity: UserEntity)
}

Y finalmente nuestra base de datos en el archivo SampleRoomDatabase.kt

@Database(entities = [UserEntity::class], version = 1, exportSchema = false)
abstract class SampleRoomDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
Diagrama de la estructura de RoomDatabase.

The Repository — The Mediator

El repositorio es la clase que maneja todas las operaciones que tienen que ver con datos, funciona como una API limpia para manejar toda las operaciones con datos.

En aplicaciones que solo trabajan con base de datos, el repositorio directamente se comunicará con nuestra(s) clase(s) Dao. Pero en la realidad cuando desarrollamos aplicaciones más robustas ya necesitamos interactuar con Servicios Web y con Base de Datos. Entonces nuestro repositorio se convierte en un mediador entre diferentes fuentes de datos.

Toda la lógica de decidir de donde se obtendrá la información ya se de base de datos, datos locales en caché o de red, estará en el repositorio. Quiero decir que cuando la UI necesite alguna información, no tendrá que preocuparse de donde se obtendrá la información. Recordar que el Repositorio no es parte de las librerías de Arquitectura de Componentes es en realidad una buena práctica.

Representación de la clase Repositorio.

En este caso nuestra clase repositorio es UserRespository.kt, por ahora no examinemos el cuerpo de esta clase, solo entendamos que esta clase contiene tanto nuestro DAO (UserDao) como el Network (SampleApi), y adicionalmente una instancia AppExecutors para manejar procesos en hilos secundarios y en el hilo principal.

@Singleton
class UserRepository
@Inject
constructor(private val appExecutors: AppExecutors,
private val userDao: UserDao,
private val sampleApi: SampleApi) {
...
}

La clase NetworkResource

Así como en Room, nuestro Dao se encarga de interactuar con la base de datos y manejar los distintos queries, lo mismo ocurre cuando queremos interactuar con los WebServices, en este caso nuestro API (un cliente Http como Retrofit) realizará las diversas llamadas(Callbacks) en un clase separada en nuestro caso NetworkResource.kt.

abstract class NetworkResource<R> {

private val result: MediatorLiveData<Resource<R>> = MediatorLiveData()

init {
result.value = Resource.loading(null)
fetchFromNetwork()
}
//INSIDE THIS METHOD YOU CAN DEFINE YOUR OWN LOGIC
private fun fetchFromNetwork() {
val apiResponseLiveData = createCall()
setValue(Resource.loading(null))
result.addSource(apiResponseLiveData) {
result.removeSource(apiResponseLiveData)
if (it.isSuccessful()) {
setValue(Resource.success(it.body))
} else {
setValue(Resource.error("error", null))
}
}
}

@MainThread
private fun setValue(newValue: Resource<R>) {
if (result.value != newValue) {
result.value = newValue
}
}

fun asLiveData(): LiveData<Resource<R>> = result

@NonNull
@MainThread
abstract fun createCall(): LiveData<ApiResponse<R>>

}

El código de arriba puede confundir un poco si no se tiene algo de experiencia utilizando LiveData, todo es cuestión de practicar. Analicemos esta clase entonces:

  • Conceptos como MediatorLiveData lo veremos luego en la sección LiveData.
  • Al ser una clase abstract significa que como mínimo tendrá métodos abstractos(createCall()) y también puede contener funciones normales (asLiveData() y setValue()).
  • La clase Resource<T> es un encapsulador de cualquier tipo(generalmente de clases de tipo response), pero lo más importante te permite agregarle un estado, data y mensaje de respuesta que tu definas.

En resumen, cuando crees una instancia de esta clase, tendrás que sobreescribir el método createCall() y realizarás la llamada al servicio que tu definas, si la respuesta es successful o error encapsularás esa respuesta en Resource.succes(…) o Resource.error(…). Finalmente retornamos el objeto MediatorLiveData con el metodo asLiveData().

Diagrama de la estructura de NetworkResource.

Lifecycle

Hay un par de conceptos y clases core que no vamos a usar directamente pero debemos ser conscientes de ellos. Esta librería contiene lo siguiente:

  • Lifecycle: Un objeto que define un ciclo de vida en Android.
  • LifecycleOwner: Un objeto que tiene un ciclo de vida como por ejemplo una Actividad o Fragmento. Desde la versión 26.1 de la librería Android Support la clase AppActivityCompat ya es un LifecycleOwner.
  • LifecycleObserver: Es una interface para observar un LifecycleOwner. Si alguna vez has trabajado con services or listeners y has tenido que detenerlos o limpiarlos de alguna forma en el método onStop() por ejemplo, pues ahora estos services or listeners podrían usar esta observación de Lifecycle y hacerlo más limpio todavía.

ViewModel

Los ViewModels son objetos que proveen datos, información para componentes de UI y sobreviven a cambios de configuración. Un claro ejemplo de cambio de configuración es cuando rotamos el dispositivo (portrait o landscape) o cuando realizamos cambios en el idioma del dispositivo.

Por lo tanto si el ViewModel sobrevive a cambios de configuración, eso significa que podemos evitar hacer tareas asíncronas cada vez que haya un cambio de configuración.

Pero lo más importante es que con el uso de ViewModel estamos siguiendo un principio muy importante que es la separación de responsabilidades, mencionado anteriormente. Esta arquitectura permite que tengamos separado nuestro código de forma más inteligente, así nuestra actividad solo será responsable de pintar los datos.

Viewmodel Lifecycle

Entonces algunos puntos a tomar acerca del ciclo de vida del ViewModel son:

  • Un ViewModel sobrevive a cambios en la configuración.
  • Un ViewModel no sobrevive(es destruido) a una actividad que es destruida. Está vinculado a un ciclo de vida la actividad pero no al de la aplicación.Si el usuario elimina la aplicación presionando el button back o haciendo un swipe en la lista de aplicaciones pues el ViewModel no sobrevive.
  • Un ViewModel no es un reemplazo a Base de Datos o persistencia de datos, es algo transitorio. Tampoco reemplaza al onSaveInstanceState aunque parezcan muy similares. Si tienes demasiada información visual, procesos, etc y tu aplicación está en background puede que el SO mate tu aplicación. En este caso el ViewModel es destruido y es mejor usar onSaveInstanceState para evitar este problema.
Ante cambios de configuración nuestro ViewModel seguirá vivo hasta que se elimine completamente la actividad, en ese caso el ViewModel se limpia automáticamente.

LiveData

Es una clase que holdea datos como objects, entities, etc y que puede ser observable.

Características

  • Es consciente del ciclo de vida.
  • Contiene un valor y permite que este valor pueda ser observado.
  • Notifica a los observadores cuando la data cambia y de esta forma puedes actualiza la UI.
  • Room soporta LiveData, de esta manera estos dos componentes de arquitectura pueden trabajar de una manera muy poderosa creando una UI reactiva.

En nuestro caso estamos aprovechando el potencial de LiveData y Room, por esta razón nuestro archivo UserDao.kt contiene el siguiente fragmento de código, donde definimos nuestro @Query y retornamos un LiveData.

@Query("SELECT * FROM UserEntity")
fun getUsers(): LiveData<List<UserEntity>>

Observer Pattern

Quizás muchos ya estén familiarizados con este patrón, pero explicaremos de manera básica como funciona ya que este patrón es utilizado para trabajar con LiveData y la UI.

El patrón observador se da cuando un objeto llamado subject(sujeto) tiene una lista de objetos asociados llamados observers(observadores). Cuando el estado del sujeto cambia, este notifica a todos los observadores usualmente llamando uno de sus métodos en los observadores.

This picture shows you how the Observer Pattern works.

En este caso nuestro subject sería nuestro LiveData que contiene una lista de usuarios LiveData<List<UserEntity>>, el Observer estará pendiente de los cambios que puedan ocurrir en la tabla de la base de datos y finalmente notificará a la UI(UserActivity).

Interactividad entre LiveData y UI

Pues bien, conociendo todos estos conceptos básicos ahora veamos cómo se realiza la interacción entre LiveData y nuestra UI en nuestra aplicación.

Interacción entre ViewModel, LiveData y UI Controller.

Imaginemos el escenario donde queremos traer un usuario especifico ya desde base de datos o desde la un servicio web. Pues bien, de esa lógica se encargará el Repositorio, así que no nos preocupemos de eso ahora, solo sabemos que al final obtendremos un LiveData<Resource<UserEntity>>.

  • Declaramos un getSpecificUserResourceLiveData que contendrá a nuestro UserEntity y que observaremos en nuestra actividad por eso debe ser público.
  • Declaramos un userIdMutableLiveData para poder asignar el userId que queremos buscar.
  • En el bloque init{} utilizamos Transformations.switchMap, los Transformations son una nueva es una funcionalidad que trae la librería Lifecycle y permite realizar diversas transformaciones a nuestros LiveData. En este caso por ejemplo el switchMap intercambia el valor recibido por el MutableLiveData y lo pasa al LiveData.
  • Creamos los métodos necesarios como loadUser() para asignar el userId a nuestro userIdMutableLiveData.

class UserViewModel
@Inject
constructor(private val userRepository: UserRepository) : ViewModel() {
... @VisibleForTesting
val userIdMutableLiveData : MutableLiveData<Int>=MutableLiveData()
var getSpecificUserResourceLiveData: LiveData<Resource<UserEntity>>

init {
...

//GET SPECIFIC USER
getSpecificUserResourceLiveData = Transformations.switchMap(userIdMutableLiveData, { userId ->
if (userId == null || userId == 0)AbsentLiveData.create()
else userRepository.getUsers(userId)
})
}
/**
* Fetch a user by their id
*/
fun loadUser(userId: Int) {
if (userIdMutableLiveData.value != null && userIdMutableLiveData.value == userId) {
return
}
userIdMutableLiveData.value = userId
}

/**
* If something was wrong with the @link{loadUser()} method, then we can retry the request.
*/
fun retryLoadUser() {
userIdMutableLiveData.value?.let {
userIdMutableLiveData.value = userIdMutableLiveData.value
}
}

Finalmente los observadores se crean en la Actividad porque desde aquí notificarán a la Vista todos los cambios que puedan ocurrir ya sea desde base de datos y algún servicio web, etc.

class UserActivity : DaggerAppCompatActivity(){...
@Inject
lateinit var userViewModel: UserViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
setSupportActionBar(toolbar)
initialize()
}
private fun initialize() { ...


//Fetch all users at the first time
userViewModel.loadAllUsers(true)

//Setting up Listeners
srlRefresh.setOnRefreshListener {
if (srlRefresh.isRefreshing) srlRefresh.isRefreshing = false
userViewModel.retryLoadAllUsers()
}

...

subscribeToUserModel()
}
private fun subscribeToUserModel() {
//OBSERVER GET ALL USERS
userViewModel.getAllUsersResourceLiveData.observe(this, Observer {
when (it!!.status) {
Status.SUCCESS -> {
userAdapter.addAllUsers(it.data!!)
}
Status.ERROR -> {
showToast(it.message!!)
}
Status.LOADING -> {
showToast("Loading Users...")
}
}
})

//OBSERVER POST NEW USER
userViewModel.postUserResourceLiveData.observe(this, Observer {
when (it!!.status) {
Status.SUCCESS -> {
userAdapter.addUser(it.data!!)
userDialogFragment.dismiss()
}
Status.ERROR -> {
showToast(it.message!!)
}
Status.LOADING -> {
showToast("Saving User...")
}
}
})
}

The final Android Architecture Component

The final Android Architecture Component

Conclusión

La arquitectura de componentes fue hecha para el desarrollo de aplicaciones Android, eliminar todo aquello que provocaba lentitud en el desarrollo y ser un standard de arquitectura para crear aplicaciones robustas, testeables y mantenibles. Trae consigo poderosas librerías que brindan grandes beneficios y al mismo tiempo buenas practicas. Finalmente serás capas de invertir mas tiempo en diseño, lógica de negocio y testing. Happy coding!!

Ver el ejemplo en GitHub

Referencias

Videos

--

--