Android ViewModel

El componente ViewModel de Android cumple dos funciones: se encarga de la preparación y administración de los datos relacionados con la vista y maneja la comunicación de la vista con el resto de la aplicación (dominio, datos, etc.).

Desde el punto de vista de arquitectura la clase ViewModel está diseñada como una plantilla para crear modelos de vista en el patrón MVVM.

ViewModel en una arquitectura simple de una App Android
ViewModel en una arquitectura de App Android

Los cuales se responsabilizan de manejar las interacciones entre la vista y el modelo (o capas adicionales).

¿Por qué usarlo?

Porque mejora el testing y la eficiencia de mantenimiento de tus casos de uso al aislar responsabilidades de tus controladores de UI (actividades y fragmentos).

¿Cómo funciona en cambios de configuración?

Asegura el guardado del estado en tu UI (Scope) cuando hay cambios de configuración como una rotación de pantalla. Ya que cuando una actividad es recreada el framework reconecta automáticamente al ViewModel a la instancia.

¿Puede relacionarse con varias vistas?

Si, permite compartir los mismos datos entre diferentes vistas. Esto debido a que este no tiene dependencia directa con las mismas.


Implementar ViewModel

Ejemplo: App Adopción De Mascotas

Para explicar el uso del componente ViewModel he creado una pequeña App que muestra una colección de mascotas y su detalle.

Puedes descargarla para ver el resultado final y ver en detalle como se implementaron los códigos que mencionaré en los apartados posteriores.

Añadir Dependencia Gradle en Android Studio

Usar el componente ViewModel requiere agregar a build.gradle la siguiente dependencia:

implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycleVersion"

Puedes ver la versión más reciente en la página de notas de releases para lifecycle

Declarar Objetos LiveData

Antes de crear tu componente view model debes pensar en los datos que quieres conservar de la vista.

En el caso de nuestro ejemplo, sabemos que deseamos mostrar la lista de mascotas y el detalle del ítem seleccionado.

Como vimos en el tutorial de Android LiveData, añadimos un objeto de este tipo por cada dato a rastrear y métodos get*() para exponerlos.

public class PetsViewModel extends ViewModel {
    
    private PetsRepository mPetsRepository;

    private final MutableLiveData<Boolean> mSync = new MutableLiveData<>(false);

    private final LiveData<List<Pet>> mPets = Transformations.switchMap(mSync,
            new Function<Boolean, LiveData<List<Pet>>>() {
                @Override
                public LiveData<List<Pet>> apply(Boolean input) {
                    if (input) {
                        mPetsRepository.refreshPets();
                    }
                    return mPetsRepository.observePets();
                }
            });

    private final MutableLiveData<String> mPetId = new MutableLiveData<>();

    private final LiveData<Pet> mPetById = Transformations.switchMap(
            mPetId, petId -> mPetsRepository.observePet(petId));

    private final MutableLiveData<Event<String>> mOpenPetEvent = new MutableLiveData<>();


    /**
     * Inicializar view model
     */
    public PetsViewModel(PetsRepository repository) {
        mPetsRepository = repository;
    }

    

    public MutableLiveData<Event<String>> getOpenPetEvent() {
        return mOpenPetEvent;
    }

    public LiveData<List<Pet>> getPets() {
        return mPets;
    }

    public LiveData<Pet> getPetById() {
        return mPetById;
    }

}

Obtener la lista de mascotas depende de un booleano (mSync) que determina si se refrescan los datos o no.

Y la mascota del detalle (mPetById) cambia al variar el ID seleccionado por el usuario (mPetId).

Ahora, nos preguntamos:

¿Qué acciones provocan una comunicación de mi vista a mi view-model?

  • Iniciar fragmento de lista > Cargar mascotas
  • Iniciar fragmento de detalle > Cargar mascota por ID
  • Click en ítem e lista > Navegar a detalle

Representamos cada evento con un método que notifique a los observadores.

public class PetsViewModel extends ViewModel {
    

    public void loadPets(boolean pull) {
        mSync.setValue(pull);
    }

    public void loadPetDetail(String petId) {

        // Si ya se cargó la mascota, retornar
        if (petId.equals(mPetId.getValue())) {
            return;
        }

        mPetId.setValue(petId);
    }

