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.
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:
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?
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.