Adaptadores para bases de datos

Un adaptador (Adapter) es un mecanismo de Android que hace de puente entre nuestros datos y las vistas contenidas en un RecyclerView, ListView, GridView o Spinner.

En el siguiente ejercicio vamos a crear un adaptador que toma la información de la base de datos que acabamos de crear y se la muestra a un RecyclerView. Realmente podríamos usar el adaptador AdaptadorLugares que ya tenemos creado. Este adaptador toma la información de un objeto que sigue la interfaz Lugares, restricción que cumple la clase LugaresBD. No obstante, vamos a realizar una implementación alternativa. La razón es que la implementación actual de AdaptadorLugares necesitaría una consulta a la base de datos cada vez que requiera una información de Lugares. (Veremos más adelante que cada llamada a elemento(), anyade(), nuevo()… va a suponer un acceso a la base de datos.)

El nuevo adaptador, AdaptadorLugaresBD, va a trabajar de una forma más eficiente. Vamos a realizar una consulta de los elementos a listar y los va a guardar en un objeto de la clase Cursor. Mantendrá esta información mientras no cambie la información a listar, por lo que solo va a necesitar una consulta a la base de datos.

Ejercicio: Un Adaptador para base de datos en Mis Lugares

1.      Crea la clase AdaptadorLugaresBD con el siguiente código:

public class AdaptadorLugaresBD extends AdaptadorLugares {

    protected Cursor cursor;

    public AdaptadorLugaresBD(Context contexto, Lugares lugares,
                  Cursor cursor) {
        super(contexto, lugares);
        this.cursor = cursor;
    }

    public Cursor getCursor() {
        return cursor;
    }

    public void setCursor(Cursor cursor) {
        this.cursor = cursor;
    }

    public Lugar lugarPosicion(int posicion) {
        cursor.moveToPosition(posicion);
        return LugaresBD.extraeLugar(cursor);
    }

    public int idPosicion(int posicion) {
        cursor.moveToPosition(posicion);
        return cursor.getInt(0);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        Lugar lugar = lugarPosicion(position);
        personalizaVista(holder, lugar);
    }

    @Override
    public int getItemCount() {
        return cursor.getCount();
    }
}

Esta clase extiende AdaptadorLugares; de esta forma aprovechamos la mayor parte del código del adaptador y solo tenemos que indicar las diferencias. La más importante es que ahora el constructor tiene un nuevo parámetro de tipo Cursor, que es el resultado de una consulta en la base de datos. Realmente es aquí donde vamos a buscar los elementos a listar en el RecyclerView. Esta forma de trabajar es mucho más versátil que utilizar un array, podemos listar un tipo de lugar o que cumplan una determinada condición sin más que realizar un pequeño cambio en la consulta SQL. Además, podremos ordenarlos por valoración o cualquier otro criterio, porque se mostrarán en el mismo orden como aparecen en el Cursor. Por otra parte, resulta muy eficiente dado que se realiza solo una consulta a la base de datos, dejando el resultado almacenado en la variable de tipo Cursor.

El constructor de la clase se limita a llamar al super() y a almacenar el nuevo parámetro en una variable global. A continuación se han añadido los métodosgetter y setter que permiten acceder al Cursor desde fuera de la clase.

Con el método lugarPosicion() vamos a poder acceder a un lugar, indicar su posición en Cursor o, lo que es lo mismo, su posición en el listado.  Para ello, movemos el cursor a la posición indicada y extraemos el lugar en esta posición, utilizando un método estático de LugarBD.

Cuando queramos realizar una operación de borrado o edición de un registro de la base de datos, vamos a identificar el lugar a modificar por medio de la columna _id. Recuerda que esta columna ha sido definida en la posición 0. Para obtener el idde un lugar conociendo la posición que ocupa en el listado, se ha definido el método lugarPosicion().

Los dos últimos métodos ya existen en la clase que extendemos pero los vamos a reemplazar; por esta razón tienen la etiqueta de @Override. El primero es onBindViewHolder() que se utilizaba para personalizar la vista ViewHolder en una determinada posición. La gran diferencia entre el nuevo método es que ahora el lugar lo obtenemos del cursor, mientras que en el método anterior se obtenía de lugares. Esto supondría una nueva consulta en la base de datos por cada llamada a onBindViewHolder(), lo que sería muy poco eficiente. En el método getItemCount() pasa algo similar, obtener el número de elementos directamente del cursor es más eficiente que hacer una nueva consulta.

2.     Añade a la clase LugaresBD los siguientes métodos:              

public static Lugar extraeLugar(Cursor cursor) {
    Lugar lugar = new Lugar();
    lugar.setNombre(cursor.getString(1));
    lugar.setDireccion(cursor.getString(2));
    lugar.setPosicion(new GeoPunto(cursor.getDouble(3),
            cursor.getDouble(4)));
    lugar.setTipo(TipoLugar.values()[cursor.getInt(5)]);
    lugar.setFoto(cursor.getString(6));
    lugar.setTelefono(cursor.getInt(7));
    lugar.setUrl(cursor.getString(8));
    lugar.setComentario(cursor.getString(9));
    lugar.setFecha(cursor.getLong(10));
    lugar.setValoracion(cursor.getFloat(11));
    return lugar;
}

public Cursor extraeCursor() {
    String consulta = "SELECT * FROM lugares";
    SQLiteDatabase bd = getReadableDatabase();
    return bd.rawQuery(consulta, null);
}