    public void openPet(String petId) {
        // Notificar acción de navegación al detalle
        mOpenPetEvent.setValue(new Event<>(petId));
    }

   
}

Normalmente crearás un ViewModel por cada vista, pero debido a que usaremos un ejemplo de dos paneles, compartiremos uno para los dos fragmentos con el fin de probar la comunicación.

Pasar parámetros al constructor

El paquete androidx.lifecycle tiene la clase ViewModelFactory, la cual genera instancias de nuestros ViewModels en caso de que requieran construcción con parámetros.

Por ejemplo, en nuestra app de mascotas necesitamos pasar el repositorio.

Pasarlo como argumento al constructor requiere que declaremos una clase que extienda de ViewModelProvider.NewInstanceFactory. Al implementarla sobrescribiremos el método create() para que conecte el parámetro en el constructor del ViewModel:

public static class Factory extends ViewModelProvider.NewInstanceFactory{

        private final PetsRepository mRepository;

        public Factory(PetsRepository mRepository) {
            this.mRepository = mRepository;
        }

        @NonNull
        @Override
        public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
            return (T) new PetsViewModel(mRepository);
        }
    }

Asociar ViewModel A La Vista Con ViewModelProvider

La clase ViewModelProvider se encarga de entregar las instancias de los ViewModels a la activity/fragment con el fin de generar el vínculo de asociación.

En el método onCreate() de la actividad (o en onCreateView() del fragmento) construiremos un proveedor y luego llamaremos al método get() pasando a la actividad como ViewModelStoreOwner (objeto con capacidades de guardar view models).

Por ejemplo, para el fragmento de lista de mascotas creamos la factory con el parámetro y luego lo pasamos al proveedor:

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Binding
        mDataBinding = DataBindingUtil.inflate(inflater,
                R.layout.pets_frag, container, false);

        // ViewModel
        PetsViewModel.Factory factory =
                new PetsViewModel.Factory(DefaultPetsRepository.getInstance());
        mViewModel = new ViewModelProvider(requireActivity(), factory).get(PetsViewModel.class);

        // Conexión
        mDataBinding.setViewModel(mViewModel);


        return mDataBinding.getRoot();
    }

Usar ViewModel En La UI

Una vez creado tu objeto ViewModel ya es posible procesar las interacciones con la vista.

Ya sea la vista ordenando al ViewModel realizar una acción: como cargar el detalle:

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // ...

        // Iniciar carga de detalle
        mViewModel.loadPetDetail(mPetId);

        return mDataBinding.getRoot();
    }

O el ViewModel detectando cambios de datos (observe()) y actualizando la vista: como setear las mascotas al adaptador:

    private void setupListAdapter() {
        // Lista
        PetsAdapter adapter = new PetsAdapter(mViewModel);
        int gutter = getResources().getDimensionPixelSize(R.dimen.grid_gutter);
        mDataBinding.list.addItemDecoration(new GutterDecoration(gutter));
        mDataBinding.list.setAdapter(adapter);
        mViewModel.getPets().observe(
                this,
                pets -> {
                    adapter.setPetList(pets);
                    if (mViewModel.isTwoPane()) {
                        // Default: Cargar detalle por posición activa, si hay dos paneles
                        mViewModel.openPet(pets.get(mViewModel.getActivatedItem()).getId());
                    }
                });
    }

Compartir ViewModel Entre Fragments

Un objeto ViewModel puede ser compartido por varios fragmentos asociados a una actividad. Evitándonos crear interfaces de comunicación y definiendo a la actividad como el alcance intermediario.

Debido a que usamos la actividad como alcance, obtenemos la referencia al ViewModel a través de requireActivity() con el ModelViewProvider en los fragmentos:

public class PetsActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //...
        mViewModel = new ViewModelProvider(this, factory).get(PetsViewModel.class);

    }
}

public class PetsFragment extends Fragment {

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        //...
        mViewModel = new ViewModelProvider(requireActivity(), factory).get(PetsViewModel.class);
    }
}

