Cómo mejorar tus ViewModels en Android

Cómo mejorar tus ViewModels Cover photo

Como ya mencionamos en entradas anteriores los ViewModels son uno de los mejores componentes de Arquitectura que se han incorporado en Android Jetpack, aunque no el único. Sin embargo, estamos aquí para comprender cómo mejorar tus ViewModels en Android y dejaremos los demás componentes para otra ocasión.

A lo largo del último tiempo me he topado con algunas prácticas que no son muy productivas a la hora de utilizar ViewModel o LiveData que pueden no sólo desaprovechar las ventajas de los componentes en sí sino también traernos problemas indeseables en producción. Muchas veces son pequeños descuidos, muchas otras desconocimiento.

Hoy les traigo una pequeña recopilación de buenas prácticas que deberíamos implementar para poder evitarnos dolores de cabeza por un mal uso de estos componentes en nuestras aplicaciones. Los espero en los comentarios para poder discutirlas.

ViewPresenterModel

Uno de los errores más comunes (o al menos de los que he visto últimamente) es lo que me gustó llamar ViewPresenterModel. Generalmente lo solemos cometer cuando venimos del otro patrón de arquitectura muy utilizado en Android, el Model View Presenter o MVP (por sus siglas en inglés) y que tendemos a trasladar a ViewModel. Veamos un pequeño ejemplo sobre de qué se trata esto.

Si nuestra aplicación tiene un Activity principal el cual debemos cargar información desde una API externa (o precargar algo de nuestra base de datos local) y estamos utilizando MVP probablemente tengamos un llamado con el siguiente:

override fun onCreate(savedInstanceState: Bundle) {
    presenter.onCreate()
}

En este caso nuestro método está atado al ciclo de vida del Activity. En cada onCreate() intentaremos volver a cargar la información. Un error es hacer esto en nuestros ViewModels:

override fun onCreate(savedInstanceState: Bundle){
    viewModel.onCreate()
}

Al hacer esto estamos desaprovechando por completo una de las principales ventajas que tienen los ViewModels. Éstos sobreviven los cambios de configuración por lo que si rotamos la pantalla, volveríamos a hacer un pedido cargando la información nuevamente. Sabemos que al utilizar los ViewModel‘s junto a los LiveData‘s esa información queda guardada en ellos.

Entonces, ¿en qué momento hago el pedido?

Aquí tenemos dos posibles estrategias que podemos utilizar para realizar nuestra carga de datos:

ViewModel’s init

class MyViewModel() : ViewModel() {
    private val _myData = MutableLiveData<List<String>>()
    val myData: LiveData<List<String>> = _myData
    init {
       val data = getDataFromAPI()
       myData.value = data
    }
}

De esta manera nos aseguramos que el pedido se hace sólo la primera vez que se crea el ViewModel. Cada vez que se rote la pantalla, se volverá a emitir la info guardada en myData sin realizar nuevamente el pedido.

Declaración de la property

Otra de las posibilidades que tenemos es la de delegar en la property que utilizamos para nuestro ViewModel los pedidos de información aunque a decir verdad, prefiero la primer opción. En caso que estemos utilizando RxKotlin, también disponemos de LiveDataReactiveStreams para poder mapear directamente el LiveData al pedido que estemos realizando con Rx.

Ten cuidado con lo que observas

Otro error muy común a la hora de utilizar la estrategia de ViewModel + LiveData es cuando observamos los cambios de estos últimos. La cuestión está en qué LifecycleOwner utilizamos para observar. Veamos un ejemplo:

class MyFragment : Fragment {
   private val myViewModel by viewModels()
   override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      myViewModel.myData.observe(this, Observer { ... })
   }
}

En este caso estamos utilizando el Fragment como lifecycleOwner. Es decir, continuaremos observando myData según el ciclo de vida del Fragment. ¿Qué problemas podríamos tener? A simple vista ninguno. Sin embargo es recomendable utilizar el lifecycleOwner de la view asociada a ese Fragment ya que puede ocurrir que el Observer que definimos realice una acción sobre algún elemento de la View y esta no esté disponible y creamos que sí resultando en un problema.

Esto ocurre justamente debido a los distintos lifecycleOwner y los eventos del ciclo de vida que se utilizan para «empezar a escuchar» y «dejar de escuchar» en cada uno.

View vs Fragment

Si utilizamos el lifecycleOwner del Fragment estaremos utilizando los eventos ON_CREATE y ON_DESTROY para escuchar y dejar de escuchar las emisiones de los LiveData respectivamente.

