Consultar Datos Con Room

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:

Consultar Datos Con Room

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:

  1. 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.
Android Studio 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ón switchMap().
  • 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:

Edición de lista de compras

Siguiente tutorial: Actualizar Datos Con Room

Ú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!