public class PetDetailFragment extends Fragment {

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        //...
        mViewModel = new ViewModelProvider(requireActivity(), factory).get(PetsViewModel.class);
    }
}

Debido que todos estos controladores de UI están conectados al mismo ViewModel, es posible seleccionar un ítem de la lista y actualizar el panel de detalle en caso de que estemos en un tablet.

Por ejemplo: Con esta relación también podemos saber desde cualquier fragmento si nos encontramos en una densidad de dos paneles (PetsViewModel::isTwoPane()).

Guardar Estado De UI

Si has identificado que por experiencia de usuario es necesario retener un estado de tu UI incluso cuando el sistema cierra el proceso de tu app, entonces puedes usar el módulo saved instance.

Veamos cómo hacerlo.

Mantener Item Seleccionado

Haz de cuenta que estamos ejecutando la app de mascotas en una tablet y seleccionamos la mascota «Zeus» del grid:

Selección de mascota en el Grid

Luego presionamos el botón de Home simulamos que Android mata el proceso con el siguiente comando en la terminal (Android P+):

adb shell am kill com.herpro.viewmodel

¿Qué sucedería?

Pérdida del estado para la mascota activa

El ítem activo se desvanecería y se reiniciaría al primer elemento.

Partiendo de dicha situación usaremos el módulo Saved State para evitar la pérdida del estado de UI.

La Clase SaveStateHandle

El módulo Saved State permite escribir ViewModels que guardan su estado de UI cuando el proceso muere, y lo restauran cuando el proceso es reiniciado.

Para usarlo incluimos su dependencia:

implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycleVersion"

Luego añadimos un campo del tipo SavedStateHandle al ViewModel y lo recibimos en el constructor:

public class PetsViewModel extends ViewModel {

    // Conector para guardar estados
    private final SavedStateHandle mState;


    /**
     * Inicializar view model
     */
    public PetsViewModel(SavedStateHandle savedStateHandle, PetsRepository repository) {
        mState = savedStateHandle;
    }
}

SavedStateHandle se basa en pares clave-valor para almacenar los estados. La asignación se logra con set() y setLiveData(); Y la obtención con get() y getLiveData().

Teniendo en cuenta lo anterior creamos a setActivatedItem() y getActivatedItem() para asignar/leer el estado cuando lo necesitemos:

    public void setActivatedItem(int position) {
        mState.set(ACTIVATED_PET_KEY, position);
    }

    public int getActivatedItem() {
        if (mState.contains(ACTIVATED_PET_KEY)) {
            return mState.get(ACTIVATED_PET_KEY);
        } else {
            return 0;
        }
    }

Lo que sigue es heredar nuestra Factory de la clase AbstractSavedStateViewModelFactory, ya que estamos usando un constructor con parámetros.

    public static class Factory extends AbstractSavedStateViewModelFactory {

        private final PetsRepository mRepository;

        public Factory(@NonNull SavedStateRegistryOwner owner,
                       @Nullable Bundle defaultArgs,
                       PetsRepository mRepository) {
            super(owner, defaultArgs);
            this.mRepository = mRepository;
        }

        @SuppressWarnings("unchecked")
        @NonNull
        @Override
        protected <T extends ViewModel> T create(@NonNull String key,
                                                 @NonNull Class<T> modelClass,
                                                 @NonNull SavedStateHandle handle) {
            return (T) new PetsViewModel(handle, mRepository);
        }
    }

Finalmente modificamos el uso del constructor de la Factory en nuestros controladores de UI asignando así:

public class PetsFragment extends Fragment {

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Binding
        mDataBinding = DataBindingUtil.inflate(inflater,
                R.layout.pets_frag, container, false);

        // ViewModel
        PetsViewModel.Factory factory = new PetsViewModel.Factory(
                requireActivity(),
                null,
                DefaultPetsRepository.getInstance()
        );
        mViewModel = new ViewModelProvider(requireActivity(), factory).get(PetsViewModel.class);

        mDataBinding.setLifecycleOwner(getViewLifecycleOwner());

        return mDataBinding.getRoot();
    }

}

Al correr la App veremos que si matamos el proceso manualmente y la restauramos esta mantendrá el ítem activo.

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!