Last Updated: 2021-30-05
En el caso de las aplicaciones de Android, el entorno móvil implica una serie de restricciones sobre los recursos computacionales disponibles para la aplicación. Los dispositivos móviles son dispositivos pensados para la portabilidad los cuales suelen tener menor capacidad en términos de memoria y capacidad de procesamiento que otros dispositivos más grandes.
Las aplicaciones móviles deben ser construidas con esto en mente, pues cada vez es más usual que la cantidad de datos que maneja una aplicación móvil excede sus capacidades al punto de poder afectar el desempeño. Así mismo, el acceso de un dispositivo móvil a una red estable no está garantizado y existen muchos escenarios en los que es necesario que la aplicación siga funcionando a pesar de no contar con red.
Entonces, utilizar un caché para almacenar parte de la información que se ha obtenido en la aplicación permite reducir el tráfico de red en caso de utilizarse para almacenar datos o multimedia localmente. Estos datos estarán disponibles para la aplicación en la memoria del dispositivo, por lo cual su consulta también será más rápida y estará disponible siempre y cuando no se elimine del caché.
Además de esto, también existen beneficios de implementar un caché en sus aplicaciones desde la perspectiva del servidor. En los sistemas es muy común que exista un componente que expone información por medio de un API, y que a su vez consulta otros componentes como bases de datos y posiblemente requiera procesar la información ante cada consulta. Si el cliente utiliza un caché para la información que el servidor le entrega, también puede reducir el número de peticiones al API REST y ocupar menos espacio de procesamiento del servidor.
Nota: este tutorial se enfoca en el caché utilizado con el fin de almacenar datos obtenidos de internet. Existen muchos otros usos para el caché en Android, como por ejemplo, para el manejo de la sesión de un usuario.
En este tutorial se trabajará sobre una aplicación desarrollada para otros tutoriales anteriores. Dicha aplicación permite visualizar la información de un API REST que contiene información acerca de coleccionistas de álbumes musicales y los comentarios que realizan sobre ellos.
Al final de este tutorial usted tendrá:
Al desarrollar este tutorial aprenderá:
En este tutorial se tendrán en cuenta fuentes de datos en servicios web, y se hablará acerca del caché desde una perspectiva del almacenamiento local en el dispositivo. Es importante recordar que utilizar cachés como componentes intermedios es también una solución bastante utilizada, y se suele sustentar en servicios en la nube como Redis, Memcached, entre otros.
En principio, las estrategias funcionan de la misma forma para un caché local y un caché en un componente independiente. A continuación se explicará el detalle de cada caso.
La primera estrategia de obtención de datos desde un servicio web es consultar directamente el endpoint por medio de una petición realizada a través de un protocolo como, por ejemplo, HTTP. Para este fin se suelen utilizar librerías o clientes dentro de la aplicación, como las que se mencionaron en el tutorial "Cómo invocar APIs Rest desde aplicaciones nativas por medio de Frameworks". En dicho tutorial también se mencionó que varias de estas librerías manejan un caché de forma interna sin necesidad de una configuración por parte del desarrollador. No obstante, la estrategia Network Only implica que cada vez que se necesite actualizar la información desde el servidor web, se debe hacer una petición al servicio web.
Con esta aproximación se presentan múltiples desventajas que se han mencionado anteriormente como las siguientes:
En segundo lugar, se tiene el uso más común de un caché, en donde la aplicación se comunica tanto con el componente del caché como con la fuente de datos principal. En esta aproximación, la aplicación prioriza la información obtenida desde el caché, buscando aprovechar todas sus ventajas. Sin embargo, en caso de que no se encuentre información adecuada en el caché, es necesario hacer la consulta con la fuente original de datos, que suele ser un servicio web.
Esta estrategia de caché se suele utilizar para operar cargas de trabajo altas en cuanto a lectura. Desde una perspectiva del cliente en el entorno móvil, esta estrategia es beneficiosa ya que permite definir los criterios para consultar cada fuente de datos, y porque ayuda a reducir el impacto de obtener la información siempre de la misma fuente.
Además, también se puede aprovechar esta estrategia para usar esquemas de datos distintos para cada fuente de datos sin afectar la funcionalidad del sistema. No obstante, esto puede generar muchas inconsistencias entre las fuentes de datos; lo cual genera una necesidad de desarrollar estrategias de manejo de la consistencia. Estas estrategias dependen del tipo de aplicación y de la información que maneje, pero por lo general se utiliza un time-to-live para determinar en qué momento se deben refrescar los datos en la aplicación.
En esta aproximación se escribe directamente en las fuentes originales de datos y únicamente se guarda en el caché la información cuando se lee. Esto suele ser útil cuando las operaciones de lectura son inusuales, ya que se implementa un caché de poco uso, pero que puede cumplir su propósito en las ocasiones que sí se utiliza.
Esta aproximación de caché funciona de la misma manera que Write Through Cache, con la diferencia de que el caché está disponible de forma inmediata y quien escribe la información de vuelta en las fuentes de datos originales es la aplicación.
Esta estrategia sitúa al caché como intermediario entre la aplicación y la base de datos, de forma que la aplicación lee siempre del caché, y el caché se actualiza de forma lazy con respecto a la fuente original de datos. Esto quiere decir que se cargan los datos únicamente la primera vez que se realiza la lectura. Además, en esta aproximación, es evidente que el caché tiene la responsabilidad de comunicarse con el servicio de red, lo cual no suele ser una capacidad de estos componentes.
Cuando se implementa esta estrategia, la estructura de la información en la base de datos y el caché debe ser idéntica. Esta es adecuada para los casos en donde la carga de trabajo de lectura es muy elevada, sobre todo si se consulta la misma información múltiples veces, como en las noticias.
Esta estrategia funciona de la misma forma que la anteriormente mencionada, pero teniendo en cuenta las operaciones de escritura en el sentido desde la aplicación hacia las fuentes de datos. Implementar un caché de este tipo permite que las operaciones de escritura actualicen una fuente que es de acceso más rápido para la aplicación, e incluso permite que la actualización de las fuentes originales de datos se delegue de forma asíncrona. No obstante, se agrega latencia a la actualización de las fuentes originales.
Se suele utilizar esta estrategia de la mano con Read Through Cache, con el fin de garantizar la consistencia de los datos y evitar la necesidad de implementar técnicas de invalidación de caché.
Las estrategias presentadas anteriormente mostraron diferentes formas de organizar un componente de caché dentro de la arquitectura de una aplicación de Android. Dichas estrategias son altamente adaptables para las necesidades de cada caso.
Al momento de elegir una estrategia, no basta con seleccionar la forma en que se comunican los componentes, pues también hay que pensar en los criterios que mueven el funcionamiento de los mismos (sobre todo al momento de implementar un repositorio). En particular, es importante tener en cuenta varios aspectos como:
Anteriormente se explicó que existen formas de almacenar información en caché por medio de varios componentes arquitectónicos. En este paso se explicarán las estrategias y la lógica que se puede implementar para decidir entre el caché local y las fuentes remotas.
Como el nombre lo indica, en esta estrategia, siempre se toma la información desde el almacenamiento local. Esto únicamente tiene sentido para aplicaciones con información local, que no requieren de solicitudes de red. Muchas aplicaciones se alimentan de datos generados por sí mismas, como es el caso, por ejemplo, de los juegos que guardan progreso de forma local.
Como el nombre lo indica, y por lo contrario a la anterior, en esta estrategia siempre se toma la información desde la red. Esta estrategia tiene sentido para datos que cambian a una alta velocidad, o donde se requiera ver un estado inmediato de la información accediendo directamente a la fuente original. Es importante no caer en malas prácticas al utilizar esta estrategia, pues los errores en la red pueden afectar la experiencia del usuario según como sean manejados.
En este caso, se tiene como objetivo una aplicación con filosofía offline-first. Esta estrategia es aplicable a la gran mayoría de casos de uso, y se puede resumir en una combinación de la estrategia cache only cuando la información buscada se encuentra en el cache, y network only cuando no se encuentra la información en el almacén local.
Existe una estrategia que puede funcionar en los casos en que el dispositivo no cuente con los recursos adecuados y, por ejemplo, tenga un acceso muy lento al disco. En este caso existe la posibilidad de consultar tanto la fuente del cache como la fuente remota por medio de la red, tomando como fuente para la información a aquella que sea capaz de brindar una respuesta primero. Esta estrategia requiere concurrencia, lo cual también puede ser una barrera en ciertos dispositivos.
En caso de que la información que se quiera consultar se actualice con frecuencia, suele ser importante mostrar el contenido más recientemente actualizado. Sin embargo, es posible mostrar la última información recolectada en caso de que no se cuente con conexión a internet, o en caso de que falle la comunicación. Esta estrategia implica también, que cada vez que se obtiene información de la red, se actualiza el cache, pero este cache solo se utiliza cuando es estrictamente necesario. Esta estrategia puede traer problemas cuando la conexión es lenta, ya que debe esperar a que falle (por timeout) antes de acceder al cache.
En esta estrategia también se accede a las dos fuentes, pero sin un principio de competencia entre ellas. Por el contrario, se accede al cache para mostrar un contenido en primera instancia, y este se actualiza en caso de que la solicitud de red muestre una información distinta. Esto puede funcionar en algunos casos, pero no es adecuado, por ejemplo, si puede hacer que un texto que el usuario esté leyendo se actualice y lo pierda. En el caso de Twitter, se utiliza, y se maneja internamente el contenido más reciente por separado del contenido almacenado en cache.
Para algunos recursos, suele ser útil almacenar de forma local una respuesta genérica en caso de que no haya ninguna forma de obtener el contenido. Esto puede aplicarse, por ejemplo, para mostrar una imagen de avatar que no es accesible desde ninguna de las fuentes estándar.
En la aplicación que se ha venido utilizando para los tutoriales del curso no se ha tenido en cuenta la existencia de un componente de caché que almacene información de forma local para el rápido acceso. Sin embargo, desde tutoriales anteriores se ha reconocido el papel del repositorio en la implementación de una estrategia de obtención de información y de caché.
El repositorio, en la arquitectura de Android propuesta con Jetpack, es el responsable de consultar las fuentes de datos y comunicar los resultados con los ViewModels. Como hasta el momento solo existía una fuente de datos, que corresponde al servicio web, el código del repositorio es trivial. En el método refreshData
del repositorio, únicamente se hace un llamado a la función que obtiene la información desde el API REST.
Abra el archivo de un repositorio, por ejemplo CollectorsRepository
, y note que su contenido se ve de la siguiente forma:
En el comentario se especifica que parte importante de la consulta de la información corresponde a decidir por la fuente de datos que se va a consultar. Esto depende de la estrategia que se decida utilizar, y se hará con el apoyo de varias librerías de Android. Hasta el momento, la estrategia que se ha utilizado podría catalogarse como Network Only, ya que se decide que siempre se va a consultar al servicio web.
En este punto vale la pena recordar que en la aplicación que se ha venido desarrollando, un alto porcentaje de las operaciones realizadas consisten en lecturas de información y flujos de navegación entre vistas que muestran dicha información a modo de listas. Además, se está utilizando un API REST cuya información no cambia con mucha frecuencia. También vale la pena recordar que se busca implementar un caché de memoria local en el dispositivo Android y no un caché externo que se puede implementar por medio de servicios en la nube.
Con esto en mente, la mejor opción para esta aplicación es implementar un caché local con una estrategia Cache falling back to network donde, por ahora, el almacenamiento local se hará por medio de Shared Preferences, y dada la baja tasa de actualización, no se manejará un tiempo de vigencia para los datos almacenados.
Así, el repositorio de cada entidad debe consultar el estado del almacenamiento local en busca de la información, y en caso de no encontrarla, se debe consultar al servicio web por medio de internet como se ha venido haciendo hasta ahora.
Dado que esta estrategia establece el caché dentro del mismo proceso, es importante recordar que la memoria utilizada por el caché va a estar asociada al espacio de almacenamiento de la aplicación, y que hay que tener en cuenta las limitaciones del mismo.
El proceso para aplicar la estrategia de caché en sus repositorios ya existentes es el mismo para el caso de las tres entidades del sistema. Por este motivo se darán instrucciones para modificar el CollectorRepository
, y usted debe replicar los cambios tanto en el AlbumsRepository
como el CommentsRepository
.
Anteriormente, usted implementó una estructura de datos en el ViewModel de álbumes para reducir el número de consultas relacionadas con los comentarios de cada álbum. Esta estrategia implicó varios ajustes, incluyendo un singleton llamado CacheManager
.
Varios de estos cambios hacen que la refactorización de su código sea sencilla. En particular, dicho singleton servirá para acceder a las SharedPreferences
de forma uniforme en todos los lugares que se requiera. En este objeto habrá una constante para llamar las preferencias relacionadas a los álbumes, y una función que, dado un contexto, retorna las preferencias del nombre indicado. El código de este objeto, luego de agregar las propiedades para las SharedPreferences debe verse de la siguiente forma:
object SPrefsCache {
const val APP_SPREFS = "com.example.jetpack_codelab.app"
const val ALBUMS_SPREFS = "com.example.jetpack_codelab.albums"
fun getPrefs(context: Context, name:String): SharedPreferences{
return context.getSharedPreferences(name,
Context.MODE_PRIVATE
)
}
}
Ahora, abra el CommentsRepository
, y cree una función de suspensión getComments
, para leer los comentarios que se han almacenado en dicha fuente. Esta toma por parámetro el identificador de un álbum, y consulta en las preferencias de los álbumes si existe el álbum. En caso de existir, lo obtiene como cadena de texto y modifica su formato para retornar una lista de comentarios. El código de esta función es el siguiente:
suspend fun getComments(albumId:Int): List<Comment>{
val format = Json { }
val prefs = CacheManager.getPrefs(application.baseContext, CacheManager.ALBUMS_SPREFS)
if(prefs.contains(albumId.toString())){
val storedVal = prefs.getString(albumId.toString(), "")
if(!storedVal.isNullOrBlank()){
return format.decodeFromString<List<Comment>>(storedVal)
}
}
return listOf<Comment>()
}
En este mismo archivo, cree una función de suspensión addComments
, la cual toma un identificador de un álbum y un listado de comentarios para escribir su valor, en formato de String
, en las preferencias. El código de esta función debe ser el siguiente:
suspend fun addComments(albumId:Int, comments: List<Comment>){
val format = Json { }
val prefs = CacheManager.getPrefs(application.baseContext, CacheManager.ALBUMS_SPREFS)
if(!prefs.contains(albumId.toString())){
var store = format.encodeToString(comments)
with(prefs.edit(),{
putString(albumId.toString(), store)
apply()
})
}
}
Note que, en el código, se almacenan los comentarios en el caché de las Shared Preferences directamente como un listado de objetos del tipo Comment. Para poder hacer esto, es necesario que incluya la anotación @Serializable en la definición de la clase Comment, como se ve a continuación:
import kotlinx.serialization.Serializable
@Serializable
data class Comment (
val description:String,
val rating:String,
val albumId:Int
)
Ahora, en el método refreshData
, usted va a invocar los métodos del caché o del NetworkServiceAdapter
según conveniencia. Dado que se busca optimizar el tiempo de respuesta en las consultas, lo primero que debe hacer este método es consultar el caché. En caso de haber un cache hit, se carga la información desde allí. De lo contrario, es necesario consultar el estado de red, e intentar realizar una petición de red, la cual actualice la información del caché para el futuro. Esta estrategia es bastante sencilla y aplica como Cache Aside, ya que la aplicación se comunica tanto con la fuente original como con el caché, prioriza la información local, y solo la actualiza cuando se obtiene la información de internet. El código correspondiente a este método se debe ver de la siguiente forma:
suspend fun refreshData(albumId: Int): List<Comment>{
var comments = getComments(albumId)
return if(comments.isNullOrEmpty()){
val cm = application.baseContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if( cm.activeNetworkInfo?.type != ConnectivityManager.TYPE_WIFI && cm.activeNetworkInfo?.type != ConnectivityManager.TYPE_MOBILE){
emptyList()
} else {
comments = NetworkServiceAdapter.getInstance(application).getComments(albumId)
addComments(albumId, comments)
comments
}
} else comments
}
Note que se hace uso del Connectivity Manager para consultar el estado de la red. Esta es una consideración a tener en cuenta particularmente para las aplicaciones móviles, ya que el entorno suele influir en la disponibilidad de los datos y los servicios. Para comprender más acerca de la clase ConnectivityManager
, y cómo modificar el comportamiento de la aplicación con base en la red, consulte el siguiente enlace: https://developer.android.com/training/efficient-downloads/connectivity_patterns.
Ejecute la aplicación desde Android Studio. Interactúe con las vistas y observe que la información se obtiene de forma adecuada sin necesidad de haber modificado nada más que el repositorio.
Incluya mensajes en consola que le ayuden a identificar qué operaciones se están ejecutando con la función Log.d
. Vuelva a ejecutar la aplicación en su dispositivo para ver reflejados los cambios.
Desde Android Studio, revise la vista Run
de la parte inferior del IDE. Allí podrá ver los resultados de la ejecución de la aplicación y, con ellos, los registros que usted creó con la función Log.d
. Note que para cada vista, al momento de cargar la información del RecyclerView
se indica la fuente de la cual se obtuvieron los datos.
Observe que la primera vez que ejecuta la aplicación y se cargan los datos de cada vista, se recurre al NetworkServiceAdapter
y luego, en las siguientes ejecuciones, se recurre al caché en SharedPreferences
, tal y como se esperaba.
¡Felicidades!
Al finalizar este tutorial, pudo familiarizarse con las diferentes aproximaciones para implementar un caché de datos obtenidos de una fuente de datos por medio de internet. Así mismo, pudo conocer un poco acerca de las alternativas de caché externas y locales que puede utilizar en una aplicación de Android.
Ahora podrá aplicar los principios expuestos en este tutorial para decidir e implementar estrategias de caché que se ajusten a las necesidades de sus proyectos.
Juan Sebastián Espitia Acero | Autor |
Norma Rocio Héndez Puerto | Revisora |
Mario Linares Vásquez | Revisor |