Uso de base de datos en Android

Las bases de datos son una herramienta de gran potencia en la creación de aplicaciones informáticas. Hasta hace muy poco resultaba costoso y complejo utilizar bases de datos en nuestras aplicaciones. No obstante, Android incorpora la librería SQLite, que nos permitirá utilizar bases de datos mediante el lenguaje SQL, de una forma sencilla y utilizando muy pocos recursos del sistema. Como verás en este apartado, almacenar tu información en una base de datos no es mucho más complejo que almacenarla en un fichero, y además resulta mucho más potente.

SQL es el lenguaje de programación más utilizado para bases de datos. No resulta complejo entender los ejemplos que se mostrarán en este libro. No obstante, si deseas hacer cosas más complicadas te recomiendo que consultes alguno de los muchos manuales que se han escrito sobre el tema.

Para manipular una base de datos en Android usaremos la clase abstracta SQLiteOpenHelper, que nos facilita tanto la creación automática de la base de datos como el trabajar con futuras versiones de esta base de datos. Para crear un descendiente de esta clase hay que implementar los métodos onCreate() y onUpgrade(), y opcionalmente, onOpen(). La gran ventaja de utilizar esta clase es que ella se preocupará de abrir la base de datos si existe, o de crearla si no existe. Incluso de actualizar la versión si decidimos crear una nueva estructura de la base de datos. Además, esta clase tiene dos métodos: getReadableDatabase() y getWritableDatabase(), que abren la base de datos en modo solo lectura o lectura y escritura. En caso de que todavía no exista la base de datos, estos métodos se encargarán de crearla.

En los próximos ejercicios pasamos a demostrar cómo guardar los datos de la aplicación Mis Lugares en una base de datos. Esta estará formada por una única tabla (lugares). A continuación, se muestran las columnas que contendrán y las filas que se introducirán como ejemplo. Los valores que aparecen en las columnas _id y fecha no coincidirán con los valores reales:

>video[Tutorial] Uso de bases de datos en Android

_id

nombre

direccion

longitud

latitud

tipo

foto

telefono

url

Comentario

fecha

valoracion

1

Escuela

C/ Paran

-0.166

38.99

7

962849

ht

Uno de lo

2345

3.0

2

Al de

P. Industr

-0.190

38.92

2

636472

ht

No te pier

2345

3.0

4

android

ciberesp

0.0

0.0

7

ht

Amplia tu

2345

5.0

7

Barranc

Vía Verd

-0.295

38.86

9

ht

Espectacu

2345

4.0

5

La Vital

Avda. de

-0.172

38.97

6

962881

ht

El típico c

2345

2.0

Estructura de la tabla lugares de la base de datos lugares.

Ejercicio: Utilizando una base de datos en Mis Lugares.

1.     Comenzamos haciendo una copia del proyecto, dado que en la nueva versión se eliminará parte del código desarrollado y es posible que queramos consultarlo en un futuro. Abre en el explorador de ficheros  la carpeta que contiene el proyecto.  Para hacer esto puedes pulsar con el botón derecho sobre app en el explorador del proyecto y seleccionar Show in Explorer. Haz una copia de esta carpeta con un nuevo nombre.

2.     Crea la clase LugaresBD, en el paquete datos, con el siguiente código:

public class LugaresBD extends SQLiteOpenHelper {

   Context contexto;

   public LugaresBD(Context contexto) {
      super(contexto, "lugares", null, 1);
      this.contexto = contexto;
   }

