En este tutorial te explicaré cómo funciona el componente de arquitectura LiveData en tus Apps Android. La idea es presentar el fundamento conceptual del componente y luego crear un proyecto de Ejemplo.
Consulta el índice en la siguiente tabla si deseas dirigirte a un lugar específico:
Android LiveData
La clase abstracta LiveData representa una envoltura para datos que deseas observar dentro de un ciclo de vida.
Esta clase empareja un objeto LifecycleOwner
(ver ciclos de vida) con otro Observer
a fin de que el observer notifique datos solo si el owner está en estado activo (Lifecycle.State.STARTED
y Lifecycle.State.RESUMED
).
Dicho comportamiento es mucho más poderoso que los Campos Observables si deseamos evitar la revisión de ciclos de vida.
De esta forma, al usar LiveData
podremos:
- Evitar leaks de memoria al dejar de recibir cambios de datos cuando el ciclo de vida se encuentra en inactividad
- Mantener la UI actualizada al cambiar los datos (la vista recibe actualizaciones de estado en vez de solicitarlas)
- Recibe los datos más nuevos incluso en cambios de configuración
Teniendo en cuenta esto, pasemos a comprender los pasos para incluir el componente en nuestro proyecto Android Studio y como observar las variables para actualizar nuestros views.
Como Usar Objetos LiveData
Los siguientes son los pasos básicos que debes seguir para recibir actualizaciones de un dato para disponer de actualizaciones de UI y procesamiento de eventos.
1. Declarar Dependencia Gradle Para LiveData
Usa la siguiente línea para incluir en tus proyectos la funcionalidad de LiveData
:
// LiveData
implementation 'androidx.lifecycle:lifecycle-livedata:2.2.0'
Recuerda usar la versión más reciente del componente lifecycle. En mi caso es la 2.2.0
.
2. Crear Objetos LiveData
LiveData puede rastrear desde tipos elementales como Integer
o String
hasta colecciones y objetos personalizados.
El lugar donde serán declarados estos objetos normalmente será el ViewModel
asociado al controlador de UI que sea LifecycleOwner
.
Ejemplo:
public class LoginViewModel extends ViewModel {
private MutableLiveData<String> user;
private MutableLiveData<String> password;
public MutableLiveData<String> getUser() {
if (user == null) {
user = new MutableLiveData<>();
}
return user;
}
public MutableLiveData<String> getPassword() {
if (password == null) {
password = new MutableLiveData<>();
}
return password;
}
// ...
}
Como ves, los Strings del usuario
y password
son envueltos con la subclase MutableLiveData<T>
(en la actualización veremos su rol) dentro del correspondiente ViewModel
.
Adicionalmente exponemos sus valores con métodos get*()
con el fin de observarlos desde la vista o un layout con Data Binding.
3. Observar Objetos LiveData
Normalmente (a menos que tu caso de uso lo disponga diferente) declararemos la observación en los métodos onCreate()
de los LifecycleOwners
para conseguir el valor lo más pronto posible del LiveData (STARTED).
La idea es llamar al método observe()
y pasar la pareja owner-observador. El controlador Observer.onChanged()
nos pasará el valor más reciente de la variable rastreada y podremos aplicar las acciones correspondientes.
Ejemplo:
public class LoginActivity extends AppCompatActivity {
private LoginViewModel model;
private TextInputLayout mPasswordInputLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ...
// Crear view model.
model = new ViewModelProvider(this).get(LoginViewModel.class);
// Observar pasando el par owner-observer
model.getPassword().observe(this, new Observer<String>() {
@Override
public void onChanged(String password) {
mPasswordInputLayout.setError(
password.length() < 6 ? "Son mínimo 6 caracteres" : null);
}
});
}
}
En la actividad de ejemplo anterior obtenemos el valor del password y le aplicamos observe()
para setear un error basados en el último valor del String
que onChanged()
nos notifica.
4. Actualizar Valores De Objetos LiveData
Debido a que LiveData
es abstracta nace la subclase MutableLiveData
para que podamos actualizar el contenido de nuestros datos y alertar al observador.
La actualización se realiza con los métodos setValue()
y postValue()
. El primero modifica el valor en el hilo principal directamente y el segundo asigna el valor si el código está un hilo de trabajo diferente.
Ejemplo:
// Actualizar password por cada edición
mPasswordTextField.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editable) {
model.getPassword().setValue(editable.toString());
}
});
La actualización del LiveData se da cuando el cambio es escuchado por el TextWatcher asociado al EditText
del password.
Al llamar a setValue()
en cada edición se llegará a onChanged()
y aplicaremos la validación del error que definimos antes.
Transformar Objetos LiveData
Las transformaciones (Transformations
) nacen de querer representar el valor de salida de un LiveData
con otra interpretación o hacer que un LiveData
reemplace la observación de otro.
map()
El método map(source, mapFunction)
transforma cada ítem del LiveData
pasado como fuente (source
) aplicando una función definida (mapFunction
).
Como resultado tendrás otro LiveData
que emite los valores finales producidos por la función.
Por ejemplo:
private MutableLiveData<List<String>> items = new MutableLiveData<>();
private LiveData<String> itemsNumber =
Transformations.map(items, x -> String.valueOf(x.size()));
El anterior código se usa una lista de items como fuente y la conversión a String
de su tamaño como función.
La salida se asigna al LiveData
llamado itemsNumber
que estará notificando el número de artículos cuando la lista cambie.
switchMap()
El método switchMap(source, switchMapFunction)
cancela la observación del LiveData
previo (resultado) y suscribe una nueva para el siguiente producto de la función switchMapFunction
.
Es decir, cambiamos la observación hacia el nuevo LiveData
y nos despreocupamos del anterior, ya que no emitirá actualizaciones.
Por ejemplo:
private MutableLiveData<String> label = new MutableLiveData<>("New");
private LiveData<List<Product>> products = Transformations.switchMap(
label,
y -> mProductsRepository.getProductsByLabel(y)
);
Este ejemplo muestra el intercambio continuo de una LiveData
que guarda a una lista de productos.
Cada que label
cambia su valor, switchMap()
retorna el resultado de la consulta en el repositorio. Dejando de observar los productos previos y suscribiendo el observador a los entrantes.
Ejemplo: Administrar Categorías De Gastos
El ejemplo seguiremos de ejemplo administra las categorías de una aplicación Android hipotética que se encarga de gastos personales.
La idea consiste en proveer creación, edición y eliminación de categorías asociadas a los gastos.
Descargar Proyecto Android Studio
A través de este ejemplo quiero mostrarte los fragmentos de código relevantes donde apliqué el uso de LiveData
en cada caso de uso.
Puedes descargar el código completo y funcional del proyecto en Android Studio desde aquí:
¡Veamos la resolución!
Lista De Categorías
La interfaz de la lista es simple. Desplegamos el nombre de las categorías en cada ítem y añadimos un FAB para iniciar la creación de nuevos ítems.
A eso le sumamos la presentación de una SnackBar cuando se retorne de una cualquiera de las operaciones sobre las categorías.
Datos A Observar Con LiveData
Necesitamos percibir los siguientes cambios:
- La actualización de los ítems de lista
- El cambio del título de la Toolbar con la cantidad de ítems
- La visibilidad de la lista en caso de ausencia de ítems
Añadido a eso también podemos usar LiveData
para representar los eventos de:
- Apertura de actividad para crear/editar categorías
- Aparición del mensaje de éxito de operaciones
Para resolver el caso de uso se crearon los siguientes componentes:
Donde la CategoriesActivity
actúa como la vista principal, apoyándose en CategoriesAdapter
para inflar un RecyclerView.
CategoriesViewModel
ejercerá de intermediario ante el CategoriesRepository
y la actividad para cargar y mostrar objetos del tipo Category
.
Entidad Category
La clase Category
contiene el ID y el nombre de la categoría. Adicionalmente posee dos constructores. Uno para crear un objeto nuevo y otro para autogenerar el id de forma secuencial.
public class Category {
private final int id;
private final String name;
public Category(int id, String name) {
this.id = id;
this.name = name;
}
public Category(String name) {
this(CategoriesRepository.autogeneratedId, name);
}
public int getId() {
return id;
}
public String getName() {
return name;
}
// TODO: Más atributos...
}
Repositorio De Categorías
En este ejemplo se usa una fuente de datos en memoria (dummyData
) soportado por un HashMap
.
Se le proporcionan datos iniciales para visualizar la lista los cuales se cubren con un LiveData
para permitir su observación hacia la vista (observableCategories
).
public class CategoriesRepository {
private static CategoriesRepository sInstance;
// Indice simulador para autogeneración de IDs
public static int autogeneratedId = 6;
private final MutableLiveData<HashMap<Integer, Category>> observableCategories;
private final HashMap<Integer, Category> dummyData;
private CategoriesRepository() {
// Inserción por defecto de datos
dummyData = new HashMap<>();
dummyData.put(1, new Category(1, "Alimentación"));
dummyData.put(2, new Category(2, "Servicios"));
dummyData.put(3, new Category(3, "Ropa"));
dummyData.put(4, new Category(4, "Diversión"));
dummyData.put(5, new Category(5, "Salud"));
observableCategories = new MutableLiveData<>();
observableCategories.setValue(dummyData);
}
public static CategoriesRepository getsInstance() {
if (sInstance == null) {
sInstance = new CategoriesRepository();
}
return sInstance;
}
public LiveData<Category> getCategory(int categoryId) {
return new MutableLiveData<>(observableCategories.getValue().get(categoryId));
}
public void addCategory(Category category) {
dummyData.put(category.getId(), category);
observableCategories.setValue(dummyData);
autogeneratedId++;
}
public void delete(int categoryId) {
dummyData.remove(categoryId);
observableCategories.setValue(dummyData);
}
public LiveData<HashMap<Integer, Category>> getCategories() {
return observableCategories;
}
}
Actualizar Categorías
Para retornar las categorías del repositorio tendrás un LiveData
que cubra un HashMap<Integer, Category>
.
También existe un LiveData
para un String
que representa un filtro (filter
) hipotético sobre las categorías.
De esta forma puedes usar switchMap()
para generar una carga inicial que se actualice constantemente con el LiveData
retornado por CategoriesRepository.getCategories()
.
public class CategoriesViewModel extends ViewModel {
private final CategoriesRepository repository;
private final MutableLiveData<String> filter = new MutableLiveData<>("*");
private final LiveData<HashMap<Integer, Category>> categories;
public CategoriesViewModel() {
repository = CategoriesRepository.getsInstance();
categories = Transformations.switchMap(filter, y -> repository.getCategories());
}
public LiveData<HashMap<Integer, Category>> getCategories() {
return categories;
}
}
Asignamos las categorías al RecyclerView
a través de Data Binding en el layout content_main.xml.
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/categories_list"
...
app:items ="@{viewModel.categories}">
Ya que no existe un método set para los ítems de este view, verás creado un Binding Adapter que obtiene el adaptador del recycler y asigna el contenido:
public class BindingAdapters {
@BindingAdapter("items")
public static void setItems(RecyclerView list, HashMap<Integer, Category> items) {
CategoriesAdapter adapter = (CategoriesAdapter) list.getAdapter();
if (items != null) {
adapter.setCategoriesList(new ArrayList<>(items.values()));
}
}
}
Data Binding De Items
El adaptador de categorías asigna como parámetro de binding a cada objeto en onBindViewHolder()
.
@Override
public void onBindViewHolder(@NonNull CategoryViewHolder holder, int position) {
holder.binding.setCategory(mCategoriesList.get(position));
holder.binding.executePendingBindings();
}
De esta forma se asigna el nombre de la categoría a cada ítem en category_item.xml.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="category"
type="com.herpro.livedata.model.Category" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
...>
<TextView
android:id="@+id/category_name"
android:text="@{category.name}"
... />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Actualizar Título De La ActionBar
La cantidad de items (size
) es un LiveData
entero que obedece a una transformación map()
, cuyo producto es el valor de size()
del hash map de categorías.
size = Transformations.map(categories, HashMap::size);
Dicho valor se observa en el método setupToolbar()
desde la actividad, donde asignamos el string de formato categories_number
si existen items, de lo contrario asignamos zero_categories
.
private void setupToolbar() {
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// Observamos cantidad de items y actualizamos título
mViewModel.getCategoriesNumber().observe(this, integer -> {
String title;
if (integer > 0) {
title = getString(R.string.categories_number, integer);
} else {
title = getString(R.string.zero_categories);
}
getSupportActionBar().setTitle(title);
});
}
Actualizar Visibilidad De Lista
Similar al número de ítems, determinar cuándo ocultar la lista (MutableLiveData<Boolean>
) y mostrar el texto de ausencia de items es una transformación map()
. La función de salida es la comprobación del método isEmpty()
del mapa con categorías.
empty = Transformations.map(categories, HashMap::isEmpty);
El valor resultante se lo bindearemos a la lista y al texto de vacío en content_main.xml.
<androidx.constraintlayout.widget.ConstraintLayout
>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/categories_list"
app:emptyState="@{!viewModel.empty}"
>
</androidx.recyclerview.widget.RecyclerView>
<TextView
android:id="@+id/empty_state_text"
app:emptyState="@{viewModel.empty}"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
Donde el atributo app:emptyState
representa el intercambio de visibilidad en el siguiente binding adapter:
@BindingAdapter("emptyState")
public static void show(View v, boolean show) {
v.setVisibility(show ? View.VISIBLE : View.GONE);
}
El intercambio de visibilidades se vería así:
Procesar Retornos De Otra Activity
indicar el momento para mostrar la snackbar debido al evento de navegación desde AddEditCategoryActivity
, es un aspecto que podemos seguir con LiveData
.
Sin embargo, debemos asegurarnos que sean observados una sola vez. Característica que conseguimos si envolvemos objetos con la clase Event
(ver LiveData with SnackBar, Navigation and other events).
/**
* Clase genérica que permite hacer seguimientos a eventos con LiveData
*/
public class Event<T> {
private final T mContent;
private boolean hasBeenHandled = false;
public Event(T content) {
if (content == null) {
throw new IllegalArgumentException("los valores null en Event no son permitidos.");
}
mContent = content;
}
public T getContentIfNotHandled() {
if (hasBeenHandled) {
return null;
} else {
hasBeenHandled = true;
return mContent;
}
}
public boolean hasBeenHandled() {
return hasBeenHandled;
}
public T peekContent() {
return mContent;
}
}
Lo primero será recibir el resultado de la actividad de creación/edición en onActivityResult()
, donde invocaremos el método handleActivityResult()
del view model:
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
mViewModel.handleActivityResult(requestCode, resultCode);
}
¿Qué hace este método?
Comprueba los códigos de petición y de resultado provenientes. Basados en el valor, despacharemos el mensaje de la snackbar en el evento:
public void handleActivityResult(int requestCode, int resultCode) {
if (AddEditCategoryActivity.REQUEST_CODE == requestCode) {
switch (resultCode) {
case AddEditCategoryActivity.EDIT_RESULT_OK:
mSnackbarText.setValue(new Event<>(R.string.updated_category_message));
break;
case AddEditCategoryActivity.ADD_RESULT_OK:
mSnackbarText.setValue(new Event<>(R.string.added_category_message));
break;
case AddEditCategoryActivity.DELETE_RESULT_OK:
mSnackbarText.setValue(new Event<>(R.string.deleted_category_message));
break;
}
}
}
Completamos el flujo observando el cambio de valores desde setupSnackbar()
ejecutado en onCreate()
.
private void setupSnackbar() {
// Mostrar snackbar en resultados positivos de operaciones (crear, editar y eliminar)
mViewModel.getSnackbarText().observe(this, integerEvent -> {
Integer stringId = integerEvent.getContentIfNotHandled();
if (stringId != null) {
Snackbar.make(findViewById(R.id.coordinator),
stringId, Snackbar.LENGTH_LONG).show();
}
});
}
El método getContentIfNotHandled()
obtiene el valor del id del string si es la primera vez que se verifica. A su vez marca la bandera de Event
para marcarlo como consumido.
Manejar Evento Para Crear/Editar
Navegar hacia AddEditCategoryActivity
también puede ser representado con un objeto Event
.
En el view model tendremos el LiveData
asociado a esta acción y modificaremos su valor con addEditCategory()
.
public class CategoriesViewModel extends ViewModel {
private final MutableLiveData<Event<Integer>> mOpenCategoryEvent = new MutableLiveData<>();
// Abrir la categoría a editar
public void addEditCategory(int categoryId) {
mOpenCategoryEvent.setValue(new Event<>(categoryId));
}
public MutableLiveData<Event<Integer>> getOpenCategoryEvent() {
return mOpenCategoryEvent;
}
}
El rastreo lo registraremos en onCreate()
de la actividad con el método setupNavigation()
.
private void setupNavigation() {
// Abrir actividad para crear o editar categoría
mViewModel.getOpenCategoryEvent().observe(this, integerEvent -> {
Integer id = integerEvent.getContentIfNotHandled();
if (id != null) {
goToAddEdit(id);
}
});
}
El método goToAddEdit()
inicia la actividad de creación/edición asignando el ID de la categoría si este está presente.
private void goToAddEdit(int categoryId) {
Intent intent = new Intent(this, AddEditCategoryActivity.class);
if (categoryId > 0) {
// Abrir en modo edición
intent.putExtra(EXTRA_CATEGORY_ID, categoryId);
}
startActivityForResult(intent, AddEditCategoryActivity.REQUEST_CODE);
}
Iniciar navegación desde el FAB
Aquí bindeamos la ejecución de addEditCategory()
al atributo android:onClick()
del FAB en activity_categories.xml:
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:onClick="@{()->viewModel.addEditCategory(0)}"
app:srcCompat="@drawable/ic_add" />
Iniciar navegación desde ítem de lista
El layout del ítem también tiene data binding, en consecuencia asignamos a la escucha de clicks del padre la ejecución del método.
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:onClick="@{() -> viewModel.addEditCategory(category.id)}"
android:layout_height="wrap_content"
android:minHeight="?listPreferredItemHeight">
Crear Y Editar Categorías
La creación y edición se representa con un formulario que recibe en un campo de texto el nombre de la categoría.
Usaremos la misma actividad para crear y editar la categoría.
En el proyecto encontrarás los siguientes archivos como base de la solución con la arquitectura MVVM.
Ahora bien, ¿qué datos se observan?
El nombre de la categoría en el campo de texto. Puesto que es el origen para crear un nuevo elemento en el repositorio.
Y ¿qué eventos y estados procesamos?
Basados en la interfaz tenemos conocimientos de:
- Click en Guardar > Navegación a pantalla de lista
- Nombre de categoría inválido > Mostrar error
A continuación te muestro como se abordó la solución de lo descrito.
Observar Nombre De Categoría
Observar el valor del campo de texto es primordial para cargar la categoría en una edición y para obtener el último valor a la hora de guardarla.
Es por eso que en AddEditCategoryViewModel
lo seteamos con un LiveData
:
public class AddEditCategoryViewModel extends ViewModel {
private final MutableLiveData<String> name = new MutableLiveData<>();
private final CategoriesRepository mCategoriesRepository;
public AddEditCategoryViewModel() {
mCategoriesRepository = CategoriesRepository.getsInstance();
}
public MutableLiveData<String> getName() {
return name;
}
}
Cargar Categoría A Editar
La edición requiere que se cargue el objeto de categoría basado en el ID que se pudo haber enviado desde CategoriesActivity
.
Basado en eso, desde AddEditCategoriesActivity
.onCreate()
invocamos al método loadData()
encargado de dicha comprobación:
private void loadData() {
if (getIntent().getExtras() != null) {
int categoryId = getIntent().getExtras().getInt(CategoriesActivity.EXTRA_CATEGORY_ID);
mViewModel.start(categoryId);
} else {
mViewModel.start(0);
}
}
Esta acción dispara al método start()
de AddEditCategoryViewModel
, el cual retiene el valor del id. Luego comprueba si es una nueva categoría, si lo es, marca la bandera como true.
De lo contrario la marca como false, carga la categoría del repositorio y setea el valor al LiveData del nombre actual del formulario.
public class AddEditCategoryViewModel extends ViewModel {
private int mCategoryId;
private boolean mIsNewCategory;
public void start(int categoryId) {
mCategoryId = categoryId;
// ¿Es una nueva categoría?
if (categoryId <= 0) {
mIsNewCategory = true;
return;
}
mIsNewCategory = false;
LiveData<Category> category = mCategoriesRepository.getCategory(categoryId);
name.setValue(category.getValue().getName());
}
}
De esa manera, al asignar el view model al layout activity_add_edit_category.xml es posible actualizar el campo de texto a través de Two-way Data Binding:
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/category_name_text_field"
...
android:text="@={viewModel.name}" />
Guardar Categoría
Guardar una categoría consiste en:
- Validar si el nombre no es nulo y tiene al menos un carácter. En caso negativo, enviar evento de asignación de error
- Crear un nuevo objeto
Category
con ID autogenerado si es nuevo o con el ID de edición en caso contrario - Añadir al repositorio
- Lanzar evento de navegación hacia la lista de categorías
Este algoritmo se refleja en el método saveCategory()
de la siguiente manera:
public void saveCategory() {
// Realizar validaciones del estado
if (name.getValue() == null) {
mErrorText.setValue(new Event<>(R.string.category_name_error));
return;
}
if (name.getValue().isEmpty()) {
mErrorText.setValue(new Event<>(R.string.category_name_error));
return;
}
Category category;
if (mIsNewCategory) {
// "creación"
category = new Category(name.getValue());
} else {
// "edición"
category = new Category(mCategoryId, name.getValue());
}
// Guardar categoría
mCategoriesRepository.addCategory(category);
// Enviar evento de navegación
mCategorySavedEvent.setValue(new Event<>(mCategoryId));
}
Ahora procesamos el evento de click sobre el action buton desde la actividad.
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int itemId = item.getItemId();
if (R.id.action_delete == itemId) {
} else if (R.id.action_save == itemId) {
mViewModel.saveCategory();
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
Procesar Evento De Error
En onCreate()
llamamos al método setupInputErrors()
el cual aplica dos observaciones. Una sobre el evento de ocurrencia del error y otra sobre el nombre de la categoría.
private void setupInputErrors() {
mViewModel.getName().observe(this, s -> {
// Limpiar error al editar
if (mBinding.categoryInputLayout.getError() != null) {
mBinding.categoryInputLayout.setError(null);
}
});
// Limpiar código para poner lambdas
mViewModel.getError().observe(this, event -> {
// Mostrar error en el nombre de la categoría
Integer msg = event.getContentIfNotHandled();
if (msg != null) {
mBinding.categoryInputLayout.setError(getText(msg));
}
});
}
El evento del error trae el recurso string con para setear el valor sobre el error mostrado por el TextInputLayout.
La observación del nombre es necesaria ya que deseamos limpiar el error cuando el usuario edite de nuevo el campo de texto.
Procesar Evento De Categoría Guardada
Este manejo lo hacemos con el método setupNavigation()
en onCreate()
. En su interior cerramos la actividad de creación/edición y seteamos un código exitoso de resultado para el caso actual:
private void setupNavigation() {
mViewModel.getCategorySavedEvent().observe(this, categoryEvent -> {
Integer categoryId = categoryEvent.getContentIfNotHandled();
if (categoryId != null) {
// Finalizar actividad con resultado exitoso
setResult(categoryId > 0 ? EDIT_RESULT_OK : ADD_RESULT_OK);
finish();
}
});
}
Eliminar Categorías
Para finalizar veremos cómo se ha solucionado la eliminación.
La secuencia de interfaz sería:
- Presionar action button con icono de papelera
- Despliega un diálogo de confirmación
- Al confirmar procesar eliminación
- Mostrar mensaje de eliminación exitosa
Veamos el flujo de estas acciones a través de la arquitectura.
Procesar Click Sobre Action Button
La visibilidad por defecto del action button de eliminación es GONE
. Si queremos que se revele a la hora de editar una categoría lo hacemos desde onCreateOptionsMenu()
:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.add_edit_category_menu, menu);
// Mostrar icono de eliminar si hay edición
if (isEdition()) {
menu.findItem(R.id.action_delete).setVisible(true);
}
return true;
}
Con este comportamiento, ya es posible procesar su click, donde iniciaremos un diálogo de confirmación.
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int itemId = item.getItemId();
if (R.id.action_delete == itemId) {
// Mostrar diálogo de confirmación
AlertDialogFragment.create(R.string.dialog_delete_msg,
R.string.dialog_delete_positive, R.string.dialog_save_negative)
.show(getSupportFragmentManager(), AlertDialogFragment.DELETE_TAG);
return true;
} else if (R.id.action_save == itemId) {
mViewModel.saveCategory();
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
El click en el botón de confirmación será despachado al controlador onPositiveClick()
de la interfaz que comunica a la activity con el diálogo. Justo allí donde llamaremos al método deleteCategory()
del view model:
@Override
public void onPositiveClick(DialogFragment dialog) {
switch (dialog.getTag()) {
case AlertDialogFragment.DELETE_TAG:
mViewModel.deleteCategory();
break;
case AlertDialogFragment.DISCARD_TAG:
// manejar descarte
break;
}
}
Comunicar Eliminación Al ViewModel
Eliminar una categoría consiste en pedir al repositorio la remoción del objeto y comunicar un evento de navegación indicando que la eliminación fue exitosa.
El método deleteCategory()
muestra lo simple de estas instrucciones:
public void deleteCategory() {
mCategoriesRepository.delete(mCategoryId);
mCategoryDeletedEvent.setValue(new Event<>(new Object()));
}
Remover Categoría Del Repositorio
El método CategoriesRepositorio.delete()
remueve una categoría basado en el id. Al ser nuestro origen de datos un HashMap
invocamos al método remove()
.
Luego pasamos con setValue()
el nuevo estado para que se transmita hacia la lista:
public void delete(int categoryId) {
dummyData.remove(categoryId);
observableCategories.setValue(dummyData);
}
Procesar Evento De Categoría Eliminada
Similar que con el evento de categoría guardada, observamos el LiveData
desde setupNavigation()
para cerrar la actividad de creación/edición y notificar el código de resultado:
private void setupNavigation() {
mViewModel.getCategoryDeletedEvent().observe(this, objectEvent -> {
if(objectEvent.getContentIfNotHandled()!=null){
// Ir hacia la lista de categorías
setResult(DELETE_RESULT_OK);
finish();
}
});
}
Conclusión
En este tutorial viste como usar la clase LiveData para recibir actualizaciones de tus datos manteniendo una relación con el ciclo de vida de la vista.
La aplicación de categorías de gastos te permitió explorar varios tipos de actualizaciones de UI y procesamiento de eventos.
Ahora, te invito a seguir mi tutorial de ViewModel [próximamente] para que comprendas a fondo más su comportamiento.