BasicNavigationDrawer – ViewModel – LiveData

ViewModel

En esta entrada vamos a implementar, a partir de BasicNavigationDrawer – ActivityData – RecyclerView ItemClickListener at Fragment dos aplicaciones.

Pulsar en las imágenes animadas para verlas en grande.

ViewModelLive
  • Primero vamos a sustituir el dato compartido alojado en ActivityData por un objeto ViewModel compartido entre la Activity y sus Fragments.
    Veremos cómo operar con el ViewModel y como nos facilita la cosa eliminando los callbacks de propagación de eventos.
    Como en esta versión utilizamos un ViewModel y hemos eliminado la propagación de eventos, no se actualiza el textview chivato que indica el color seleccionado (ver la animación)
    Otra ventaja de usar ViewModels es que los cambios persisten a la rotación de manera automática sin hacer nada. La versión anterior no lo hacer, comprobarlo.
  • Luego vamos a modificar el ViewModel para modificar el dato compartido para que sea de la clase LiveData lo que nos permitirá establecer observadores sobre el dato.
    Ahora si, por un lado nos libramos de los callbacks de propagación de eventos y por otro el chivato se actualiza automáticamente, gracias a que tenemos un observador que es puesto en ejecución cuando cambia el dato observado.

ViewModel

El componente de la arquitectura Android ViewModel está explicado en Descripción General de ViewModel

Un ejemplo sencillo de implementación de ViewModel muy ilustrativo podéis encontrarlo en Medium in Android Developers – ViewModels : A Simple Example

Se diseñó la clase ViewModel a fin de almacenar y administrar datos relacionados con la Interfaz de Usuario de manera optimizada para los ciclos de vida.
Es decir, la clase ViewModel permite que se conserven los datos aunque se realicen cambios de configuración, como las rotaciones de pantallas, de manera automática.

Para cada Fragment o Activity podemos crear una clase ViewModel que guardará sus datos.

La clase ViewModel se puede crear a partir de una ViewModelFactory. El objeto Application ya cuenta con una, la default ViewModelFactory. Podemos usar esa default ViewModelFactory siempre que definamos en nuestro ViewModel un constructor por defecto sin parámetros. Cuando queramos enviar parametros al constructor del ViewModel, entonces tendremos que definir nuestra propia Factory.

La mayoría de código que se encuentra sobre ViewModel en Java está obsoleto, pero podemos encontrar ejemplos en Kotlin que pueden ayudar.
No obstante aquí lo trataremos en Java.

Para ver un ejemplo donde se utilizan las factory (default y custom) con la sintaxis correcta en Java, podemos ver la siguiente aplicación, que he extendido de GitHub juanricardorc/viewmodel-providers-of y que podéis descargar de mi implementación de GitHub ViewModel-Providers-of. Es importante hacer notar que mucha de la información que se encuentra por ahí todavía no se ha actualizado pues ViewModel.Providers.of está deprecated, pero en este ejemplo se explica como se implementa. Tener en cuenta que se puede hacer de varias formas, y por eso se comentan alguna de ellas, ir comentando y descometando para verlo con detalle.

Para poder trabajar con ViewModel y con LiveData tenemos que instalar las dependencias de extensión del ciclo de vida, en la imagen vemos la version 2.2.0

El esquema

En la imagen podemos ver el esquema de la implementación de la primera versión. Os aconsejo que comparés este esquema con el que vimos en BasicNavigationDrawer – ActivityData – RecyclerView ItemClickListener at Fragment.

Tenemos una clase llamada SharedViewModel que extiende de ViewModel. La primera vez que se instancia esta clase se hace en el onCreate de la Activity. Las siguientes veces que se instancie, la Factory detectará que ya había una instancia de esta clase y nos devolverá una referencia al objeto sharedViewModel ya creado. Esto es muy importante puesto que es el kit de la cuestión de la compartición del ViewModel entre Activities y Fragments. Además como se instancia por primera vez en la Activity, se vincula a su ciclo de vida pero como se explica en Descripción General de ViewModel no se pierden los datos aunque se produzcan rotaciones. Cuando la Activity se destruya (definitivamente), también lo hará el sharedViewModel.

Tenemos que tener en cuenta que si la Factory desde la que se instancia el ViewModel es distinta, entonces no encontrará el ViewModel previo y nos da otra instancia. Nosotros en el ejemplo usaremos siempre la default Factory. Lo vemos luego.

El dato que vamos a compartir es como en la otra aplicación, el color que ha sido seleccionado por el usuario, selectedColor que era un Pair<String, Integer> (nombre de color y valor de color)

Nuestra clase SharedViewModel en el esquema tiene <T> como dato por simplicidad, en el ejemplo T es realmente Pair<String, Integer>. Vemos también que se han declarado tres funciones, el constructor sin parámetros, una función getSelectedColor() y una función setSelectedColor. Por tanto una vez que tengamos acceso al ViewModel compartido, nuestro sharedViewModel podremos usarlas.

  • En el onCreate de la activity pedimos una instancia del SharedViewModel a la Factory, que como no existe lo crea y nos devuelve una instancia nueva.
  • En el Constructor del Adapter para nuestro RecyclerView pedimos una instancia del SharedViewModel a la Factory, que como ya existe nos devuelve la instancia común.
  • En el onCreate del FragmentA (PreferenciasFragment) y en el onCreate del FragmentB (WellcomeFragment) pedimos a la factory una instancia del SharedViewModel, recibimos el común.

Como vemos en los tres sitios tenemos la misma instancia del objeto sharedViewModel y desde ella tenemos acceso a sus métodos set() y get()

Como es la misma instancia, cuando modifico el dato en cualquiera de los tres sitios automáticamente está disponible en los otros. Esto nos permite eliminar los callbacks de propagación de eventos.