   @Override public void onCreate(SQLiteDatabase bd) {
      bd.execSQL("CREATE TABLE lugares ("+
             "_id INTEGER PRIMARY KEY AUTOINCREMENT, "+
             "nombre TEXT, " +
             "direccion TEXT, " +
             "longitud REAL, " +  
             "latitud REAL, " +   
             "tipo INTEGER, " +
             "foto TEXT, " +
             "telefono INTEGER, " +
             "url TEXT, " +
             "comentario TEXT, " +
             "fecha BIGINT, " +
             "valoracion REAL)");  
     bd.execSQL("INSERT INTO lugares VALUES (null, "+
       "'Escuela Politécnica Superior de Gandía', "+
       "'C/ Paranimf, 1 46730 Gandia (SPAIN)', -0.166093, 38.995656, "+
       TipoLugar.EDUCACION.ordinal() + ", '', 962849300, "+ 
       "'http://www.epsg.upv.es', "+
       "'Uno de los mejores lugares para formarse.', "+ 
       System.currentTimeMillis() +", 3.0)");
     bd.execSQL("INSERT INTO lugares VALUES (null, 'Al de siempre', "+
       "'P.Industrial Junto Molí Nou - 46722, Benifla (Valencia)', "+
       " -0.190642, 38.925857, " +  TipoLugar.BAR.ordinal() + ", '', "+ 
       "636472405, '', "+"'No te pierdas el arroz en calabaza.', " + 
       System.currentTimeMillis() +", 3.0)");
     bd.execSQL("INSERT INTO lugares VALUES (null, 'androidcurso.com', "+
       "'ciberespacio', 0.0, 0.0,"+TipoLugar.EDUCACION.ordinal()+", '', "+ 
       "962849300, 'http://androidcurso.com', "+
       "'Amplia tus conocimientos sobre Android.', "+ 
       System.currentTimeMillis() +", 5.0)");
   bd.execSQL("INSERT INTO lugares VALUES (null,'Barranco del Infierno',"+
       "'Vía Verde del río Serpis. Villalonga (Valencia)', -0.295058, "+
       "38.867180, "+TipoLugar.NATURALEZA.ordinal() + ", '', 0, "+ 
      "'http://sosegaos.blogspot.com.es/2009/02/lorcha-villalonga-via-verde-del-"+
       "rio.html', 'Espectacular ruta para bici o andar', "+ 
       System.currentTimeMillis() +", 4.0)");
     bd.execSQL("INSERT INTO lugares VALUES (null, 'La Vital', "+
      "'Avda. La Vital,0 46701 Gandia (Valencia)',-0.1720092,38.9705949,"+
       TipoLugar.COMPRAS.ordinal() + ", '', 962881070, "+
       "'http://www.lavital.es', 'El típico centro comercial', "+ 
       System.currentTimeMillis() +", 2.0)");
   }

   @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, 
                                                      int newVersion) {
   }
} 
class LugaresBD(val contexto: Context) :
                          SQLiteOpenHelper(contexto, "lugares", null, 1) {
   override fun onCreate(bd: SQLiteDatabase) {
     bd.execSQL("CREATE TABLE lugares (" +
          "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
          "nombre TEXT, " +
          "direccion TEXT, " +
          "longitud REAL, " +
          "latitud REAL, " +
          "tipo INTEGER, " +
          "foto TEXT, " +
          "telefono INTEGER, " +
          "url TEXT, " +
          "comentario TEXT, " +
          "fecha BIGINT, " +
          "valoracion REAL)")
    bd.execSQL(("INSERT INTO lugares VALUES (null, " +
       "'Escuela Politécnica Superior de Gandía', " +
       "'C/ Paranimf, 1 46730 Gandia (SPAIN)', -0.166093, 38.995656, "+
       TipoLugar.EDUCACION.ordinal + ", '', 962849300, " +
       "'http://www.epsg.upv.es', " +
       "'Uno de los mejores lugares para formarse.', " +
       System.currentTimeMillis() + ", 3.0)"))
    bd.execSQL(("INSERT INTO lugares VALUES (null, 'Al de siempre', " +
       "'P.Industrial Junto Molí Nou - 46722, Benifla (Valencia)', " +
       " -0.190642, 38.925857, " + TipoLugar.BAR.ordinal + ", '', " +
       "636472405, '', " + "'No te pierdas el arroz en calabaza.', " +
       System.currentTimeMillis() + ", 3.0)"))
    bd.execSQL(("INSERT INTO lugares VALUES (null, 'androidcurso.com', "+
      "'ciberespacio', 0.0, 0.0,"+TipoLugar.EDUCACION.ordinal+", '', "+
      "962849300, 'http://androidcurso.com', " +
      "'Amplia tus conocimientos sobre Android.', " +
      System.currentTimeMillis() + ", 5.0)"))
    bd.execSQL(("INSERT INTO lugares VALUES (null,'Barranco del Infierno',"+
      "'Vía Verde del río Serpis. Villalonga (Valencia)', -0.295058, "+
      "38.867180, " + TipoLugar.NATURALEZA.ordinal + ", '', 0, " +
      "'http://sosegaos.blogspot.com.es/2009/02/lorcha-villalonga-via-verde-del-"+
      "rio.html', 'Espectacular ruta para bici o andar', " +
      System.currentTimeMillis() + ", 4.0)"))
    bd.execSQL(("INSERT INTO lugares VALUES (null, 'La Vital', " +
      "'Avda. La Vital,0 46701 Gandia (Valencia)',-0.1720092,38.9705949,"+
      TipoLugar.COMPRAS.ordinal + ", '', 962881070, " +
      "'http://www.lavital.es', 'El típico centro comercial', " +
      System.currentTimeMillis() + ", 2.0)"))
   }

   override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, 
                                              newVersion: Int) {}
} 

