Un ViewModel para guardarlos a todos
--
Si pensabas que no podías exprimir más a tus ViewModels, aquí charlaremos sobre cómo y qué más puedes hacer con ellos usando SavedState.
Los últimos años han sido, para todos los desarrolladores Android, un sin fin de emociones. Los avances que se han hecho, la cantidad de librerías nuevas, mejoras y cambios no sólo estéticos sino de performance en la plataforma han entusiasmado a todos. Entre todos estos avances y mejoras, la incorporación y clasificación de librerías dentro de Android Jetpack, es a mi entender, de lo mejor.
Si bien estas librerías continúan avanzando y se van incorporando algunas otras — como lo es Hilt — , hoy vamos a concentrarnos sobre una en particular, los ViewModels. Éstos, en combinación con LiveData, nos dan un marco seguro para implementar — y por qué no migrar — nuestra arquitectura a MVVM.
La particularidad de los ViewModels así también como la recién salida Hilt, tiene que ver con lo que Google está apostando para el presente y el futuro de las aplicaciones Android. Nos han empezando a recomendar - sugerir- que las utilicemos, algo que nunca había ocurrido antes y sí era una constante en otras plataformas.
Se ha hablado bastante del uso de los ViewModels, las buenas y las malas prácticas, sus aplicaciones, recomendaciones, etc. Sin embargo, hay una cosa más que podemos hacer con ellos para extender su funcionalidad y es, mantener información aún cuando Android nos mata el proceso estando nuestra app en background; generalmente ocasionado por baja memoria (lowMemory).
SavedState
Ya es sabido que los ViewModels han sido pensados para mantener el estado de la UI cuando se producen cambios de configuración. Esto nos evita tener que realizar pedidos extras por información que ya teníamos disponible, haciendo esperar al usuario, sobrecargando nuestras API, etc. Adicionalmente contamos con el callbackonSavedInstanceState
en nuestros activities/fragments que nos permite guardar información que puede ser recuperada incluso un tiempo después de que ésta haya sido destruida por el sistema.
Perfecto, ya está resuelto. Entonces, ¿para qué necesitamos SavedState? Bueno, digamos que si vamos a persistir cierta información liviana que necesitamos sobreviva a la posible destrucción del Activity sería ideal tener todo "en un mismo lugar", tal de no tener inconvenientes a la hora de dónde buscar qué cosa. Si bien los ciclos de "retención" de la información son distintos si hablamos de LiveData's y onSaveInstanceState, al tenerlos separados estamos delegando la responsabilidad de mantener nuestras vistas actualizadas tanto en el ViewModel como en los activities/fragments por igual.
SavedState viene de cierto modo a cubrir esta necesidad de unificar responsabilidades. Veamos de qué se trata.
Configuración
Para poder trabajar con SavedState necesitamos incluír la dependencia de Fragments o Activities de androidx.
En este caso vamos a usar las dependencias de Fragment, pero podríamos haber usado la de Activities y sería lo mismo.
Para versiones anteriores de estas dependencias, es necesario incluír las dependencias de lifecycle-viewmodel-savedstate.
Una vez incluída la dependencia, los ViewModels tendrán la posibilidad de recibir como parámetro un objecto SavedStateHandle
que se encargará de guardar y de devolvernos información aún en aquellos casos donde nuestro proceso fue terminado por el sistema.
Podemos hacer uso del delegate de viewModels
y nos quedaría de la siguiente manera:
Nuestro ViewModel nos quedaría de la siguiente manera:
Si prestamos atención a nuestro constructor, estamos recibiendo el objeto state de tipo SavedStateHandle. A través de este, podremos guardar y acceder luego a los datos que guardemos ahí. Si bien estamos guardando un objeto del tipo Serializable
, podemos guardar otros tipos de datos.
Una particularidad de este objeto, es que nos permite obtener un valor que hayamos guardado anteriormente a través de un LiveData. En el snippet anterior podemos ver como recuperamos la posición del scroll de un RecyclerView en caso de que nuestro proceso muera.
Cada una de las vistas es capaz de guardar sus propios datos en caso de cambios de configuración, pueden comprobar que sus recyclers y cualquier otra vista mantiene su información. Básicamente cada view que contenga un id implementa su propio
onSaveInstanceState
.
Dijimos que nuestro objeto sobrevive la finalización del proceso por parte de Android. Veamos cómo validarlo utilizando el proyecto que dejaré al final del artículo.
Una vez instalada la aplicación en nuestro dispositivo (o utilizando un emulador), vamos a recrear el proceso de destruír nuestro Activity, entonces al recrearse deberíamos quedar en el mismo estado:
- Abrimos la app de ejemplo
- Scrolleamos “un poco”
- Mandamos la app a background.
- Buscamos la app utilizando el adb y la matamos.
- Al volver a la app, chequeamos que efectivamente nuestra app quedó con el recycler en la posición en que lo dejamos.
Para buscar nuestra app en los procesos del dispositivo hacemos adb shell ps -A | grep com.my-package y para finalizar el proceso adb shell am kill com.my-package
Si tenemos habilitado en nuestros dispositivos las opciones de desarrollador, podemos configurar la opción Don't keep activities para emular matar el proceso en caso de cambiar de Activity o enviarla a background.
Momento… mis ViewModels usan Interactors.
Así se encuentren trabajando con alguna arquitectura en particular para toda su aplicación, como pueden ser Clean Architecture o IDD, o bien, estén haciendo unit testing sobre sus ViewModels, es necesario inyectarles otros objetos (dependencias), por lo que el uso del delegate tal cual lo usamos no nos sirve.
Lo que necesitamos es indicarle a nuestro delegate cuál es el factory con el que tiene que construír nuestro ViewModel y para poder hacer uso de SavedState, tenemos que extender del AbstractSavedStateViewModelFactory
. De esta manera, podemos construír nuestro ViewModel con todas sus dependencias y adicionar el SavedStateHandler
.
De aquí podemos identificar algunos objetos que valen la pena mencionar, para entender cómo se logra el objetivo de preservar nuestros datos:
SavedStateRegistryOwner
: Básicamente lo que necesitamos es alguna clase que pueda manejar unSavedStateRegistry
. En nuestro caso, le hemos provistothis
porqueFragment
implementa esta interfaz.AbstractSavedStateViewModelFactory
recibe un segundo parámetro que hemos puesto comonull
(opcional en realidad) nos permite en caso de no tener estado previo, proveer unBundle
con info por default.
Casos prácticos
Esta implementación que presentamos es un mero ejemplo y quizás, poco útil y hasta trivial. Los recyclers, al igual que todas las vistas a las que hayamos indicado un id (identificador) son capaces de guardar su propio estado. Sin embargo, SavedState puede ser una solución a la hora de evitar crashes en flujos complejos, que requieran transiciones entre varios fragments, como pueden ser flujos de login, compra, carga de encuestas, etc; en donde nuestra app puede ir a background, ser destruída por Android y al regresar, no contamos más con esa información necesaria que creíamos tener asegurada ocasionando crashes en producción, problemas de usabilidad y mala experiencia de usuario.
Conclusión
Los ViewModel han sido uno de los patrones de arquitectura de esos que han llegado para quedarse definitivamente (o por lo menos durante un tiempo). Han sabido combinar el patrón MVVM y el ciclo de vida de Android de tal manera que, bien utilizados, nos permiten obtener buenos resultados de manera segura. SavedState como complemento, es simplemente más útil aún para aquellas ocasiones y contextos en los que necesitamos ir más allá, unificando las responsabilidades y asegurandonos que no importa qué pase con nuestra aplicación en background, la información seguirá allí.
Adelante, pruébenlo!
Pueden encontrar todo el código utilizado en el siguiente repositorio:
Para contactarnos, @nogueirasjn en 🐦.
Happy coding!