Uso de base de datos en Android

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 un nuevo nombre del proyecto.

2.     Crea la clase LugaresBD en el proyecto  y escribe 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 Lugares. 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 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 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 una 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.

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(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;

        return cursor.getInt(0);
    }

    @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, 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.

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. La ideas es que 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 LugaresBD 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 pasado adaptador dentro de Aplicacion para que sea accesible desde cualquier clase. Ahora la lista de lugares que el usuario a buscado en la base de datos estará almacenada dentro de adaptador. Y queremos acceder a esta lista desde varios sitios. 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.     Añade en MainActivity la siguiente propiedad:

private RepositorioLugaresBD lugares;
private AdaptadorLugaresBD adaptador;

@Override protected void onCreate(Bundle savedInstanceState) {
   …
   adaptador = ((Aplicacion) getApplication()).adaptador;
} 
val adaptador by lazy { (application as Aplicacion).adaptador } 
5.     Reemplaza en MainActivity dentro de onCreate() en 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.

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

lugar = lugares.elemento adaptador.lugarPosicion (pos);
lugar = lugares.elemento adaptador.lugarPosicion (pos) 

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 la lista almacenada en adaptador.

8.     En la clase EdicionLugarActivity realiza la misma operación.
9.     En la clase CasosUsoLugar pasaremos el adaptador al constructor:
 

private RepositorioLugaresBD lugares;
private AdaptadorLugaresBD adaptador;

public CasosUsoLugar(Activity actividad, 
                     RepositorioLugares LugaresBD lugares,
                     AdaptadorLugaresBD adaptador) {
   …
   this.adaptador = adaptador;
} 
class CasosUsoLugar(
   val actividad: Activity,
   val lugares: RepositorioLugares LugaresBD,
   val adaptador: AdaptadorLugaresBD) 
10.   Añade el nuevo parámetro en las llamadas a este constructor.

11.     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:  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 Aplicacion.lugares por Aplicacion.adaptador.

1.     Reemplaza inicialización de la variable lugares por adaptador:

private RepositorioLugares lugares;
private AdaptadorLugaresBD adaptador;

@Override public void onCreate(Bundle savedInstanceState) {
   …
   lugares = ((Aplicacion) getApplication()).lugares;
   adaptador = ((Aplicacion) getApplication()).adaptador; 
val lugares by lazy { (application as Aplicacion).lugares }
val adaptador by lazy { (application as Aplicacion).adaptador } 

2.     Reemplaza el código del método onMapReady():

if (lugares.tamanyo() adaptador.getItemCount() > 0) {
    GeoPunto p= lugares.elemento adaptador.lugarPosicion(0).getPosicion();
…
for (int n=0; n< lugares.tamanyo() adaptador.getItemCount(); n++) {
    Lugar lugar = lugares.elemento adaptador.lugarPosicion(n); 
if (lugares.tamanyo() adaptador.itemCount > 0) {
   val p = lugares.elemento adaptador.lugarPosicion(0).posicion
…
for (n in 0 until lugares.tamanyo() adaptador.itemCount) {
   val lugar = lugares.elemento adaptador.lugarPosicion(n) 

3.     En el método onInfoWindowClick() reemplaza:

for (int pos=0; pos< lugares.tamaño() adaptador.getItemCount(); pos++){
    if (lugares.elemento adaptador.lugarPosicion(pos).getNombre() 
for (pos in 0 until lugares.tamanyo() adaptador.itemCount) {
   val lugar = lugares.elemento adaptador.lugarPosicion(pos) 

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

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 adaptado e indicar 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:

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(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?) {
   if (requestCode == RESULTADO_PREFERENCIAS) {
      adaptador.cursor = lugares.extraeCursor()
      adaptador.notifyDataSetChanged()
   }
} 

Preguntas de repaso:  SQLite I