El constructor de la clase se limita a llamar al constructor heredado. Los parámetros se describen a continuación:
 

contexto: Contexto usado para abrir o crear la base de datos.

nombre: Nombre de la base de datos que se creará. En nuestro caso, “puntuaciones”.

cursor: Se utiliza para crear un objeto de tipo cursor. En nuestro caso no lo necesitamos.

version: Número de versión de la base de datos empezando desde 1. En el caso de que la base de datos actual tenga una versión más antigua se llamará a onUpgrade() para que actualice la base de datos.

El método onCreate() se invocará cuando sea necesario crear la base de datos. Como parámetro se nos pasa una instancia de la base de datos que se acaba de crear. Este es el momento de crear las tablas que contendrán información. El primer campo tiene por nombre _id y será un entero usado como clave principal. Su valor será introducido de forma automática por el sistema, de forma que dos registros no tengan nunca el mismo valor.

En nuestra aplicación necesitamos solo la tabla lugares, que es creada por medio del comando SQL CREATE TABLE lugares… La primera columna tiene por nombre _id y será un entero usado como clave principal. Su valor será introducido automáticamente por el sistema, de forma que dos filas no tengan nunca el mismo valor de _id.

Las siguientes líneas introducen nuevas filas en la tabla utilizando el comando SQL INSERT INTO lugares VALUES ( , , … ). Los valores deben introducirse en el mismo orden que las columnas. La primera columna se deja como null dado que corresponde al _id y es el sistema quien ha de averiguar el valor correspondiente. Los valores de tipo TEXT deben introducirse entre comillas, pudiendo utilizar comillas dobles o simples. Como en Java se utilizan comillas dobles, en SQL utilizaremos comillas sencillas. El valor TipoLugar.EDUCACION.ordinal() corresponde a un entero según el orden en la definición de este enumerado y System.currentTimeMillis() corresponde a la fecha actual representada como número de milisegundos transcurridos desde 1970. El resto de los valores son sencillos de interpretar.
Ha de quedar claro que este constructor solo creará una base de datos (llamando a onCreate()) siestá todavía no existe. Si ya fue creada en una ejecución anterior, nos devolverá la base de datos existente.

El método onUpgrade() está vacío. Si más adelante, en una segunda versión de Mis Lugares, decidiéramos crear una nueva estructura para la base de datos, tendríamos que indicar un número de versión superior, por ejemplo la 2. Cuando se ejecute el código sobre un sistema que disponga de una base de datos con la versión 1, se invocará el método onUpgrade(). En él tendremos que escribir los comandos necesarios para transformar la antigua base de datos en la nueva, tratando de conservar la información de la versión anterior.

3.     Para acceder a los datos de la aplicación se definió la interfaz RepositorioLugares. Vamos a implementar esta interfaz para que los cambios sean los mínimos posibles. Añade el texto subrayado a la clase:

public class LugaresBD extends SQLiteOpenHelper
                       implements RepositirioLugares { 
class LugaresBD(val contexto: Context) :
      SQLiteOpenHelper(contexto, "lugares", null, 1), RepositorioLugares { 

Aparecerá un error justo en la línea que acabas de introducir. Si sitúas el cursor de texto sobre el error, aparecerá una bombilla roja con opciones para resolver el error. Pulsa en “Implement methods”, selecciona todos los métodos y pulsa OK. Observa como en la clase se añaden todos los métodos de esta interfaz. De momento vamos a dejar estos métodos sin implementar. En la sección  “Operaciones con bases de datos en Mis Lugares” aprenderemos a realizar las operaciones básicas cuando trabajamos con datos: altas, bajas, modificaciones y consultas.

4.     No ejecutes todavía la aplicación. Hasta que no hagamos el siguiente ejercicio no funcionará correctamente.

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 RepositorioLugares, 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 consultas a la base de datos cada vez que requiera una información de Lugares. (Veremos más adelante que cada llamada a elemento(), añade(), nuevo(), … va a suponer un accedo 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. En la aplicación el Cursor será cargado al mostrar el listado inicial en MainActivity. Cuando el usuario quiera mostrar el detalle de algún lugar, no será necesario hacer una nueva consulta a la base de datos, dado que la información ya está en el Cursor.

Ejercicio: Un Adaptador para base de datos en Mis Lugares

1.     Crea la clase AdaptadorLugaresBD, en el paquete presentacion, con el siguiente código:

public class AdaptadorLugaresBD extends AdaptadorLugares {

    protected Cursor cursor;

    public AdaptadorLugaresBD(RepositorioLugares 
                                                lugares, Cursor cursor) {
        super(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);
        if (cursor.getCount()>0) return cursor.getInt(0);
        else                     return -1;s

    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int posicion) {
        Lugar lugar = lugarPosicion(posicion);
        holder.personaliza(lugar);
        holder.itemView.setTag(new Integer(posicion));
    }

    @Override public int getItemCount() {
        return cursor.getCount();
    }
} 
class AdaptadorLugaresBD(lugares: LugaresBD, var cursor: Cursor): AdaptadorLugares(lugares){

   fun lugarPosicion(posicion: Int): Lugar {
      cursor.moveToPosition(posicion)
      return (lugares as LugaresBD).extraeLugar(cursor)
   }

   fun idPosicion(posicion: Int): Int {
      cursor.moveToPosition(posicion)
      if (cursor.count>0) return cursor.getInt(0)
      else                return -1
   }

   override fun onBindViewHolder(holder: AdaptadorLugares.ViewHolder,
                                                        posicion: Int) {
      val lugar = lugarPosicion(posicion)
      holder.personaliza(lugar, onClick)
      holder.view.tag = posicion 
   }

   override fun getItemCount(): Int {
      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, y 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.

En Java el constructor de la clase se limita a llamar al super() y a almacenar el nuevo parámetro en una variable global. En Kotlin este proceso se indica en la declaración. En Java se han añadido los métodos getter 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 id de 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.

Observa la última línea de onBindViewHolder() (holder.view.tag = posicion). El atributo Tag permite asociar a una vista cualquier objeto con información extra. La idea es asociar a cada vista del RecyclerView la posición que ocupa en el listado. Así, cuando asociamos un onClickListener este nos indica la vista pulsada, pero no la posición. De esta forma, sabiendo la vista conoceremos su posición. En la implementación anterior usábamos un método alternativo: posición = recyclerView.getChildAdapterPosition(vista). Pero tiene el inconveniente de necesitar el recyclerView. Y ahora no vamos a disponer de él.

En Kotlin tanto las clase como los atributos son por defecto cerrados . Por lo tanto, aparece un error al intentar heredar de AdaptadorLugares. Para resolverlo pulsa sobre la bombilla roja y selecciona Make AdaptadorLugares Open.

2.     Añade a la clase LugaresBD el siguiente método estático:

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);
} 
fun extraeLugar(cursor: Cursor) = Lugar(
   nombre = cursor.getString(1),
   direccion = cursor.getString(2),
   posicion = GeoPunto(cursor.getDouble(3), cursor.getDouble(4)),
   tipoLugar = TipoLugar.values()[cursor.getInt(5)],
   foto = cursor.getString(6),
   telefono = cursor.getInt(7),
   url = cursor.getString(8),
   comentarios = cursor.getString(9),
   fecha = cursor.getLong(10),
   valoracion = cursor.getFloat(11) )

fun extraeCursor(): Cursor =
   readableDatabase.rawQuery("SELECT * FROM lugares",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 Aplicacion y reemplaza la declaración de la variable lugares y adaptador:

public RepositorioLugaresBD lugares;
public AdaptadorLugaresBD adaptador;

@Override public void onCreate() {
   super.onCreate();
   lugares = new LugaresBD(this);
   adaptador= new AdaptadorLugaresBD(lugares, lugares.extraeCursor()); 
} 
val lugares = LugaresBD(this)
val adaptador by lazy {
   AdaptadorLugaresBD(lugares, lugares.extraeCursor())} 

Hemos cambiado la declaración de lugares y adaptador para utilizar las nuevas clases. En Kotlin la inicialización de las propiedades se recomienda realizarla en su declaración. Observa como para adaptador se utiliza by lazy, para indicar que la inicialización se realice cuando vallamos a utilizar la variable. De hacerlo inmediatamente corremos el peligro de que la base de datos no esté creada.

4.     En Java, modifica las siguientes propiedades de MainActivity:

private RepositorioLugaresBD lugares;
private AdaptadorLugaresBD adaptador;
5.     Reemplaza en MainActivity dentro de onCreate() el código subrayado:
adaptador.setOnItemClickListener(new View.OnClickListener() {
   @Override public void onClick(View v) {
      int pos =(Integer)(v.getTag());
      usoLugar.mostrar(pos);
   }
}); 
adaptador.onClick = {            
   val pos = it.tag as Int
   usoLugar.mostrar(pos)
} 

6.     Ejecuta la aplicación y verifica que la listas se muestra correctamente. Si pulsas sobre un lugar se producirá un error.

Ejercicio:  Una caché para evitar accesos a los datos

Cuando la información que visualiza la aplicación se almacena en un servidor externo (puede ser en la nube o una base de datos) hay que tratar de minimizar el número de acceso al servidor. En estos casos, es frecuente almacenar en una memoria local esta información, para evitar accesos en caso de volver a necesitar la información. Esta técnica se conoce en informática como caché.

En este ejercicio vamos a crear una clase inspirada en este concepto. Realmente no crearemos una estructura de datos para implementar la caché, si no que aprovecharemos que tenemos los datos en el adaptador para no hacer nuevos accesos.

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

public class LugaresBDAdapter extends LugaresBD {

   private AdaptadorLugaresBD adaptador;

   public LugaresBDAdapter(Context contexto) {
      super(contexto);
   }

   public Lugar elementoPos(int pos) { 
      return adaptador.lugarPosicion (pos);
   }
}
class LugaresBDAdapter(val contexto: Context) : LugaresBD(contexto) {
   val adaptador: AdaptadorLugaresBD

   fun elementoPos(pos: Int) = adaptador.lugarPosicion(pos)   
}

Al extender de LugaresDB conseguimos que herede el comportamiento de RepositorioLugares. Añadimos el atributo adaptador que es la estructura que actuará como cache. El constructor se limita a llamar al constructor del padre. De momento solo sobrescribimos un método más, elementoPos(), que devolverá un elemento dada su posición.

Si te fijas hemos repartido el código en dos clases. En LugaresDB resolveremos el acceso a la base de datos y en LugaresDBAdapter añadimos la caché utilizando un adaptador.

2.     En Java, añade el getter y el setter para adaptador (opción Generate… > Getter and Setter).

3.     En la clase Aplicacion reemplaza la declaración de la variable lugares para que sea de la nueva clase y añade al final de onCreate():

lugares = new LugaresBDAdapter(this);
adaptador= new AdaptadorLugaresBD(lugares, lugares.extraeCursor());
lugares.setAdaptador(adaptador);
val lugares = LugaresBDAdapter(this)
…
lugares.adaptador = adaptador

4.     En Java, modifica las clases MainActivity, VistaLugarActivity, EdicionLugarActivity y CasosUsoLugar haz que lugares sea de tipo LugaresDBAdapter. En Kotlin, no es necesario, el tipo no se indica al venir de la declaración en Aplicacion.

5.     En VistaLugarActivity, EdicionLugarActivity y CasosUsosLugar modifica la siguiente línea:

lugar = lugares.elementoPos(pos);
lugar = lugares.elementoPos(pos)

Al entrar en la vista de un lugar podemos obtenerlo del adaptador a partir de su posición.

6.     Ejecuta la aplicación y verifica que funciona. 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:  Ejercicio: Adaptando la actividad del Mapa a LugaresDBAdapter

En este ejercicio adaptaremos la actividad MapaActivity para que use adecuadamente la nueva forma de acceder a los lugares. Es decir, los lugares a mostrar en el mapa los obtendremos directamente del adaptador, en lugar de hacer una nueva consulta a la base de datos.

1.     En MapaActivity modifica la siguiente propiedad. Solo es necesario en Java.

private RepositorioLugaresBDAdapter lugares; 

2.     Modifica las tres llamadas a lugares.elemento() por lugares.elementoPos(). Así los lugares son extraídos del adaptador.

3.     En LugaresBDAdapter añade la siguiente función:

@Override public int tamaño(){
   return adaptador.getItemCount();
}
override fun tamaño(): Int = adaptador.itemCount 

Sobrescribimos la función para que cuando trabajemos con un LugaresBDAdapter, el número total de elementos corresponda con los que se estén listando en el RecyclerView.

4.     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.
 

Práctica: Añadir criterios de ordenación y máximo en Preferencias

1.     Modifica el método extraeCursor() para que el criterio de ordenación y el máximo de lugares a mostrar corresponda con  los valores que el usuario ha indicado en las preferencias.

2.     Si el usuario escoge el primer criterio de ordenación has de dejar la consulta original sin introducir la clausula “ORDER BY”.

3.     Si escoge el orden por valoración este ha de ser descendiente, de más valorados a menos. Puedes usar la clausula “ORDER BY valoracion DESC”.

4.     Para ordenar por distancia puedes usar la siguiente consulta SQL:
 

"SELECT * FROM lugares ORDER BY " +
        "(" + lon + "-longitud)*(" + lon + "-longitud) + " +
        "(" + lat + "-latitud )*(" + lat + "-latitud )" 

Donde las variables lon y lat han de corresponder con la posición actual del dispositivo. Esta ecuación es una simplificación que no tiene en cuenta que los polos están achatados, pero funciona de forma adecuada.
 

5.     Si no actualizamos el cursor con la lista el cambio de preferencias no será efectivo hasta que salgas de aplicación y vuelvas a entrar. Para evitar este incoveniente, llama a la actividad PreferenciasActivity mediante startActivityForResult(). En el método onActivityResult() has de actualizar el cursor de adaptador e indicar que todos los elementos han de redibujarse. Para esta última acción puedes utilizar adaptador.notifyDataSetChanged().

Solución:

1.     Reemplaza en lugaresBD el siguiente método:

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 = ((Aplicacion) contexto.getApplicationContext())
                                           .posicionActual.getLongitud();
            double lat = ((Aplicacion) contexto.getApplicationContext())
                                           .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);
} 
fun extraeCursor(): Cursor {
   val pref = PreferenceManager.getDefaultSharedPreferences(contexto)
   var consulta = when (pref.getString("orden", "0")) {
      "0" -> "SELECT * FROM lugares "
      "1" -> "SELECT * FROM lugares ORDER BY valoracion DESC"
      else -> {
         val lon = (contexto.getApplicationContext() as Aplicacion)
            .posicionActual.longitud
         val lat = (contexto.getApplicationContext() as Aplicacion)
            .posicionActual.latitud
         "SELECT * FROM lugares ORDER BY " +
                 "($lon - longitud)*($lon - longitud) + " +
                 "($lat - latitud )*($lat - latitud )"
      }
   }
   consulta += " LIMIT ${pref.getString("maximo", "12")}"
   return readableDatabase.rawQuery(consulta, null)
} 

2.     En MainActivity añade. NOTA: Si utilizas casos de uso tendrás que adaptar este código.

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) {
   super.onActivityResult(requestCode, resultCode, data);
   if (requestCode == RESULTADO_PREFERENCIAS) {
      adaptador.setCursor(lugares.extraeCursor();
      adaptador.notifyDataSetChanged();
   }
} 
val RESULTADO_PREFERENCIAS = 0

fun lanzarPreferencias(view: View? = null) = startActivityForResult(
   Intent(this, PreferenciasActivity::class.java), RESULTADO_PREFERENCIAS)

override 
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
   super.onActivityResult(requestCode, resultCode, data)
   if (requestCode == RESULTADO_PREFERENCIAS) {
      adaptador.cursor = lugares.extraeCursor()
      adaptador.notifyDataSetChanged()
   }
} 

Preguntas de repaso:  SQLite I