Pero si ejecutáis la primera versión, veréis que tras seleccionar un color el textView chivato del color seleccionado no cambia, pero al salir e ir al principio (WellcomFragment) este ya ve el cambio y al volver a entrar en Preferencias el chivato si marca el color que habíamos seleccionado. Esto pasa porque el evento onClick lo recibimos de Android en el ViewHolder y desde allí modificamos el objeto ViewModel, pero desde el ViewHolder no tenemos acceso al textView chivato que está ubicado en el FragmentA (PreferenciasFragment). Para que cambiase de color no tendríamos más remedio que crear un Interfaz y propagar el evento en un callback en el FragmentA, pero eso es lo que queremos evitar.

LiveData

LiveData es una clase abstracta que no tiene implementación, nosotros usaremos MutableLiveData (hija de LiveData).

En Descripción general de LiveData podemos encontrar la documentación del componente de arquitectura LiveData.

Básicamente nos permite establecer observadores del dato que queremos saber si cambia, porque cuando cambie, se pondrá en ejecución el código que definamos en el observador. Revisar la documentación para los detalles.

El esquema

En la segunda aplicación, ya hacemos funcionar el click en el RecyclerView de forma que se actualiza el textView chivato del color seleccionado.

La diferencia en la clase SharedViewModel es que hemos definido el dato a compartir, color (selectedColor en el código), como un objeto de tipo MutableLiveData, lo que permite establecer observadores sobre el.

Un Observer es un objeto que en su método onChanged nos permite poner el código a ejecutar cuando el dato observado es modificado por alguien. En nuestro ejemplo en cualquier instancia que se modifique el dato hará que en aquellos sitios donde hayamos puesto al observador se ejecute el código asociado al callback onChanged().

El evento onClick de selección del RecyclerView se captura en el ViewHolder del Adapter. Desde ahí haemos uso de la función setSelectedColor de nuestro SharedViewModel. Esta función, setSelectedColor() llama a la función setValue() que tiene el objeto selectedColor por ser un MutableLiveData y le pasa el color que determinamos ha seleccionado el usuario.

Para solucionar el problema de que no se actualizaba el chivato, simplemente ponemos en el onViewCreated un observador del dato selectedColor de nuestro sharedViewModel. Automáticamente se recibe el callback onChanged() y en el actualizamos el textView chivato.

Revisando el código, veréis que en el código aparece una forma comentada de definir el Observer(), que no utiliza el callback onChanged() explicitamente, pero si se define implicitamente. Esta forma abreviada hace uso de una expresión Lambda para definir el código a ejecutar asociado al dato a observar.

Default ViewModel Factory

En aquellos sitios donde me interese instanciar el ViewModel compartido, lo primero que tengo que hacer es tener una referencia al Factory con el que se creó la primera instancia del objeto ViewModel a compartir.

En el caso más general, donde no tendremos parámetros para un ViewModel (en su constructor) haremos uso del siguiente código par acceder a la referencia del default ViewModelFactory que se encuentra en el contexto de la Application.

Por ejemplo en la MainActivity, instanciamos por primera vez el sharedViewModel y por tanto la Factory lo crea:

------------- En la MainActivity declaramos la Factory y el SharedViewModel a compartir
    // Referencia a la default Factory de la App, a usar cuando el ViewModel no recibe parámetros y usando su constructor por defecto
    private ViewModelProvider.AndroidViewModelFactory theAppFactory;

    // Declaramos una referencia para el ViewModel de SharedViewMoel.
    private SharedViewModel sharedViewModel;


------------- En el onCreate() de la Activity por ejemplo
        // Sin Factory, cogiendo la Factory del objeto App.
        theAppFactory = ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication());
        sharedViewModel = new ViewModelProvider(this, (ViewModelProvider.Factory) theAppFactory).get(SharedViewModel.class);

Después en los Fragments que quieran tener acceso a los datos compartidos en sharedViewModel, también tenemos que instanciar la default Factory de Application y luego acceder a través de el al sharedViewModel:

--------------- En el Fragment declaramos la Factory y el SharedViewModel
    // Referencia a la default ViewModelFactory de la App, a usar cuando el ViewModel no recibe parámetros y se usa su constructor por defecto
    private ViewModelProvider.AndroidViewModelFactory theAppFactory;

    // Declaramos una referencia para el ViewModel de Preferencias.
    private SharedViewModel sharedViewModel;

--------------- En el onCreate() del Fragment 
        // Sin Factory, cogiendo la devault ViewMOdelFactory del objeto Application.
        theAppFactory = ViewModelProvider.AndroidViewModelFactory.getInstance(getActivity().getApplication());
        sharedViewModel = new ViewModelProvider( requireActivity(), (ViewModelProvider.Factory) theAppFactory).get(SharedViewModel.class);


---------------- Y lo usamos donde corresponda con
        //Utilizando un nuevo objeto Observer y sobrecargando el método onChanged
        sharedViewModel.getSelectedColor().observe(this, new Observer<Pair<String, Integer>>() {
            @Override
            public void onChanged(Pair<String, Integer> selectedColor) {
                TextView tvSelectedColor = view.findViewById(R.id.tvSelectedColor);

                tvSelectedColor.setBackgroundColor(selectedColor.second);
                tvSelectedColor.setText(selectedColor.first.toString());

                if (selectedColor.second == Color.BLUE || selectedColor.second == Color.BLACK){
                    tvSelectedColor.setTextColor(Color.WHITE);
                }
                else tvSelectedColor.setTextColor(Color.BLACK);
            }
        });

Conclusión

Hemos presentado cómo utilizar un ViewModel compartido, con y sin LiveData.

Revisar el código de los ejemplos para entender la implementación de los detalles que se han comentado.