Un ViewModel para guardarlos a todos

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 callback onSavedInstanceState 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 incluir la dependencia de Fragments o Activities de androidx.

dependencies {
  ...
  implementation("androidx.fragment:fragment-ktx:1.2.5")
  ...
}

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:

class MainFragment : Fragment(R.layout.main_fragment) {
  
  private val viewModel: MainViewModel by viewModels()
  ...
}

Nuestro ViewModel nos quedaría de la siguiente manera:

class MainViewModel(private val state: SavedStateHandle) : ViewModel() {

    private val _data = MutableLiveData<List<String>>()

    val scrollPosition: LiveData<Scroll> = state.getLiveData(CONFIGURATION_KEY)

    val data: LiveData<List<String>> = _data

    init {
        val data = (1..100).map { it.toString() }
        _data.postValue(data)
    }

    fun onSaveInstanceState(configuration: Scroll) {
        state.set(CONFIGURATION_KEY, configuration)
    }

    private companion object {
        const val CONFIGURATION_KEY = "scroll_position"
    }
}
class MainFragment: Fragment(...) {

    override fun onSaveInstanceState(outState: Bundle) {
        val manager = binding?.recycler?.layoutManager as LinearLayoutManager
        val configuration = Scroll(
            position = manager.findFirstVisibleItemPosition()
        )
        // The name of the method could be anything.
        viewModel.onSaveInstanceState(configuration)
        super.onSaveInstanceState(outState)
    }

}

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.


Pruebas, necesito pruebas

Una vez instalada la aplicación en nuestro dispositivo (o utilizando un emulador), vamos a recrear el proceso de destruir nuestro Activity, entonces al recrearse deberíamos quedar en el mismo estado:

  1. Abrimos la app de ejemplo

    Vamos a posicionarnos sobre el Activity el cual verificaremos que se está recreando de la manera en que lo dejamos.

  2. Hacemos un poco de scroll

    Necesitamos verificar que efectivamente nuestro estado fue recuperado.

  3. Mandamos la app a background.

    Vamos a simular la destrucción cuando lo hace el sistema.

  4. Buscamos la app utilizando el adb y la matamos.

    Realizamos manualmente el cierre del Activity.

  5. Al volver a la app, chequeamos que efectivamente nuestra app quedó con el recycler en la posición en que lo dejamos.

    Verificamos que todo ha quedado donde debería estar.

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, es neceasrio que extendamos de la clase AbstractSavedStateViewModelFactory.

De esta manera, podemos construir nuestro ViewModel con todas sus dependencias y adicionar el SavedStateHandler.

class MainFragment : Fragment(...) {
  
  private val viewModel: MainViewModel by viewModels(factoryProducer = {
        provideMainViewModelFromFactory(this)
    })
}

fun provideMainViewModelFromFactory(owner: SavedStateRegistryOwner) =
        object : AbstractSavedStateViewModelFactory(owner, null) {
            override fun <T : ViewModel?> create(
                key: String,
                modelClass: Class<T>,
                handle: SavedStateHandle
            ) = MainViewModel(handle) as T
        }
}

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 un SavedStateRegistry. En nuestro caso, le hemos provisto this porque Fragment implementa esta interfaz.
  • AbstractSavedStateViewModelFactory recibe un segundo parámetro que hemos puesto como null (opcional en realidad) nos permite en caso de no tener estado previo, proveer un Bundle 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.

Si querés saber más sobre ViewModels te recomiendo pasarte por el siguiente artículo donde te cuento qué podes hacer con tus ViewModels y Android Jetpack.


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.

Sigamos conectados 😄
Happy Coding! ✍️

0

Comentarios

  1. Pingback: Cómo mejorar tus ViewModels en Android | Android Blog by Jorge Nogueiras

Deja un comentario