LiveData: todo sobre el componente de arquitectura Android

,

LiveData es quizás el componente más necesario y útil de Android Architecture Components. Se trata de una envoltura observable para nuestros datos con la ventaja de que está asociado a un ciclo de vida (de un fragment o activity).

En este post veremos con detalle cuáles son sus ventajas y qué usos podemos darle.

Ventajas

  • Tu UI siempre mostrará los datos más actualizados. LiveData no deja de ser un Observable vitaminado. Si construimos nuestra UI para que recoja la información a mostrar observando objetos LiveData, cualquier cambio que se produzca en estos datos será transmitido a cualquier vista o componente que lo esté observando. Esto significa que siempre se mantendrán actualizados.
  • No más errores ni código defensivo para evitar activities/fragments inactivos. Como LiveData está asociado al ciclo de vida de la activity o fragment, no se producirán llamadas a sus observables si la vista no está activa.
  • Tus datos siempre actualizados. Si la vuelta de un servicio ocurre cuando nuestra activity está en segundo plano, ese valor se almacena en el objeto LiveData. Además, en cuanto esa activity vuelva a estar activa, lo notificará inmediatamente a los observadores relacionados.
  • Evita Memory Leaks. Los observadores están asociados a un objeto con ciclo de vida. En el momento que ese objeto es destruido, los observadores se liberan de memoria.

Usos

La clase LiveData es un objeto abstracto que no podemos instanciar como tal. Para la creación de estos objetos usaremos la clase MutableLiveData la cual ya implementa todo lo necesario. Declaramos ese objeto en nuestro ViewModel, Repositorio o incluso en un Presenter de esta forma:

private final MutableLiveData mUserInfoLiveData = new MutableLiveData<>();

Nota: Recomiendo encarecidamente el salto a ViewModel, tiene muchas más ventajas que un Presenter al uso.

Para añadir o modificar un valor se usan los métodos setValue (si estás en el hilo de UI) o postValue (si estás en segundo plano). La diferencia entre ellos es que setValue alerta a los observadores inmediatamente, y postValue crea un Task para alertarlos en UI. En este segundo caso, si cambiamos los valores muy rápidamente, los observadores podrían recibir solo el último valor. En la mayoría de casos no nos importa, pero si necesitamos leer todos los valores que se emitan, es mejor utilizar setValue aunque tengamos que cambiar de hilo.

User user = ws.getUserInfo();
mUserInfoLiveData.setValue(user); // Si estamos en UI
mUserInfoLiveData.postValue(user); // Si estamos en segundo plano

Ya tenemos nuestro LiveData y estamos actualizando su valor, sólo nos queda que la vista reaccione a sus cambios.

El primer paso es exponer este objeto con un getter. Una buena práctica que nos ayuda a limitar la modificación de ese MutableLiveData a la propia clase en la que se encuentra es devolverlo como LiveData. Esto es así porque los métodos setValue y postValue no están implementados en, por lo que no podrán modificarse fuera de esta clase. Si necesitamos que se pueda modificar, es mejor añadir una función que lo actualice antes que exponerlo como MutableLIveData:

public LiveData getUser(){
    return mUserInfoLiveData;
}

public void updateUser(User userUpdated){
    mUserInfoLiveData.postValue(userUpdated);
}

Una vez que tenemos los getters, vamos a usarlos para observar desde la vista. Para esto se usa el método observe, al que hay que pasarle un objeto con ciclo de vida y un observador. Este observador se asociará con ese ciclo de vida, de forma que se alertará cuando esté activo y se destruirá cuando el ciclo de vida también se destruya.

ViewModel viewModel = ViewModelProviders.of(this).get(UserViewModel.class);
viewModel.getUser().observe(this, new Observer() {
    @Override
    public void onChanged(@Nullable User user) {
       if(user != null){
          mName.setText(user.getName());
          mEmail.setText(user.getEmail());
       }
    }
)

Resource

Hasta aquí todo parece muy bonito, pero te debes estar planteando una serie de dudas. ¿Qué pasa si ocurre un error al obtener el usuario? ¿Cómo se entera la vista en ese caso? Además, al ser una llamada WS, es recomendable dar feedback de esa petición con un loader, ¿cómo se entera la vista?

Hay varias soluciones a estos planeamientos, desde tener más objetos LiveData en el ViewModel, como por ejemplo MutableLiveData mErrorLiveData o MutableLiveData mLoadingLiveData. Pero esto nos parece rizar el rizo, y al final se empieza a hacer una bola de nieve difícil de manejar.

Desde Google, en uno de sus ejemplos, han usado una clase Resource para envolver al dato en cuestión y manejar estos estados. Adaptando el ejemplo anterior quedaría de la siguiente forma:

View model

private final MutableLiveData> mUserInfoLiveData = new MutableLiveData<>();

public UserViewModel(){
   mUserInfoLiveData.postValue(Resource.loading());
   User user = ws.getUserInfo(new WsCallback(){
       @Override
       public void onSuccess(User user) {
          mUserInfoLiveData.postValue(Resource.success(user));
       }

       @Override
       public void onError(String errorMessage) {
          mUserInfoLiveData.postValue(Resource.error(errorMessage));
       }
   });
}

public LiveData> getUser(){
    return mUserInfoLiveData;
}

public void updateUser(User userUpdated){
    mUserInfoLiveData.postValue(Resource.success(userUpdated));
}

Fragment

ViewModel viewModel = ViewModelProviders.of(this).get(UserViewModel.class);
viewModel.getUser().observe(this, new Observer>() {
    @Override
    public void onChanged(@Nullable Resource resource) {
       if(resource != null){
           switch (listResource.status){
              case SUCCESS:
                 setLoading(false);
                 mName.setText(resource.data.getName());
                 mEmail.setText(resource.data.getEmail());
                 break;
              case ERROR:
                 setLoading(false);
                 showErrorMessage(resource.message);
                 break;
              case LOADING:
                 setLoading(true);
                 break;
           }

       }
    }
);

Integración de LiveData con otras galerías

  • Room. Para las consultas se puede envolver los datos de retorno directamente con un LiveData. De esta forma, Room se encarga tanto de la creación del objeto LiveData como de su actualización en tiempo real ante cualquier cambio en esa consulta.
  • Data Binding. El soporte para LiveData se encuentra desde la versión 3.1 de Android Studio, concretamente desde Android Studio 3.1 Canary 6. Desde esta versión se añade soporte para que nuestro layout se actualice automáticamente si bindeamos con objetos LiveData.

LiveData: un must

Si has leído hasta aquí, ya lo sabes todo para empezar a usarlo. No hay excusa que valga ya que la curva de aprendizaje es mínima. Te aseguro que es un verdadero gustazo salir del Callback-Hell y manejar todos los datos de nuestra UI con observables.

Además, nuestro código queda mucho más simple y fácil de entender. Esto hace más sencillo cualquier cambio en la clase al tiempo que de reducir la aparición de errores.

¡¡Corred a implementarlo, insensatos!!

Android Team
ALTEN