El primer método crea un nuevo lugar con los datos de la posición actual de un Cursor. El segundo nos devuelve un cursor que contiene todos los datos de la tabla.

3.    Abre la clase MainActivity y reemplaza la declaración de la variables siguientes:

public static Lugares lugares = new LugaresVector();
public AdaptadorLugares adaptador;

por:

public static LugaresBD lugares;
public static AdaptadorLugaresBD adaptador;

usamos la clases específicas para poder acceder a los nuevo métodos y hacemos adaptador static para poder usarlo desde otras clases.

4.      Añade al principio de onCreate() la inicialización de lugares:

lugares = new LugaresBD(this);

Esta inicialización ya no puede hacerse en la declaración porque necesitamos conocer el contexto que se pasa como parámetro.

5.     En este mismo método añade en la inicialización de adaptador el código subrayado:

adaptador = new AdaptadorLugaresBD(this, lugares, lugares.extraeCursor());

6.     Ejecuta la aplicación y verifica que las vistas se muestran correctamente.

7.     Si pulsas sobre un elemento del RecyclerView se producirá un error. Para solucionarlo abre la clase VistaLugarActivity y reemplaza:

lugar = MainActivity.lugares.elemento((int) id);

por:

lugar = MainActivity.adaptador.lugarPosicion((int) id);

Recuerda que el método elemento(), todavía no ha sido implementado. Además, como ya hemos comentado resulta más eficiente acceder a este lugar usando el Cursor.

8.     En la la clase EdicionLugarActivity realiza la misma operación.

9.     Ejecuta la aplicación y verificala. Puedes seleccionar un lugar e incluso editarlo, aunque si guardas una edición no se almacenarán los cambios. Lo arreglaremos más adelante.

Ejercicio: Adaptando la actividad del Mapa al nuevo adaptador

En este ejercicio adaptaremos la actividad MapaActivity para que use adecuadamente el nuevo adaptador basado en Cursor. El proceso es el mismo que acabamos de realizar: Reemplazaremos los accesos a  MainActivity.lugares por MainActivity.adaptador.

1.     Reemplaza el código del método onMapReady() de la clase MapaActivity:

if (MainActivity.lugares.tamanyo() > 0) {
    GeoPunto p = MainActivity.lugares.elemento(0).getPosicion();

por:

if (MainActivity.adaptador.getItemCount() > 0) {
    GeoPunto p = MainActivity.adaptador.lugarPosicion(0).getPosicion();

Reemplaza:

for (int n=0; n<MainActivity.lugares.tamanyo(); n++) {
    Lugar lugar = MainActivity.lugares.elemento(n);

por:

for (int n=0; n<MainActivity.adaptador.getItemCount(); n++) {
    Lugar lugar = MainActivity.adaptador.lugarPosicion(n);

En el método onInfoWindowClick() reemplaza:

for (int id=0; id<MainActivity.lugares.tamanyo(); id++){
    if (MainActivity.lugares.elemento(id).getNombre()

por:

for (int id=0; id<MainActivity.adaptador.getItemCount(); id++) {
    if (MainActivity.adaptador.lugarPosicion(id).getNombre()

Como hemos indicado, la ventaja de este cambio es que no realizaremos nuevos accesos a la base de datos una vez obtenido el Cursor.

2.     Verifica el funcionamiento de la actividad MapaActivity.

Práctica: Probando consultas en Mis Lugares

1.     En el método extraeCursor() de la clase LugaresBD reemplaza el comando SELECT * FROM lugares por SELECT * FROM lugares WHERE valoracion>1.0 ORDER BY nombre LIMIT 4. Ejecuta la aplicación y verifica la nueva lista.

2.     Realiza otras consultas similares. Si tienes dudas, puedes consultar en Internet la sintaxis del comando SQL SELECT.

3.     Si quieres practicar el uso del método query(), puedes tratar de realizar una consulta utilizando este método.

Solución:

1.     Reemplaza en lugaresBD el siguiente metodo:

public Cursor extraeCursor() {
    SharedPreferences pref =
            PreferenceManager.getDefaultSharedPreferences(contexto);
    String consulta;
    switch (pref.getString("orden", "0")) {
        case "0":
            consulta = "SELECT * FROM lugares ";
            break;
        case "1":
            consulta = "SELECT * FROM lugares ORDER BY valoracion DESC";
            break;
        default:
            double lon = Lugares.posicionActual.getLongitud();
            double lat = Lugares.posicionActual.getLatitud();
            consulta = "SELECT * FROM lugares ORDER BY " +
                    "(" + lon + "-longitud)*(" + lon + "-longitud) + " +
                    "(" + lat + "-latitud )*(" + lat + "-latitud )";
            break;
    }
    consulta += " LIMIT "+pref.getString("maximo","12");
    SQLiteDatabase bd = getReadableDatabase();
    return bd.rawQuery(consulta, null);
}

2.     En MapaActivity añade:

static final int RESULTADO_PREFERENCIAS = 0;

public void lanzarPreferencias(View view) {
    Intent i = new Intent(this, PreferenciasActivity.class);
    startActivityForResult(i, RESULTADO_PREFERENCIAS);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, 
             Intent data) {
    if (requestCode == RESULTADO_PREFERENCIAS) {
        adaptador.setCursor(MainActivity.lugares.extraeCursor());
        adaptador.notifyDataSetChanged();
    }
}