En este tutorial veremos con más detalle como consultar datos con Room y la anotación @Query
.
Más específicamente: consultar pasando uno o múltiples parámetros y como consultar solo las columnas que necesitemos.
¿Cómo lo reflejaremos en nuestra App de listas de compras?
Añadiremos un filtro simple en la actividad principal y crearemos una actividad de edición como lo ilustra el siguiente boceto:
Puedes descargar el código completo del proyecto desde el siguiente enlace:
Veamos de qué va la solución.
1. Enlazar Un Parámetro En Una Consulta
Los métodos anotados con @Query
en nuestros DAOs pueden recibir parámetros con el fin de enlazarlos con argumentos de la consulta.
Para ello usamos la sintaxis :nombre
con el fin de diferenciar el contexto.
Ejemplo:
Podemos añadir como parámetro el ID que proporcione el item clickeado en el recycler view para consultar la lista de compras.
Abre ShoppingListDao
y crea un nuevo método llamado getShoppingList()
que reciba el ID y bindealo así:
@Query("SELECT * FROM shopping_list WHERE id = :id LIMIT 1")
LiveData<ShoppingList> getShoppingList(String id);
De esta forma :id
será reemplazado por el parámetro id
.
2. Enlazar Múltiples Parámetros En Una Consulta
Con la misma lógica, Room también soporta el enlace de una lista de parámetros. La librería genera automáticamente la consulta en tiempo de ejecución, asignando cada elemento en la sentencia.
Ejemplo:
Probemos esta característica con los filtros por categoría propuestos en el boceto al inicio del tutorial.
Lo primero será agregar la categoría como columna a la entidad ShoppingList
así:
@Nullable
@ColumnInfo(name = "category")
private final String mCategory;
Ahora crea un nuevo método en el DAO que se llame getShoppingListsByCategories()
y pásale una lista de strings como parámetro. Bindeala en un operador IN
:
@Query("SELECT * FROM shopping_list WHERE category IN(:categories)")
LiveData<List<ShoppingList>> getShoppingListsByCategories(List<String> categories);
Y listo, de esa forma consultaremos los elementos de la actividad principal cuando agreguemos los check boxes para filtrar.
3. Seleccionar Un Subconjunto De Columnas En Room
Room nos permite retornar POJOs personalizados que representen solo las columnas que deseamos retornar si el caso de uso lo amerita.
Por ejemplo:
Añade dos columnas más a ShoppingList
para la fecha de creación y última modificación. Con estas serían ya 5 columnas de nuestra tabla (la propiedad defaultValue
permite asignar un valor por defecto al campo si no es definido en la inserción).
@ColumnInfo(name = "created_date", defaultValue = "CURRENT_TIMESTAMP")
private final String mCreatedDate;
@ColumnInfo(name = "last_updated", defaultValue = "CURRENT_TIMESTAMP")
private final String mLastUpdated;
Ahora crea una nueva clase llamada ShoppingListForList
y añade solo dos campos: id
y name
.
public class ShoppingListForList {
public String id;
public String name;
}
Termina asignado esta clase como tipo de retorno y seleccionando las dos columnas objetivo:
@Query("SELECT id, name FROM shopping_list")
LiveData<List<ShoppingListForList>> getAll();
@Query("SELECT id, name FROM shopping_list WHERE category IN(:categories)")
LiveData<List<ShoppingListForList>> getShoppingListsByCategories(List<String> categories);
Consultando solo las columnas necesarias optimizas la velocidad de tus consultas.
Nota: Modifica los métodos del repositorio y view model para que acepten este tipo en sus retornos.
4. Insertar Registros Parcialmente
También es posible usar POJOs arbitrarios que representen la inserción con tan solo columnas que nos interesen.
Ejemplo:
Vamos a crear una entidad llamada ShoppingListInsert
con solo 3 campos: id
, name
y category
. Donde category
tomará como valor inicial una de las tres categorías usadas para el ejemplo:
public class ShoppingListInsert {
String id;
String name;
String category = generateCategory();
public ShoppingListInsert(String id, String name) {
this.id = id;
this.name = name;
}
public static String generateCategory() {
String[] categories = new String[]{"Fitness", "Eventos", "Rápidas"};
return categories[new Random().nextInt(3)];
}
}
Ahora creamos un método para insertar este tipo de objetos llamado partialInsert()
. Es importante pasarle la propiedad entity
con la entidad original a la que Room interpretará:
@Insert(onConflict = OnConflictStrategy.IGNORE, entity = ShoppingList.class)
void partialInsert(ShoppingListInsert shoppingList);
@Insert(onConflict = OnConflictStrategy.IGNORE, entity = ShoppingList.class)
void insertShoppingLists(List<ShoppingListInsert> lists);
Actualiza también el método insertShoppingLists()
para la inserción múltiple.
5. Actualizar Versión De La Base De Datos
Debido a que nuestro esquema ha cambiado, iremos a ShoppingListDatabase
y cambia la propiedad versión por el valor 2
.
Adicionalmente agrega a la creación de la instancia con el builder el método fallbackToDestructiveMigration()
para eliminar todo el contenido actual de la base de datos y recrearlo con la siguiente versión.
INSTANCE = Room.databaseBuilder(
context.getApplicationContext(), ShoppingListDatabase.class,
DATABASE_NAME)
.addCallback(mRoomCallback)
.fallbackToDestructiveMigration()
.build();
6. Filtrar Listas De Compras
Una vez modificado nuestra capa de datos para satisfacer las características de nuestros bocetos, comenzaremos a crear la interfaz propuesta.
Comencemos con el filtro:
- Mueve el RecyclerView de activity_main.xml a un nuevo layout llamado main_content.xml. Android Studio puede hacerlo automáticamente si das click derecho en el componente y presionas Refactor > Layout.
2. Abre el nuevo layout, agrega como nodo raíz un ConstraintLayout
y sitúa en la parte superior a tres etiquetas CheckBox. La solución sería:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/normal_padding">
<CheckBox
android:id="@+id/filter_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/filter_1"
app:layout_constraintEnd_toStartOf="@+id/filter_2"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<CheckBox
android:id="@+id/filter_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/filter_2"
app:layout_constraintEnd_toStartOf="@+id/filter_3"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/filter_1"
app:layout_constraintTop_toTopOf="parent" />
<CheckBox
android:id="@+id/filter_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/filter_3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/filter_2"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="@dimen/normal_padding"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/filter_2"
tools:listitem="@layout/shopping_list_item"
tools:showIn="@layout/activity_main" />
</androidx.constraintlayout.widget.ConstraintLayout>
3. Procesa las categorías seleccionadas agregando al view model un LiveData para ellas. Complementariamente provee un método para añadirlas y otro para removerlas:
public class ShoppingListViewModel extends AndroidViewModel {
// Filtros observados
private final MutableLiveData<List<String>> mCategories
= new MutableLiveData<>(new ArrayList<>());
// Filtros
private final List<String> mFilters = new ArrayList<>();
public void addFilter(String category) {
mFilters.add(category);
mCategories.setValue(mFilters);
}
public void removeFilter(String category) {
mFilters.remove(category);
mCategories.setValue(mFilters);
}
}
4. Usaremos una transformación switchMap()
para obtener las listas de compras por categorías en el constructor del ViewModel. Si no existen categorías marcadas entonces obtenemos todos los registros:
public ShoppingListViewModel(@NonNull Application application) {
super(application);
mRepository = new ShoppingListRepository(application);
// Obtener listas de compras por categorías
mShoppingLists = Transformations.switchMap(
mCategories,
categories -> {
if (categories.isEmpty()) {
return mRepository.getShoppingLists();
} else {
return mRepository.getShoppingListsWithCategories(categories);
}
}
);
}
5. Por último actualizamos mFilters
desde MainActivity
a través del método setupFilters()
. Aquí conseguiremos las referencias de los CheckBoxes y les añadimos una escucha. Esta añade o elimina los filtros dependiendo de su estado.
private void setupFilters() {
mFilters = new ArrayList<>();
mFilters.add(findViewById(R.id.filter_1));
mFilters.add(findViewById(R.id.filter_2));
mFilters.add(findViewById(R.id.filter_3));
// Definir escucha de filtros
CompoundButton.OnCheckedChangeListener listener = (compoundButton, checked) -> {
String category = compoundButton.getText().toString();
if (checked) {
mViewModel.addFilter(category);
} else {
mViewModel.removeFilter(category);
}
};
// Setear escucha
for (CheckBox filter : mFilters) {
filter.setOnCheckedChangeListener(listener);
}
}
Si ejecutas el proyecto podrás filtrar las listas por la categoría asignada:
7. Editar Listas De Compras
Por el momento la pantalla de edición estará vacía. El único dato que desplegaremos será el nombre en la Toolbar.
Veamos cómo conseguirlo:
7.1 Crear Actividad De Edición
Añade una nueva actividad yendo a File > New > Activity > Empty Activity y nombrala EditShoppingListActivity
.
Seguido, habilita la navegación hacia arriba:
public class EditShoppingListActivity extends AppCompatActivity {
public static final String EXTRA_SHOPPING_LIST_ID = "com.develou.shoppinglist.shoppingListId";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit_shopping_list);
// Obtener id de la lista de compras
String id = getIntent().getStringExtra(EXTRA_SHOPPING_LIST_ID);
setupActionBar();
}
private void setupActionBar() {
ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayHomeAsUpEnabled(true);
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
}
Recuerda que recibiremos el id de la lista de compras, por lo que es necesario extraerlo desde el extra.
7.2 Añadir Escucha De Ítems Al Adaptador
Hasta el momento nuestro adaptador no reaccionaba a los eventos de clics sobre sus ítems, así que añadiremos una interfaz que se encargue de esta responsabilidad.
Esto implica:
- Añadir una interfaz de escucha al adaptador
- Procesar el click sobre cada ítem en el ViewHolder
- Añadir un método para asignar la escucha
- Ubicar como clase interna al ViewHolder
Al codificar todas las características mencionadas tendrás:
public class ShoppingListAdapter
extends RecyclerView.Adapter<ShoppingListAdapter.ShoppingListViewHolder> {
private List<ShoppingListForList> mShoppingLists;
private ItemListener mItemListener;
@NonNull
@Override
public ShoppingListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ShoppingListViewHolder(
LayoutInflater.from(parent.getContext())
.inflate(R.layout.shopping_list_item, parent, false)
);
}
@Override
public void onBindViewHolder(@NonNull ShoppingListViewHolder holder, int position) {
ShoppingListForList item = mShoppingLists.get(position);
holder.bind(item);
}
@Override
public int getItemCount() {
return mShoppingLists == null ? 0 : mShoppingLists.size();
}
public void setItems(List<ShoppingListForList> items) {
mShoppingLists = items;
notifyDataSetChanged();
}
public void setItemListener(ItemListener listener) {
mItemListener = listener;
}
interface ItemListener {
void onClick(ShoppingListForList shoppingList);
}
public class ShoppingListViewHolder extends RecyclerView.ViewHolder {
private final TextView mNameText;
public ShoppingListViewHolder(@NonNull View itemView) {
super(itemView);
mNameText = itemView.findViewById(R.id.name);
itemView.setOnClickListener(view -> {
if (mItemListener != null) {
ShoppingListForList clickedItem = mShoppingLists.get(getAdapterPosition());
mItemListener.onClick(clickedItem);
}
});
}
public void bind(ShoppingListForList item) {
mNameText.setText(item.name);
}
}
}
7.3 Iniciar Actividad De Edición
Añade una escucha al adaptador en MainActivity
. La idea es enviar el Id de la lista de compras en el Intent explícito con la finalidad de procesarlo en la actividad de edición.
private void setupList() {
mList = findViewById(R.id.list);
mAdapter = new ShoppingListAdapter();
mList.setAdapter(mAdapter);
// Asignar escucha de ítems
mAdapter.setItemListener(this::editShoppingList);
// Observar cambios de listas de compras
mViewModel.getShoppingLists().observe(this, mAdapter::setItems);
}
private void editShoppingList(ShoppingListForList shoppingList) {
Intent intent = new Intent(MainActivity.this,
EditShoppingListActivity.class);
intent.putExtra(EditShoppingListActivity.EXTRA_SHOPPING_LIST_ID,
shoppingList.id);
startActivity(intent);
}
Crea un nuevo método en el repositorio para cargar una lista de compras por ID:
public LiveData<ShoppingList> getShoppingList(String id){
return mShoppingListDao.getShoppingList(id);
}
7.4 Crear ViewModel Para Edición
Luego, crea la clase EditShoppingListViewModel
y:
- Hazla extender de
AndroidViewModel
- Añade un LiveData para el ID de la lista
- Añade un
LiveData
para la lista a editar. Relaciónala con el ID a través de una transformaciónswitchMap()
. - Agrégale un método para cargar la lista con el repositorio
El código sería el siguiente:
public class EditShoppingListViewModel extends AndroidViewModel {
private final ShoppingListRepository mRepository;
private final MutableLiveData<String> mShoppingListId = new MutableLiveData<>();
private final LiveData<ShoppingList> mShoppingList;
public EditShoppingListViewModel(@NonNull Application application) {
super(application);
mRepository = new ShoppingListRepository(application);
mShoppingList = Transformations.switchMap(
mShoppingListId,
mRepository::getShoppingList
);
}
public void start(String id){
if(id.equals(mShoppingListId.getValue())){
return;
}
mShoppingListId.setValue(id);
}
public LiveData<ShoppingList> getShoppingList() {
return mShoppingList;
}
}
Finalizando actualizamos la interfaz al observar mShoppingList
en la actividad. Cuando se cargue la lista de compras a editar cambiamos el título de la Toolbar por el nombre del registro:
private void setupActionBar() {
ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayHomeAsUpEnabled(true);
mViewModel.getShoppingList().observe(this,
shoppingList -> actionBar.setTitle(shoppingList.getName())
);
}
Si ejecutas el proyecto y como un ejemplo seleccionas la «Lista 4» deberías ver lo siguiente:
Siguiente tutorial: Actualizar Datos Con Room