En el caso del lifecycleOwner de la View asociada al Fragment, estaremos utilizando el evento ON_DESTROY para dejar de escuchar pero en aquellos casos en donde en realidad se esté ejecutando el onDestroyView(). En el onCreateView() es en donde se crea el LifecycleRegistry para esa View.

De aquí se desprende el caso en que actualizando algo de la vista en nuestro Observer tal cual lo tenemos definido podría incurrir en un crash. Esto ocurriría si nos llega el evento del LiveData después de haberse llamado al onDestroyView() pero no al onDestroy() del Fragment.

Es por esto que se recomienda utilizar el viewLifecycleOwner como podemos ver a continuación y así evitar intentar actualizar la vista cuando ella ya ha sido destruída:

override fun onCreate(savedInstanceState: Bundle?) {	 	 
super.onCreate(savedInstanceState)	 	 
myViewModel.myData.observe(viewLifecycleOwner, Observer { ... })	 	 
}

Deja tus Activities fuera de tus ViewModels

¿Has escuchado alguna vez sobre los memory leaks? Seguro que sí. Utilizando ViewModels es muy sencillo provocar uno si no se tiene cuidado. Podríamos decir que un memory leak ocurre cuando no es posible quitar de la memoria un recurso que ya no necesitamos porque existe algún objeto que todavía lo referencia y que no debería hacerlo. Estos leaks pueden ser temporales o no, dependiendo de cómo sea la lógica que estemos implementando.

Analicemos juntos la siguiente situación para más claridad. Tenemos un ViewModel el cual por alguna razón necesita que le pasemos un Context por lo que resolvemos hacerlo de la siguiente manera:

class MyActivity : FragmentActivity() {
    private lateinit var viewModel : MyViewModel
   override fun onCreate(savedInstanceState: Bundle) {
      ...
      viewModel = ViewModelProvider(this, createViewModel(this))[MyViewModel::java.class]
   private fun createViewModel(activity: Activity) = object : ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>) = MyViewModel(activity) as T
    }
}

Resultando la definición de MyViewModel como vemos a continuación:

class MyViewModel(private val context: Context) : ViewModel() {
   ...
}

Si observamos con atención vemos que estamos utilizando para proveer al ViewModel de un contexto el Activity en el cual lo utilizamos. Estos ViewModels tienen la característica de sobrevivir a los cambios de configuración y como bien sabemos, un cambio de configuración implica una reconstrucción del Activity por completo.

Entonces, dado el escenario planteado si provocamos un cambio de configuración, el Activity se va a destruir y volver a recrear obteniendo esta nueva instancia de Activity la misma instancia de ViewModel. Como la instancia de ViewModel no cambió y la property context tenía una referencia al Activity anterior, éste no podrá ser recolectado quedándonos una instancia de Activity «de más» que no podrá ser recolectada por el Garbage Collector mientras viva MyViewModel.

Recomendación

La mayoría de las veces es el applicationContext el que necesitamos para comunicarnos con los servicios de Android y el que debemos usar a fin de evitarnos estos problemas.

Nuestros ViewModels son un poco más complejos que el ejemplo que vimos anteriormente, sin embargo debemos tener en cuenta que algunos otros posibles encubrimientos de la mala utilización del context.

A través de una interfaz:

class MyViewModel(private val view: MyView)
interface MyView
class MyActivity: FragmentActivity(), MyView

Si utilizamos UseCases, Interactors, Actions, etc con acceso a repositorios o servicios:

class MyViewModel(private val getMyData: GetMyData)
class GetMyData(private val repository: MyDataRepository)
interface MyDataRepository
class RetrofitMyDataRepository(context: Context): MyDataRepository
class MyActivity: FragmentActivity() {
   val viewModel = MyViewModel(GetMyData(RetrofitMyDataRepository(this)))
}

Conclusión

Siempre está bueno innovar y comenzar a utilizar cosas nuevas a medida que las tecnologías van apareciendo y mejorando. A su vez, es clave que comprendamos cómo funcionan en realidad a fin de evitarnos problemas futuros debido a malas implementaciones. Aquí mencionamos algunos casos que pueden resultar poco conocidos o bien, se nos pueden haber escapado y que puedes utilizar para revisar y mejorar tus aplicaciones. Es tu turno de ponerlos en práctica.

Sigamos conectados 😄
Happy Coding! ✍️

Cover Photo by Jonathan Kemper on Unsplash

12

Comentarios

  1. Jen

    Gracias por escribir este artículo. Me resultó muy útil.

    (Un comentario pequeño, podrías cambiar el color del texto del código para que sea más oscuro? Con el fondo gris y el texto en blanco, he tenido que subrayar los ejemplos para verlos.)

    1. Autor de la
      Entrada

Deja un comentario