Tutoriales Android

Introducción

Introducción

Las aplicaciones descritas hasta este capítulo representaban la información a procesar en forma de variables. El problema de estas variables es que dejan de existir en el momento en que la aplicación es destruida. En muchas ocasiones vamos a necesitar almacenar información de manera permanente. Las alternativas más habituales para conservar esta información son los ficheros, las bases de datos o servicios a través de la red. Estas técnicas no solo permiten mantener a buen recaudo los datos de la aplicación, sino que también vamos a poder compartir estos datos con otras aplicaciones y usuarios. De forma adicional, el sistema Android pone a nuestra disposición dos nuevos mecanismos para almacenar datos, las preferencias y ContentProvider.

A lo largo de este capítulo estudiaremos cómo utilizar estas técnicas.  Comenzaremos describiendo el uso de las preferencias como un mecanismo sencillo para guardar de forma permanente algunas variables. Seguiremos describiendo las características del sistema de ficheros que incorpora Android. Se puede acceder a los ficheros a través de las clases estándar incluidas en Java. De forma adicional se incluyen nuevas clases para cubrir las peculiaridades de Android.

Como tercera alternativa se estudiará el uso de XML para almacenar la información de manera estructurada. Se describirán dos herramientas alternativas, las librerías SAX y JSON Como cuarta alternativa al almacenamiento de datos se estudiarán las bases de datos. Android incorpora la librería SQLite, que nos permitirá crear y manipular nuestras propias bases de datos de forma muy sencilla. Para finalizar, se describirá la clase ContentProvider  que consiste en un mecanismo introducido en Android para poder compartir datos entre aplicaciones.

En el capítulo siguiente se describe otra alternativa, el uso de Internet como recurso para almacenar y compartir información. Concretamente se describirá el uso de sockets TCP, HTML y los servicios web.

Todas estas alternativas serán ilustradas a través del mismo ejemplo. Trataremos de almacenar la lista con las mejores puntuaciones obtenidas en Asteroides que fue descrito en el capítulo 3.

Objetivos:

  • Repasar las alternativas para el almacenamiento de datos en Android.
  • Describir el uso de ficheros.
  • Ilustrar la utilización de dos herramientas para manipular ficheros XML, las librerías SAX y DOM.
  • Mostrar como desde Android podemos utilizar SQLite para trabajar con bases de datos.
  • Describir que es un ContentProvider y cómo podemos utilizar algunos ContentProvider disponibles en Android.
  • Aprender a crear nuestros propios ContentProvider.

Alternativas para guardar datos permanentemente en Android

Existen muchas alternativas para almacenar información de forma permanente en un sistema informático. A continuación mostramos una lista de las más habituales utilizadas en Android:

  • Preferencias: Es un mecanismo liviano que permite almacenar y recuperar datos primitivos en la forma de pares clave/valor. Este mecanismo se suele utilizar para almacenar los parámetros de configuración de una aplicación.
  • Ficheros: Puedes almacenar los ficheros en la memoria interna del dispositivo o en un medio de almacenamiento removible como una tarjeta SD. También puedes utilizar fichero añadidos a tu aplicación como recursos.
  • XML: Se trata de un estándar fundamental para la representación de datos, en Internet y en muchos otros entornos (como en el Android SDK). En Android disponemos de las librerías SAX y DOM para manipular datos en XML.
  •  Base de datos: Las APIs de Android contienen soporte para SQLite. Tu aplicación puede crear y usar base de datos SQLite de forma muy sencilla y con toda la potencia que nos da el lenguaje SQL.
  • Proveedores de contenidos: Un proveedor de contenidos es un componente opcional de una aplicación que expone el acceso de lectura / escritura de sus datos a otras aplicaciones.. Está sujeto a las restricciones de seguridad que quieras imponer. Los proveedores de contenido implementan una sintaxis estándar para acceder a sus datos mediante URI (Uniform Resource Identifiers) y un mecanismo de acceso para devolver los datos similar a SQL. Android provee algunos proveedores de contenido para tipos de datos estándar, tales como contactos personales, ficheros multimedia, etc.
  • Internet: No te olvides que también puedes usar la nube para almacenar y recuperar datos.

video[Tutorial]  Almacenamiento de datos en  Android

Añadiendo puntuaciones en Asteroides

A modo de ejemplo se va a implementar la posibilidad de guardar las mejores puntuaciones obtenidas en Asteroides. Se utilizarán mecanismos alternativos que serán desarrollados a lo largo de este capítulo y el siguiente:

  • Array (implementado en el CAPÍTULO 3)
  • Preferencias
  • Ficheros en memoria interna
  • Fichero en memoria externa
  • Fichero en recursos
  • XML con SAX
  • JSON con GSon
  • JSON con JSon
  • Base de datos SQLite relacionales
  • Content Provider
  • Internet a través de sockets
  • Servicios Web


Para facilitar la sustitución del método de almacenamiento en el capítulo 3 se ha creado la siguiente interfaz en la aplicación Asteroides:

public interface AlmacenPuntuaciones {
   public void guardarPuntuacion(int puntos, String nombre, long fecha);
   public Vector<String> listaPuntuaciones(int cantidad);
}

También se declaró la variable almacen de tipo AlmacenPuntuaciones y se ha creado la actividad Puntuaciones que visualizaba un ListView con las puntuaciones. Dado que en capítulo  3 todavía no teníamos la opción de jugar, no se podían añadir nuevas puntuaciones a almacen. En el siguiente ejercicio tratremos de calcular una puntuación en el juego y almacenarla en almacen.

Ejercicio paso a paso: Calculando la puntuación en Asteroides

1.     Crea una variable global en la clase VistaJuego que se llame puntuacion e inicialízala a cero:

private int puntuacion = 0;

2.     Cada vez que se destruya un asteroide hay que incrementar esta variable. añade dentro de destruyeAsteroide() la siguiente línea:

puntuacion += 1000;

3.     Cuando desde la actividad inicial Asteroides, se llame a la actividad Juego  nos interesa que esta nos devuenva la puntuación obtenida. Recuerda como en el Capítulo 3 hemos estudiado la comunicación entre actividades. Para pasar la información entre las actividades añade en siguiente código en Asteroides en sustitución del método lanzarJuego() anterior:

public void lanzarJuego(View view) {
   Intent i = new Intent(this, Juego.class);
   startActivityForResult(i, 1234);
}

@Override protected void onActivityResult (int requestCode,
                                    int resultCode, Intent data){
   super.onActivityResult(requestCode, resultCode, data);
   if (requestCode==1234 && resultCode==RESULT_OK && data!=null) {
      int puntuacion = data.getExtras().getInt("puntuacion");
      String nombre = "Yo";
      // Mejor leer nombre desde un AlertDialog.Builder o preferencias
      almacen.guardarPuntuacion(puntuacion, nombre,
                                    System.currentTimeMillis());
      lanzarPuntuaciones(null);
   }
}

4.     Para realizar la respuesta de la actividad va a ser más sencillo hacerlo desde VistaJuego que desde  Juego. El problema es que esta clase es una vista, no una actividad. Para solucionar el problema puedes usar el siguiente truco. Introduce en VistaJuego el siguiente código:

private Activity padre;
 
public void setPadre(Activity padre) {
    this.padre = padre;
}

5.     Cuando se detecte una condición de victoria o derrota es un buen momento para almacenar la puntuación y salir de la actividad. Para ello, crea el siguiente método dentro de VistaJuego:

private void salir() {
   Bundle bundle = new Bundle();
   bundle.putInt("puntuacion", puntuacion);
   Intent intent = new Intent();
   intent.putExtras(bundle);
   padre.setResult(Activity.RESULT_OK, intent);
   padre.finish();
}

6.     Al final del método destruyeAsteroide() introduce:

if (asteroides.isEmpty()) {
          salir();
}

7.     Al final del método actualizaFisica() introduce:

for (Grafico asteroide : asteroides) {
    if (asteroide.verificaColision(nave)) {
       salir();
    }
}

8.     En el método onCreate de Juego introduce:

vistaJuego.setPadre(this);

 

Preferencias

 

 

Las preferencias (clase SharedPreferences) pueden ser usadas como un mecanismo para que los usuarios modifiquen algunos parámetos de configuración de la aplicación. Este uso fue estudiado en el capítulo 3, donde se describe como podíamos crear una actividad desdendiente de PreferenceFragment para que el usuario consulte y modifique estas preferencias.

 Las preferencias también pueden ser utilizadas como un mecanismo liviano para almacena ciertos datos que tu aplicación quiera consevar de forma permanente. Es un mecanismo sencillo que te permite almacenar una serie de variables con su nombre y su valor. Puedes almacenar variables de tipo booleano, real, enteroString. En este apartado describimos su utilización.

Las preferencias son almacenadas en ficheros xml dentro de la carpeta shared_prefs en los datos de la aplicación. Recuerda que en el capítulo 3 vimos como las preferencias de usuario siempre se almacenaban en el fichero paquete_preferences , donde paquete ha de ser reemplazado por el paquete de la aplicación (en Asteroides el fichero es org.example.asteroides_preferences). Cuando utilices las preferencias para almacenar otros valores podrás utilizar otros ficheros. Tienes dos alternativas según utilices uno de los siguientes métodos:

getSharedPreferences() Te permite indicar de forma explicita el nombre de un fichero de preferencias. Puedes utilizarlo cuando necesites varios ficheros de preferencias o acceder al mismo fichero desde varias actividades.

getPreferences() No tienes que indicar ningún nombre. Puedes utilizarlo cuando solo necesites un fichero de preferencias en la actividad.

Estos dos métodos necesitan como parámetro el tipo de permiso que queramos dar al fichero de preferencias. Los valores posibles son  MODE_PRIVATE,  MODE_WORLD_READABLE o MODE_WORLD _ WRITEABLE  según queramos tener acceso exclusive, permir la lectura o permitir lectura y escritura.

Una llamada a uno de estos dos métodos te devolverá un objeto de la clase SharedPreferences.

Para escribir las preferencias puedes utilizar el siguiente código:

 SharedPreferences preferencias= getPreferences(MODE_PRIVATE);
 SharedPreferences.Editor editor = preferencias.edit();
 editor.putString("nombre""Juan");
 editor.putInt("edad"35);
 editor.commit();

Para leer las preferencias puedes utilizar el siguiente código:

 

SharedPreferences preferencias = getPreferences(MODE_PRIVATE);

String nombre = preferencias.getString("nombre","valor por defecto");

int edad = preferencias.getInt("edad"-1);

 

El ejemplo anterior puede ser modificado  reemplazando getPreferences() por getSharedPreferences(). En este caso, tendrás que indicar el fichero donde se almacenarán las preferencias.

  video[TutorialAlmacenar información usando Preferencias

 

Ejercicio paso a paso: Almacenando la última puntuación en un fichero de preferencias.

 

Veamos un ejemplo de cómo podemos crear un fichero de preferencias para almacenar la última puntuación obtenida en Asteroides.

1.     Abre el proyecto Asteroides.

2.     Crea una nueva clase AlmacenPuntuacionesPreferencias.

3.     Reemplaza el código por el siguiente:

public class AlmacenPuntuacionesPreferencias implements AlmacenPuntuaciones {
   private static String PREFERENCIAS = "puntuaciones";
   private Context context;

   public AlmacenPuntuacionesPreferencias(Context context) {
      this.context = context;
   }

   public void guardarPuntuacion(int puntos, String nombre,
                                                long fecha) {
      SharedPreferences preferencias =context.getSharedPreferences(
            PREFERENCIAS, Context.MODE_PRIVATE);
      SharedPreferences.Editor editor = preferencias.edit();
      editor.putString("puntuacion", puntos + " " + nombre);
      editor.commit();
   }

   public Vector<String> listaPuntuaciones(int cantidad) {
      Vector<String> result = new Vector<String>();
      SharedPreferences preferencias =context.getSharedPreferences(
                              PREFERENCIAS, Context.MODE_PRIVATE);
      String s = preferencias.getString("puntuacion", "");
      if (!s.isEmpty()) {
         result.add(s);
      }
      return result;
   }
}      

 

4.     Abre el fichero Asteroides.java y modifica el método onCreate() para que la variable almacen se inicialice de  la siguiente manera:

almacen = new AlmacenPuntuacionesPreferencias(this);    

5.     Ejecuta el proyecto y verifica que la última puntuación se guarda correctamente.

6.     Pulsa el botón Android Device Monitor ( ) de la barra de herramientas y selecciona la lengüeta File Explorer. Verifica que se ha creado el fichero /data/data/org.example.asteroides/shared_prefs/puntuaciones.xml

7.     Descarga este fichero a tu ordenador (botón ) y observa su contenido.
 

Práctica: Almacenando las úlitmas 10 puntuaciones en un fichero de preferencias.

En el ejercicio anterior solo guardamos la última puntuación, lo cual no coincide con la idea que habíamos plantado en un principio, nos interesaba guardar una lista con las últimas puntuaciones. En esta práctica has de tratar de solucionar este inconveniente. NOTA: Se trata de una práctica básicamente de progragramación en Java. Si no estás interesado puedes consultar directamente la solución.

1.     Las preferencias solo están preparadas para almacenar variables de tipos simple, por lo que no permiten almacenar un vector. Para solucionar este inconveniente, te recomendamos que crees 10 preferencias con nombres puntuacion0puntuacion1, … , puntuacion9.

2.     Cuando se llame a guardarPuntuacion() almacena la nueva puntuación en puntuacion0. Pero antes ten la precaución de copiar el valor de esta preferencia a puntuacion1; y puntuacion1 apuntuacion2 y así hasta la penúltima. La última se perderá. Esta operación puede realizarse por medio de un bucle con un índice entero, n, de forma que el nombre de la preferencia a mover puedes expresarlo como “puntuacion”+n.

3.     Utiliza el mismo truco para implementar el método ListaPuntuaciones()

Solución: Almacemando las úlitmas 10 puntuaciones en un fichero de preferencias.

1.     Reemplaza en guardarPuntuacion():

editor.putString("puntuacion", puntos + " " + nombre);

   por:

for (int n = 9; n >= 1; n--) {
   editor.putString("puntuacion" + n,
               preferencias.getString("puntuacion" + (n - 1), ""));
}
editor.putString("puntuacion0", puntos + " " + nombre);

2.     Reemplara en ListaPuntuaciones():

String s = preferencias.getString("puntuacion", "");
if (!s.isEmpty()) {
   result.add(s);
}

 por:

for (int n = 0; n <= 9; n++) {
   String s = preferencias.getString("puntuacion" + n, "");
   if (!s.isEmpty()) {
      result.add(s);
   }
}    

 

Accediendo a ficheros

Existen tres tipos de ficheros donde podemos almacenar información en Android: ficheros almacenados en la memoria interna del teléfono, ficheros almacenados en la memoria externa (normalmente una tarjeta SD) y ficheros almacenados en los recursos. Estos últimos son de solo lectura, por lo que no son útiles para almacenar información desde la aplicación. Cuando programes en Android debes tener en cuenta que un dispositivo móvil tiene una capacidad de almacenamiento limitada.

  video[TutorialGestión de ficheros en Android

 

Sistema interno de ficheros

 

Sistema interno de ficheros

Android permite almacenar ficheros en la memoria interna del teléfono. Por defecto, los ficheros almacenados solo son accesibles para la aplicación que los creó, no pueden ser leídos por otras aplicaciones, ni siquiera por el usuario del teléfono. Cada aplicación dispone de una carpeta especial para almacenar ficheros (/data/data/nombre_del_paquete/files). La ventaja de utilizar esta carpeta es que cuando se desinstala la aplicación los ficheros que has creado se eliminarán. Cuando trabajes con ficheros en Android, ten siempre en cuenta que la memoria disponible de los teléfonos móviles es limitada.

Recuerda que el sistema de ficheros se sustenta en la capa Linux, por lo que Android hereda su estructura. Cuando se instala una nueva aplicación, Android crea un nuevo usuario Linux asociado a la aplicación y es este usuario el que podrá o no acceder a los ficheros.

 

Puedes utilizar cualquier rutina del paquete java.io para trabajar con ficheros. Adicionalmente se han creado métodos adicionales asociados a la clase Contex para facilitarte el trabajo con ficheros almacenados en la memoria interna.

En particular los métodos  openFileInput()  y  openFileOutput()  te permiten abrir un fichero para lectura o escritura respectivamente. Si utilizas estos métodos el nombre del archivo no puede contener subdirectorios. De hecho el fichero siempre se almacena en la carpeta reservada para tu aplicación (/data/data/nombre_del_paquete/files). Recuerda siempre cerrar los ficheros con el método close(). El siguiente ejemplo muestra cómo crear un fichero y escribir en él un texto:

String fichero = "fichero.txt";

String texto = "texto almacenado";

           FileOutputStream fos;

           try {

             fos = openFileOutput(fichero,Context.MODE_PRIVATE);

             fos.write(texto.getBytes());

             fos.close();

            catch (FileNotFoundException e) {

             Log.e("Mi Aplicación",e.getMessage(),e);

            } catch (IOException e) {

             Log.e("Mi Aplicación",e.getMessage(),e);

            }

Es muy importante hacer un manejo cuidadoso de los errores. De hecho, el acceso a ficheros ha de realizarse de forma obligatoria dentro de una sección try/catch.

Además de los dos métodos indicados pueden serte útiles algunos de los siguientes: getFilesDir() devuelve la ruta absoluta donde se están guardando los ficheros.  getDir() crea un directorio en tu almacenamiento interno (o lo abre si existe).  deleteFile() borra un fichero.  fileList() devuelve un array con los ficheros almacenados por tu aplicación.

Ejercicio paso a paso: Almacenando puntuaciones en un fichero de la memoria interna

El siguiente ejercicio muestra una clase que implementa la interfaz AlmacenPuntuaciones utilizando los métodos antes descritos

1.     Abre el proyecto Asteroides.

2.     Crea una nueva clase AlmacenPuntuacionesFicheroInterno.

3.     Reemplaza el código por el siguiente:

public class AlmacenPuntuacionesFicheroInterno implements AlmacenPuntuaciones {
   private static String FICHERO = "puntuaciones.txt";
   private Context context;

   public AlmacenPuntuacionesFicheroInterno(Context context) {
      this.context = context;
   }

   public void guardarPuntuacion(int puntos, String nombre, long fecha){
      try {
         FileOutputStream f = context.openFileOutput(FICHERO,
                                    Context.MODE_APPEND);
         String texto = puntos + " " + nombre + "\n";
         f.write(texto.getBytes());
         f.close();
      } catch (Exception e) {
         Log.e("Asteroides", e.getMessage(), e);
      }
   }

   public Vector<String> listaPuntuaciones(int cantidad) {
      Vector<String> result = new Vector<String>();
      try {
         FileInputStream f = context.openFileInput(FICHERO);
         BufferedReader entrada = new BufferedReader(
                              new InputStreamReader(f));
         int n = 0;
         String linea;
         do {
            linea = entrada.readLine();
            if (linea != null) {
               result.add(linea);
               n++;
            }
         } while (n < cantidad && linea != null);
         f.close();
      } catch (Exception e) {
         Log.e("Asteroides", e.getMessage(), e);
      }
      return result;
   }
}  

 

4.     Abre el fichero MainActivity.java y en el método onCreate() reemplaza la línea adecuada por:

almacen = new AlmacenPuntuacionesFicheroInterno(this);

 

Práctica: Configurar almacenamiento de puntuaciones desde preferencias.

 Modifica las preferencias de la aplicación Asteroides para que el usuario pueda seleccionar donde se guardarán las puntuaciones. De momento incluye tres opciones: “Array”, “Preferencias” y “Fichero en memoria interna”

1.     Abre el fichero Asteroides.java y en el método onCreate() reemplaza:

almacen = new AlmacenPuntuacionesFicheroInterno(this);

 

por  el código necesario para que se inicialice la variable almacen de forma adecuada según el valor introducido en preferencias.

2.     Verifica el resultado.

3.     Cada vez que añadas un nuevo método de almacenamiento inclúyelo a la lista de preferencias.

Preguntas de repaso: Ficheros

 

 

 

Sistema de almacenamiento externo

Los teléfonos Android suelen disponer de memoria adicional de almacenamiento, conocido como almacenamiento externo. Este almacenamiento suele ser de mayor capacidad, por lo que resulta ideal para almacenar ficheros de música o vídeo. Suele ser una memoria extraíble, como una tarjeta SD, o una memoria interna no extraíble (algunos modelos incorporan los dos tipos de memoria, es decir, almacenamiento externo extraíble y almacenamiento interno no extraíble). Cuando conectamos el dispositivo Android a través del cable USB permitimos el acceso a esta memoria externa, de forma que los ficheros aquí escritos podrán ser leídos, modificados o borrados por cualquier usuario.

Para acceder a la memoria externa lo habitual es utilizar la ruta /sdcard/…

Esta es la  carpeta es donde el sistema monta la tarjeta SD. No obstante resulta más conveniente utilizar el método Environment.getExternalStorageDirectory() para que el sistema nos indique la ruta exacta.

A partir de la versión 1.6 resulta necesario declarar el permiso  WRITE_EXTERNAL_STORAGE en AndroidManifest.xml  para poder escribir en la memoria externa. En la versión 4.1 aparece el permiso READ_EXTERNAL_STORAGE. Sin embargo, este permiso se ha introducido para un futuro uso. En la actualidad todas las aplicaciones pueden leer en la memoria externa. Por lo tanto, has de tener cuidado con la información que dejas en esta memoria.

  video[Tutorial] Almacenamiento externo en Android

Ejercicio paso a paso: Almacenando puntuaciones en la memoria externa.

1.     Abre el proyecto del ejercicio anterior.

2.     Selecciona el fichero AlmacenPuntuacionesFicheroInterno.java, y cópialo en el portapapeles (Ctrl-C).

3.     Pega el fichero sobre el proyecto (Ctrl-V) y renómbralo como AlmacenPuntuacionesFicheroExterno.java.

4.     Abre la nueva clase creada y reemplaza  la inicialización de la variable FICHERO por:

   private static String FICHERO = Environment.        
               getExternalStorageDirectory() + "/puntuaciones.txt";

Dependiendo de si utilizas un emulador o un dispositivo real, el valor de FICHERO será diferente. Posibles valores son: "/sdcard/puntuaciones.txt"o "/storage/sdcard0/puntuaciones.txt".

5.     En el método guardarPuntuacion() reemplaza la inicialización de la variable f por:

FileOutputStream f = new FileOutputStream(FICHERO, true);

6.     En el método listaPuntuacion() reemplaza la inicialización  f por:

FileInputStream f = new FileInputStream(FICHERO);

7.   En el método onCreate() de la actividad Asteroides reemplaza la inicialización de almacen por:

almacen = new AlmacenPuntuacionesFicheroExterno(this);

O si has hecho la práctica Configurar almacenamiento de puntuaciones desde preferencias añade un nuevo tipo en las preferencias.

8.     Abre el fichero AndroidManifest.xml y solicita el permiso WRITE_EXTERNAL_STORAGE

9.     Ejecuta la aplicación y crea nuevas puntuaciones.

10.  Verifica con la vista File Explorer que dentro de la carpeta sdcard aparece el fichero.

 

 

Verificando acceso a la memoria externa

La memoria externa puede haber sido extraída o estar protegida contra escritura.

Puedes utilizar el método Environment.getExternalStorageState() para verificar el estado de la memoria. Veamos como se utiliza:

String stadoSD = Environment.getExternalStorageState();

if (stadoSD.equals(Environment.MEDIA_MOUNTED)) {
    // Podemos leer y escribir
    ...
} else if (stadoSD.equals(Environment.MEDIA_MOUNTED_READ_ONLY)) {
    // Podemos leer
    ...
} else {
    // No podemos leer y ni escribir

    ...
}

Práctica: Verificando acceso a la memoria externa

1.     Modifica la clase AlmacenPuntuacionesFicheroExterno para que antes de acceder a la memoria externa verifique que la operación es posible. En caso contrario mostrará un Toast y saldrá del método.

2.     Ejecuta el programa en un dispositivo real con memoria externa y verifica que se almacena correctamente.

3.     Ahora verifica el comportamiento cuando la memoria externa no está disponible. Para que el dispositivo ya no tenga acceso a esta memoria, la solución más sencilla consiste en conectar el dispositivo con el cable USB y activar el almacenamiento por USB.

Solución: Verificando acceso a la memoria externa

1.     En guardarPuntuacion() añade:

String stadoSD = Environment.getExternalStorageState();
if (!stadoSD.equals(Environment.MEDIA_MOUNTED)) {
   Toast.makeText(context, "No puedo escribir en la memoria externa",
                                              Toast.LENGTH_LONG).show();
   return;
}

2.     En listaPuntuacion() añade:

String stadoSD = Environment.getExternalStorageState();
if (!stadoSD.equals(Environment.MEDIA_MOUNTED) &&
               !stadoSD.equals(Environment.MEDIA_MOUNTED_READ_ONLY)) {
   Toast.makeText(context, "No puedo leer en la memoria externa",
                                          Toast.LENGTH_LONG).show();
   return result;
}

 

 

 

Almacenando ficheros específicos de tu aplicación en el almacenamiento externo

 

A partir de la versión 2.2 (nivel de API 8), las aplicaciones pueden almacenar los ficheros en una carpeta específica del sistema de almacenamiento externo, de forma que cuando la aplicación sea desinstalada se borren automáticamente estos ficheros. En concreto, esta carpeta ha de seguir esta estructura:

/Android/data/<nombre_del_paquete>/files/

Donde el paquete <nombre_del_paquete> ha de cambiarse por el nombre del paquete de la aplicación, por ejemplo org.example.asteroides.

A partir del nivel de API 8 puedes utilizar el método getExternalFilesDir(null) para obtener esta ruta. Si en lugar de null indicas alguna de las constantes que se indican más abajo, se devolverá la ruta a una carpeta específica según el tipo de contenido que nos interese. Este método crea la carpeta en caso de no existir previamente. Indicando la carpeta garantizamos que el escáner de medios de Android categoriza los ficheros de forma adecuada. Por ejemplo, un tono de llamada será identificado como tal y no como un fichero de música. De esta forma, no aparecerá en la lista de música que puede reproducir el reproductor multimedia. Estas carpetas también son eliminadas cuando se desinstala la aplicación.

Constante

Carpeta

Descripción

DIRECTORY_MUSIC

Music

Ficheros de música

DIRECTORY_PODCASTS

Podcasts

Descargas desde podcast

DIRECTORY_RINGTONES

Ringtones

Tono de llamada de teléfono

DIRECTORY_ALARMS

Alarms

Sonidos de alarma

DIRECTORY_NOTIFICATIONS

Notifications

Sonidos para notificaciones

DIRECTORY_PICTURES

Pictures

Ficheros con fotografías

DIRECTORY_DOWNLOADS

Download

Descargas de cualquier tipo

DIRECTORY_DCIM

DCIM

Carpeta que tradicional-mente crean las cámaras

 

NOTA: Si tu aplicación es creada para una versión inferior a la 2.2, no tendrás acceso al método getExternalFilesDir(). No obstante, puedes almacenar a mano los ficheros en una carpeta con la estructura:

   getExternalStorageDirectory() +
             ”/Android/data/
<nombre_del_paquete>/files/”

Cuando se desinstale tu aplicación, esta carpeta será eliminada si se ha instalado en dispositivos con una versión 2.2 o superior.

Práctica: Almacenando puntuaciones en una carpeta de la aplicación de la memoria externa.

1.     Selecciona el fichero AlmacenPuntuacionesFicheroExterno.java, y cópialo en el portapapeles (Ctrl-C)

2.     Pega el fichero sobre el proyecto (Ctrl-V) y renómbralo como AlmacenPuntuacionesFicheroExtApl.java

3.     Modifica los métodos listaPuntuaciones() y guardarPuntuacion() para que las puntuaciones se almacenen en la memoria externa, pero en una carpeta de tu aplicación. Realiza esta tarea con una versión de SDK para Asteroides inferior a la 2.2. Puedes utilizar el siguiente código para crear el directorio:

File ruta = new File(Environment.getExternalStorageDirectory() +
		     "/Android/data/org.example.asteroides/files/");
if(!ruta.exists()) {
  ruta.mkdirs();
}

4.     Modifica el código correspondiente para que la nueva clase pueda ser seleccionada como almacén de las puntuaciones.

5.     Verifica el resultado ejecutando la aplicación en un terminal o emulador con una versión inferior a la 2.2. Desinstala la aplicación y verifica si el fichero ha sido eliminado.

6.    Ejecutando ahora la aplicación en un terminal con una versión igual o superior a la 2.2. Desinstala la aplicación y verifica si el fichero ha sido eliminado.

 

Almacenando ficheros compartidos en el almacenamiento externo

Si quieres crear un fichero que no sea específico para tu aplicación y quieres que no sea borrado cuando tu aplicación sea desinstalada, puedes crearlo en cualquier otro directorio del almacenamiento externo.

Lo ideal es que utilices alguno de los directorios públicos creados para almacenar diferentes tipos de ficheros. Estos directorios parten de la raíz del almacenamiento externo y siguen con alguna de las carpetas listadas en la tabla anterior.

A partir del nivel de API 8 puedes utilizar el método getExternalStoragePublicDirectory(String tipo) para obtener esta ruta de uno de estos directorios compartidos. Como parámetro utiliza alguna de las constantes que se indican en la tabla anterior. Guardando los ficheros en las carpetas adecuadas garantizamos que el escáner de medios de Android categoriza los ficheros de forma adecuada. Si utilizas un nivel de API anterior al 8, lo recomendable es crear estas carpetas manualmente.

NOTA: Si quieres que tus ficheros estén ocultos al escáner de medios, incluye un fichero vacío que se llame .nomedia en la carpeta donde estén almacenados.

Almacenando externo con varias unidades

Algunos dispositivos incluyen varias unidades de almacenamiento externo. En este caso, al conectar el dispositivo con un cable USB a un ordenador aparecerá más de una unidad:

En estos casos, una unidad suele corresponder a una tarjeta extraible SD y la otra una partición en la memoria flash. Si utilizamos el método getExternalFilesDir(), y los relacionados, nos devolverá una de las unidades. Esta unidad se denomina unidad de almacenamiento primaria y el resto de unidades, secundarias. Es el fabricante quien decide cuál de las unidades es la memoria primaria. Normalmente Samsung escoge como memoria externa primaria la partición flash no extraibe.

  Hasta la versión 4.4 el API de Android no soportaba multiples unidades de memoria externa. Solo podíamos acceder de forma estándar a la memoria externa primaria y para acceder a la memoria externa secundaria es necesario conocer dónde el fabricante ha montado esta memoria. En la mayoría de los casos se monta en /mnt/sdcard/external_sd.

A partir de la versión 4.4 se incorporan varios métodos que nos permiten trabajar con varias unidades externas. En la clase Contextse añade File[] getExternalFilesDirs(String), que nos devuelve un array con la ruta a cada uno de los almacenamientos externos disponibles. El primer elemento ha de coincidir con la ruta devuelta por getExternalFilesDir(String).La clase Envirioment incorpora el método estático String getStorageState(File), que permite conocer el estado de cada unidad de almacenamiento. Nos devuelve una información equivalente a la del método  getExternalStorageState().

Preguntas de repaso: La memoria externa

 

Acceder a un fichero de los recursos

 

También tienes la posibilidad de almacenar ficheros en los recursos, es decir, adjuntos al paquete de la aplicación. Has de tener en cuenta que estos ficheros no podrán ser modificados.

Tienes dos alternativas para esto: usar la carpeta res/raw o assets. La principal diferencia a la hora de usar una carpeta u otra está en la forma de identificar el fichero. Por ejemplo, si arrastras un fichero que se llame datos.txt a la carpeta res/raw, podrás acceder a él usando context.getResources. openRawResource(R.raw.datos). Si, por el contrario, dejas este fichero en la carpeta assets, podrás acceder a él usando context.getAssets(). open("datos.txt"). Otra diferencia es que dentro de assets podrás crear subcarpetas para organizar los ficheros.

Recuerda que, tanto en la carpeta raw como en assets, los ficheros nunca son comprimidos.

1.     Con el explorador de ficheros busca en el terminal un fichero de texto que se llamer puntuaciones.txt, creado en alguno de los ejercicios anteriores.

2.     Extráelo del terminal y pégalo en la carpeta res/raw del proyecto Asteroides.

3.     Selecciona el fichero AlmacenPuntuacionesFicheroInterno.java y cópialo en el portapapeles (Ctrl-C).

4.     Pega el fichero sobre el proyecto (Ctrl-V) y renómbralo como AlmacenPuntuacionesRecursoRaw.java.

5.     Elimina de esta clase todo el código del método guardarPuntuacion(). No se realiza ninguna acción en este método.

6.     Para que las puntuaciones se lean del fichero de los recursos, en el método listaPuntuaciones() reemplaza:

FileInputStream f = context.openFileInput(FICHERO);

       por:

InputStream f = context.getResources().openRawResource(
			                      R.raw.puntuaciones);

7.     La siguiente línea ya no tiene sentido. Elimínala:

private static String FICHERO = "puntuaciones.txt";

8.     Modifica el código correspondiente para que la nueva clase pueda ser seleccionada como almacén de las puntuaciones.

9.     Verifica el resultado.

 

Ejercicio paso a paso: Leyendo puntuaciones en un fichero de recursos en assets

1.     Selecciona File / New / Folder / Assets Folder:

En la siguiente ventana deja los valores por defecto:

Hemos creado la carpeta assets que aparecerá dentro de res. Vamos a crear la subcarpeta carpeta dentro de esta carpeta. Pulsa con el botón derecho sobre assets, selecciona New/Directory e introduce “carpeta”.

2.     Copia el fichero puntuaciones.txt dentro de la carpeta que acabas de crear.

3.     Selecciona el fichero AlmacenPuntuacionesRecursoRaw.java y cópialo en el portapapeles (Ctrl-C).

4.     Pega el fichero sobre el proyecto (Ctrl-V) y renómbralo como AlmacenPuntuacionesRecursoAssets.java.

5.     En el método listaPuntuaciones() reemplaza:

InputStream f = context.getResources().openRawResource(
                                              R.raw.puntuaciones);

       por:

InputStream f = context.getAssets().open("carpeta/puntuaciones.txt");

 

6.     Modifica el código correspondiente para que la nueva clase pueda ser seleccionada como almacén de las puntuaciones.

7.    Verifica que el resultado es idéntico al ejercicio anterior.

 

 

Trabajando con XML

Como sabrás XML es uno de los estándares más utilizados en la actualidad para codificar información. Es ampliamente utilizado en Internet, además como hemos mostrado a lo largo de este libro, se utiliza para múltiples usos en el SDK de Android. Entre otras cosas es utilizado para definir Layouts, animaciones,AndroidManifest.xml,etc.

Una de las mayores fortalezas de la plataforma Android es que se aprovecha el lenguaje de programación Java y sus librerías. El SDK de Android no acaba de ofrecer todo lo disponible para su estándar del entorno de ejecución Java (JRE), pero es compatible con una fracción muy significativa de la misma. Lo mismo ocurre en lo referente a trabajar con XML, Java dispone de gran cantidad de API con este propósito pero no todas están disponibles desde Android.

Librerías disponibles:

Java’s Simple API for XML (SAX) (paquetes org.xml.sax.*)

Document Object Model (DOM) (paquetes org.w3c.dom.*)

Librerías no disponibles:

Streaming API for XML (StAX). Aunque se dispone de otra librería con funcionalidad equivalente (paquete org.xmlpull.v1.XmlPullParser).

Java Architecture for XML Binding (JAXB). Resultaría demasiado pesada para Android.

Como podrás ver al estudiar los ejemplos, leer y escribir ficheros XML es muy laborioso y necesitarás algo de esfuerzo para comprender el código empleado. Vamos a explicar las dos alternativas más importantes, SAX y DOM. El planteamiento es bastante diferente. Tras ver los ejemplos podrás decidir qué herramienta se adapta mejor a tus gustos personales o al problema en concreto que tengas que resolver.

El ejemplo utilizado para ilustrar el trabajo con XML será el mismo que el utilizado en el resto del capítulo: almacenar las mejores puntuaciones obtenidas. El formato XML que se utilizará para este propósito se muestra a continuación:

<?xml version="1.0" encoding="UTF-8"?>
<lista_puntuaciones>
    <puntuacion fecha="1288122023410">
          <nombre>Mi nombre</nombre>
          <puntos>45000</puntos>
    </puntuacion>
    <puntuacion fecha="1288122428132">
          <nombre>Otro nombre</nombre>
          <puntos>31000</puntos>
    </puntuacion>
</lista_puntuaciones>

 

Procesando XML con SAX

 

Procesando XML con SAX

El uso de la API SAX (Simple API for XML) se recomienda cuando se desea un programa de análisis rápido y se quiere reducir al mínimo el consumo de memoria de la aplicación. Eso hace que sea muy apropiado para un dispositivo móvil con Android. También resulta ventajoso para procesar ficheros de gran tamaño.

SAX nos facilita realizar un parser (analizador) sobre un documento XML para así poder analizar su contenido. Ha de quedar claro que SAX no almacena los datos. Por lo tanto necesitaremos una estructura de datos donde guardar la información contenida en el XML. Para realizar este parser se van a ir generando una serie de eventos a medida que se vaya leyendo el documento secuencialmente. Por ejemplo, al analizar el documento XML anterior, SAX generará los siguientes eventos:

Comienza elemento: lista_puntuaciones

Comienza elemento: puntuacion, con atributo fecha="1288122023410"

Comienza elemento: nombre

Texto de nodo: Mi nombre

Finaliza elemento: nombre

Comienza elemento: puntos

Texto de nodo: 45000

Finaliza elemento: puntos

Finaliza elemento: puntuacion

Comienza elemento: puntuacion, con atributo fecha="1288122428132"

Comienza elemento: nombre

Texto de nodo: Otro nombre

Finaliza elemento: nombre

Comienza elemento: puntos

Texto de nodo: 31000

Finaliza elemento: puntos

Finaliza elemento: puntuacion

Finaliza elemento: lista_puntuaciones

Para analizar un documento mediante SAX, vamos a escribir métodos asociados a cada tipo de evento. Este proceso se realiza extendiendo la clase DefaultHandler que nos permite reescribir 5 métodos. Los métodos listados a continuación serán llamados a medida que ocurran los eventos listados anteriormente.

startDocument():Comienza el Documento XML.

endDocument()Finaliza documento XML.

startElement(String uri, String nombreLocal, String nombreCualif, Attributes atributos): Comienza una nueva etiqueta; se indican los parámetros

uri: La uri del espacio de nombres o vacío, si no se ha definido.

nombreLocal: Nombre local de la etiqueta sin prefijo.

nombreCualif: Nombre cualificado de la etiqueta con prefijo.

atributos: Lista de atributos de la etiqueta.

endElement(String uri, String nombreLocal, String nombreCualif): Termina una etiqueta.

characters(char ch[], int comienzo, int longitud): Devuelve en ch los caracteres dentro de una etiqueta. Es decir, en <etiqueta> caracteres </etiqueta> devolvería caracteres. Para obtener un String con estos caracteres: String s = new String(ch,comienzo,longitud). Más adelante veremos un ejemplo de cómo utilizar este método.

Ejercicio paso a paso: Almacenando  puntuaciones en XML con SAX

Una vez descritos los principios de trabajo con SAX, pasemos a implementar la interfaz AlmacenPuntuaciones mediante esta API.

1.     Crea la clase AlmacenPuntuacionesXML_SAX en la aplicación Asteroides y escribe el siguiente código:

  public class AlmacenPuntuacionesXML_SAX implements AlmacenPuntuaciones {
   private static String FICHERO = "puntuaciones.xml";
   private Context contexto;
   private ListaPuntuaciones lista;
   private boolean cargadaLista;
   
   public AlmacenPuntuacionesXML_SAX(Context contexto) {
      this.contexto = contexto;
      lista = new ListaPuntuaciones();
      cargadaLista = false;
   }

   @Override
   public void guardarPuntuacion(int puntos, String nombre, long fecha) {
      try {
         if (!cargadaLista){
            lista.leerXML(contexto.openFileInput(FICHERO));
         }
      } catch (FileNotFoundException e) {
      } catch (Exception e) {
         Log.e("Asteroides", e.getMessage(), e);
      }
      lista.nuevo(puntos, nombre, fecha);
      try {
         lista.escribirXML(contexto.openFileOutput(FICHERO,
                                          Context.MODE_PRIVATE));
      } catch (Exception e) {
         Log.e("Asteroides", e.getMessage(), e);
      }
   }

   @Override
   public Vector<String> listaPuntuaciones(int cantidad) {
      try {
         if (!cargadaLista){
            lista.leerXML(contexto.openFileInput(FICHERO));
         }
      } catch (Exception e) {
         Log.e("Asteroides", e.getMessage(), e);
      }
      return lista.aVectorString();
   }

La nueva clase comienza definiendo una serie de variables y constantes. En primer lugar, el nombre del fichero donde se guardarán los datos. Con el valor indicado, el fichero se almacenará en /data/data/org.example.asteroides/ files/puntuaciones.xml. Pero puedes almacenarlos en otro lugar, como por ejemplo en la memoria SD. La variable más importante es lista de la clase ListaPuntuaciones. En ella guardaremos la información contenida en el fichero XML. Esta clase se define a continuación. La variable cargadaLista nos indica si lista ya ha sido leída desde el fichero.

El código continúa sobrescribiendo los dos métodos de la interfaz. En guardarPuntuacion() comenzamos verificando si lista ya ha sido cargada, para hacerlo en caso necesario. Es posible que el programa se esté ejecutando por primera vez, en cuyo caso el fichero no existirá. En este caso se producirá una excepción de tipo FileNotFoundException al tratar de abrir el fichero. Esta excepción es capturada por nuestro código, pero no realizamos ninguna acción dado que no se trata de un verdadero error. A continuación se añade un nuevo elemento a lista y se escribe de nuevo el fichero XML. El siguiente método, listaPuntuacion(), resulta sencillo de entender, al limitarse a métodos definidos en la clase ListaPuntuaciones

2.     Pasemos a mostrar el comienzo de la clase ListaPuntuaciones No es necesario almacenarla en un fichero aparte, puedes definirla dentro de la clase anterior. Para ello copia el siguiente código justo antes del último } de la clase AlmacenPuntuacionesXML_SAX:

 private class ListaPuntuaciones {

   private class Puntuacion {
      int puntos;
      String nombre;
      long fecha;
   }

   private List<Puntuacion> listaPuntuaciones;

   public ListaPuntuaciones() {
      listaPuntuaciones = new ArrayList<Puntuacion>();
   }

   public void nuevo(int puntos, String nombre, long fecha) {
      Puntuacion puntuacion = new Puntuacion();
      puntuacion.puntos = puntos;
      puntuacion.nombre = nombre;
      puntuacion.fecha = fecha;
      listaPuntuaciones.add(puntuacion);
   }

   public Vector<String> aVectorString() {
      Vector<String> result = new Vector<String>();
      for (Puntuacion puntuacion : listaPuntuaciones) {
         result.add(puntuacion.nombre+" "+puntuacion.puntos);
      }
      return result;
   }

 

El objetivo de esta clase es mantener una lista de objetoPuntuacion. Dispone de métodos para insertar un nuevo elemento (nuevo()) y devolver un listado con todas las puntuaciones almacenadas (aVectorString()).

 

3.     Lo verdaderamente interesante de esta clase es que permite la lectura y escritura de los datos desde un documento XML (leerXML() y escribirXML()). Veamos primero como leer un documento XML usando SAX.  Escribe el siguiente código a continuación del anterior:

public void leerXML(InputStream entrada) throws Exception {
   SAXParserFactory fabrica = SAXParserFactory.newInstance();
   SAXParser parser = fabrica.newSAXParser();
   XMLReader lector = parser.getXMLReader();
   ManejadorXML manejadorXML = new ManejadorXML();
   lector.setContentHandler(manejadorXML);
   lector.parse(new InputSource(entrada));
   cargadaLista = true;
}

Para leer un documento XML comenzamos creando una instancia de la clase SAXParserFactory, lo que nos permite crear un nuevo parser XML de tipo SAXParser. Luego creamos un lector, de la clase XMLReader, asociado a este parser. Creamos ManejadorXML de la clase XMLHadler y asociamos este manejador al XMLReader. Para finalizar le indicamos al XMLReader que entrada tiene para que realice el proceso de parser. Una vez finalizado el proceso, marcamos que el fichero está cargado.

Como ves el proceso es algo largo, pero siempre se realiza igual. Donde sí que tendremos que trabajar algo más es en la creación de la clase ManejadorXML,  dado que va a depender del formato del fichero que queramos leer. Esta clase se lista en el siguiente punto.

 

4.     Escribir este código a continuación del anterior:

class ManejadorXML extends DefaultHandler {
   private StringBuilder cadena;
   private Puntuacion puntuacion;

   @Override
   public void startDocument() throws SAXException {
      listaPuntuaciones = new ArrayList<Puntuacion>();
      cadena = new StringBuilder();
   }

   @Override
   public void startElement(String uri, String nombreLocal, String
               nombreCualif, Attributes atr) throws SAXException {
      cadena.setLength(0);
      if (nombreLocal.equals("puntuacion")) {
         puntuacion = new Puntuacion();
         puntuacion.fecha = Long.parseLong(atr.getValue("fecha"));
         }
   }

   @Override
   public void characters(char ch[], int comienzo, int lon) {
      cadena.append(ch, comienzo, lon);
   }

   @Override
   public void endElement(String uri, String nombreLocal,
                  String nombreCualif) throws SAXException {
      if (nombreLocal.equals("puntos")) {
         puntuacion.puntos = Integer.parseInt(cadena.toString());
      } else if (nombreLocal.equals("nombre")) {
         puntuacion.nombre = cadena.toString();
      } else if (nombreLocal.equals("puntuacion")) {
         listaPuntuaciones.add(puntuacion);
      }
   }
   
   @Override
   public void endDocument() throws SAXException {}
}

Esta clase define un manejador que captura los cinco eventos generados en el proceso de parsing en SAX. En startDocument() nos limitamos a inicializar variables. En startElement() verificamos que hemos llegado a una etiqueta <puntuación>. En tal caso, creamos un nuevo objeto de la clase Puntuacion e inicializamos el campo fecha con el valor indicado en uno de los atributos.

El método characters() se llama cuando aparece texto dentro de una etiqueta (<etiqueta> caracteres </etiqueta>). Nos limitamos a almacenar este texto en la variable cadena para utilizarlo en el siguiente método. SAX no nos garantiza que nos pasará todo el texto en un solo evento: si el texto es muy extenso, se realizarán varias llamadas a este método. Por esta razón, el texto se va acumulando en cadena.

El método endElement() resulta más complejo, dado que en función de que etiqueta esté acabando realizaremos una tarea diferente. Si se trata de </puntos> o de </nombre> utilizaremos el valor de la variable cadena para actualizar el valor correspondiente. Si se trata de </puntuacion> añadimos el objeto puntuacion a la lista.

5.      Introduce a continuación el último método de la claseListaPuntuaciones, que nos permite escribir el documento XML:

   public void escribirXML(OutputStream salida) {
      XmlSerializer serializador = Xml.newSerializer();
      try {
         serializador.setOutput(salida, "UTF-8");
         serializador.startDocument("UTF-8", true);
         serializador.startTag("", "lista_puntuaciones");
         for (Puntuacion puntuacion : listaPuntuaciones) {
            serializador.startTag("", "puntuacion");
            serializador.attribute("", "fecha",
                              String.valueOf(puntuacion.fecha));
            serializador.startTag("", "nombre");
            serializador.text(puntuacion.nombre);
            serializador.endTag("", "nombre");
            serializador.startTag("", "puntos");
            serializador.text(String.valueOf(puntuacion.puntos));
            serializador.endTag("", "puntos");
            serializador.endTag("", "puntuacion");
         }
         serializador.endTag("", "lista_puntuaciones");
         serializador.endDocument();
      } catch (Exception e) {
         Log.e("Asteroides", e.getMessage(), e);
      }
   }
} //Cerramos ListaPuntuaciones
} //Cerramos AlmacenPuntuacionesXML_SAX

Como puedes ver, todo el trabajo se realiza por medio de un objeto de la clase XmlSerializer, que escribe el código XML en el OutputStream que hemos pasado como parámetros.

6.     La variable almacen ha de inicializarse de forma adecuada.

7.     Modifica el código correspondiente para que este método pueda ser seleccionado para almacenar las puntuaciones.

8.     Verifica el resultado.

 

Procesando XML con DOM

DOM (Document Object Model) es una API creada por W3C (World Wide Web Consortium) que nos permite manipular dinámicamente documentos XML y HTML. Android soporta el nivel de especificación 3, por lo que permite trabajar con definición de tipo de documento (DTD) y validación de documentos. Para no extender en exceso los ejemplos no vamos a entrar en la definición y validación de documentos.

Como ya hemos comentado, el planteamiento de DOM es muy diferente del de SAX. SAX recorre todo el documento XML secuencialmente y lo analiza, pero sin almacenarlo. Por el contrario, DOM permite cargar el documento XML en memoria RAM y manipularlo directamente en memoria. DOM representa el documento como un árbol. Podremos crear nuevos nodos, borrar o modificar los existentes. Una vez dispongamos de la nueva versión, podremos almacenarlo en un fichero o mandarlo por Internet.

Trabajar con DOM tiene sus ventajas frente a SAX: por ejemplo, nos evitamos definir a mano el proceso de parser (en el ejemplo anterior, la clase ManejadorXML) y crear una estructura para almacenar los datos (en el ejemplo anterior, la clase ListaPuntuaciones). Pero también tiene sus inconvenientes: recorrer un documento DOM puede ser algo complejo; además, al tener que cargarse todo el documento en memoria puede consumir excesivos recursos para un dispositivo como un teléfono móvil. Este inconveniente cobra especial relevancia al trabajar con documentos grandes. Para terminar, DOM procesa la información de forma más lenta.

Veamos cómo se implementa el ejemplo anterior mediante la API DOM.

public class AlmacenPuntuacionesXML_DOM implements AlmacenPuntuaciones{
   private static String FICHERO = "puntuaciones.xml";
   private Context contexto;
   private Document documento;
   private boolean cargadoDocumento;

   public AlmacenPuntuacionesXML_DOM(Context contexto) {
      this.contexto = contexto;
      cargadoDocumento = false;
   }

   @Override
   public void guardarPuntuacion(int puntos, String nombre, long fecha){
      try {
         if (!cargadoDocumento) {
            leerXML(contexto.openFileInput(FICHERO));
         }
      } catch (FileNotFoundException e) {
         crearXML();
      } catch (Exception e) {
         Log.e("Asteroides", e.getMessage(), e);
      }
      nuevo(puntos, nombre, fecha);
      try {
         escribirXML(contexto.openFileOutput(FICHERO, 
                                          Context.MODE_PRIVATE));
      } catch (Exception e) {
         Log.e("Asteroides", e.getMessage(), e);
      }
   }

   @Override public Vector listaPuntuaciones(int cantidad) {
      try {
         if (!cargadoDocumento) {
            leerXML(contexto.openFileInput(FICHERO));
         }
      } catch (FileNotFoundException e) {
         crearXML();
      } catch (Exception e) {
         Log.e("Asteroides", e.getMessage(), e);
      }
      return aVectorString();
   }

Veamos cómo cómo se implementa el ejemplo anterior mediante la API DOM.

Trabajando con JSON

 

JSON corresponde al acrónimo de JavaScript Object Notation. Es un formato para representar información similar a XML que presenta dos ventajas frente a este: Es un más compacto, al necesitar menos bytes para codificar la información y el código necesario para realizar un parser es mucho menor. Estas ventajas hacen que cada vez sea más popular, especialmente en el intercambio de datos a través de la red. A continuación, se muestra cómo se codificaría el ejemplo que estamos desarrollando en este capítulo:

{
  "puntuaciones": [
    { "fecha": 1288122023410, "nombre": "Mi nombre", "puntos": 45000 },

    { "fecha": 1288122428132, "nombre": "Otro nombre", "puntos": 31000 }
  ]
}

Comparando el número de caracteres empleados frente al que se utilizó para codificar esta información en XML, podemos observar una reducción cercana al 50%.

La plataforma Android incorpora la librería estándar org.json con la que podremos procesar ficheros JSON. Otra alternativa es la librería com.google.gson que va a resultar más sencilla de utilizar. Veamos estas dos alternativas.

 

Procesando JSON con la librería Gson

 

GSON es una librería de código abierto creada por Google que permite serializar objetos Java para convertirlos en un String. Su uso más frecuente es para convertir un objeto en su representación JSON y a la inversa.

La gran ventaja de esta librería es que puede ser usada sobre objetos de cualquier tipo de clases, incluso clases preexistentes que no has creado. Esto es posible al no ser necesario introducir código en las clases para que sean serializadas.

El código necesario es muy reducido, como se muestra a continuación:

  private ArrayList<Puntuacion> puntuaciones=new ArrayList<>();   
private Gson gson = new Gson();
private Type type = new TypeToken<List<Puntuacion>>() {}.getType();

// Convertimos la colección de datos a un String JSON
String string = gson.toJson(puntuaciones, type);

// Convertimos un String JSON a una la colección de datos
puntuaciones = gson.fromJson(string, type);

 

En este código puntuaciones contiene una colección (List) de elementos de tipo Puntuacion. Para representar estos datos en JSON vamos a necesitar un objeto Gson y otro Type. Este último representa el tipo de datos con el que trabajamos. En la variable string se almacenará el contenido de puntuaciones en representación JSON. En la siguiente línea se hace el proceso inverso.

La librería no solo permite transformar los datos en JSON, también podemos personalizar la serialización de los datos según las necesidades del programador. También permite excluir algunos atributos para que sean incluidos en la representación JSON.

Ejercicio: Guardar puntuaciones en JSON con la librería Gson.

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

 public class Puntuacion {
    private int puntos;
    private String nombre;
    private long fecha;

    public Puntuacion(int puntos, String nombre, long fecha) {
        this.puntos = puntos;
        this.nombre = nombre;
        this.fecha = fecha;
    }
}

2.      Sitúate al final de la clase y selecciona Code > Generate > Getter and Setter. Selecciona todos los atributos y pulsa OK.

3.      Añade al fichero Gradle Scripts/Bulid.gradle (Module:app) la dependencia:

 dependencies {
    …
    compile 'com.google.code.gson:gson:2.6.2'
}

4.     Crea la clase AlmacenPuntuacionesGSon con el siguiente código:

public class AlmacenPuntuacionesGSon implements AlmacenPuntuaciones {
   private String string; //Almacena puntuaciones en formato JSON
   private Gson gson = new Gson();
   private Type type = new TypeToken<List<Puntuacion>>() {}.getType();

   public AlmacenPuntuacionesGSon() {
      guardarPuntuacion(45000,"Mi nombre", System.currentTimeMillis());
      guardarPuntuacion(31000,"Otro nombre", System.currentTimeMillis());
   }

   @Override
   public void guardarPuntuacion(int puntos, String nombre, long fecha) {
      //string = leerString();
      ArrayList<Puntuacion> puntuaciones;
      if (string == null) {
         puntuaciones = new ArrayList<>();
      } else {
         puntuaciones = gson.fromJson(string, type);
      }
      puntuaciones.add(new Puntuacion(puntos, nombre, fecha));
      string = gson.toJson(puntuaciones, type);
      //guardarString(string);
   }

   @Override
   public Vector<String> listaPuntuaciones(int cantidad) {
      //string = leerString();
      ArrayList<Puntuacion> puntuaciones;
      if (string == null) {
         puntuaciones = new ArrayList<>();
      } else {
         puntuaciones = gson.fromJson(string, type);
      }
      Vector<String> salida = new Vector<>();
      for (Puntuacion puntuacion : puntuaciones) {
         salida.add(puntuacion.getPuntos()+" "+puntuacion.getNombre());
      }
      return salida;
   }
}

En la variable stringse almacenará la lista de puntuaciones en representación JSON. Para que los datos se almacene de forma no volátil tendrías que implementar los método guardarString() y leerString(). La forma más sencilla sería almacenarlo en un fichero de preferencias. Otra alternativa sería guardarlo en un fichero en la memoria interna o externa. En el próximo capítulo veremos cómo mandar este String a través de Internet.

5.     Modifica el código correspondiente para que se pueda seleccionar esta clase para el almacenamiento.

6.     Ejecuta el proyecto y verifica su funcionamiento. Si visualizas el valor de stringeste ha de ser:

[{"fecha":1478552190154,"nombre":"Mi nombre", "puntos":45000},
 {"fecha":1478552205944,"nombre":"Otro nombre","puntos":31000}]

NOTA: Observa como los atributos son almacenados por orden alfabético.

Práctica: Guardar el string JSON en un fichero.

1.     Implementa los métodos método guardarString(String) y leerString() para que la información de almacene en un fichero de preferencias o en la memoria del dispositivo.

2.    En la versión anterior, la variable string hacia el papel de almacen de la información. En la nueva versión, este papel ha pasado a un fichero o a una preferencia. Elimina la variable global string y conviértela en variable local en los métodos donde sea necesario.

Ejercicio: Guardar una clase en JSON con la librería Gson.

El resultado del ejercicio anterior es muy similar al ejemplo JSON mostrado al principio de este apartado. Sin embargo, no es exactamente igual. En el ejemplo se muestra un objeto JSON que incluye una única propiedad con nombre “puntuaciones”. En el siguiente ejercicio veremos como obtener una estructura como esta.

1.    En AlmacenPuntuacionesGSon añade la siguiente clase:

public class Clase {
  private ArrayList<Puntuacion> puntuaciones = new ArrayList<>();
  private boolean guardado;
}

2.    Reemplaza el código subrayado de los siguientes métodos:

private Type type = new TypeToken<Clase>() {}.getType();

@Override
public void guardarPuntuacion(int puntos, String nombre, long fecha) {
   //string = leerString();
   Clase objeto;
   if (string == null) {
      objeto = new Clase();
   } else {
      objeto = gson.fromJson(string, type);
   }
   objeto.puntuaciones.add(new Puntuacion(puntos, nombre, fecha));
   string = gson.toJson(objeto, type);
   //guardarString(string);
}

@Override
public Vector<String> listaPuntuaciones(int cantidad) {
   //string = leerString();
   Clase objeto;
   if (string == null) {
      objeto = new Clase();
   } else {
      objeto = gson.fromJson(string, type);
   }
   Vector<String> salida = new Vector<>();
   for (Puntuacion puntuacion : objeto.puntuaciones) {
      salida.add(puntuacion.getPuntos()+" "+puntuacion.getNombre());
   }
   return salida;    
}

2.  Ejecuta el proyecto y verifica su funcionamiento. Si visualizas el valor de stringeste ha de ser:

{"guardado":false,
 "puntuaciones":[{"fecha":1478552190154,"nombre":"Mi nombre", "puntos":45000},
                 {"fecha":1478552205944,"nombre":"Otro nombre","puntos":31000}
                ]
}

Los saltos de línea han sido introducidos para facilitar la visualización.

Procesando JSON con la librería org.json

 

La librería org.json permite, tanto, codificar datos en formato JSON dentro de un String, como el proceso inverso. Una de sus ventajas es que esta librería ya se encuentra integrada en la plataforma Android.

Para trabajar con esta librería hay que realizar el proceso de conversión manualmente, insertando cada elemento de uno en uno. Esto puede darnos más trabajo que otras librerías como GSON, pero, al no ser un proceso automático, vamos a poder realizarlo de forma personalizada. Por ejemplo, podremos elegir el orden en que se generan los datos.

Ejercicio: Guardar puntuaciones en JSON con la librería org.json.

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

 public class AlmacenPuntuacionesJSon implements AlmacenPuntuaciones {
   private String string; //Almacena puntuaciones en formato JSON

   public AlmacenPuntuacionesJSon() {
      guardarPuntuacion(45000,"Mi nombre", System.currentTimeMillis());
      guardarPuntuacion(31000,"Otro nombre", System.currentTimeMillis());
   }

   @Override
   public void guardarPuntuacion(int puntos, String nombre, long fecha) {
      //string = leerString();
      List<Puntuacion> puntuaciones = leerJSon(string);
      puntuaciones.add(new Puntuacion(puntos, nombre, fecha));
      string = guardarJSon(puntuaciones);
      //guardarString(string);
   }

   @Override
   public Vector<String> listaPuntuaciones(int cantidad) {
      //string = leerFichero();
      List<Puntuacion> puntuaciones = leerJSon(string);
      Vector<String> salida = new Vector<>();
      for (Puntuacion puntuacion: puntuaciones) {
         salida.add(puntuacion.getPuntos()+" "+puntuacion.getNombre());
      }
      return salida;
   }

   private String guardarJSon(List<Puntuacion> puntuaciones) {
      String string = "";
      try {
         JSONArray jsonArray = new JSONArray();
         for (Puntuacion puntuacion : puntuaciones) {
            JSONObject objeto = new JSONObject();
            objeto.put("puntos", puntuacion.getPuntos());
            objeto.put("nombre", puntuacion.getNombre());
            objeto.put("fecha", puntuacion.getFecha());
            jsonArray.put(objeto);
         }
         string = jsonArray.toString();
      } catch (JSONException e) {
          e.printStackTrace();
      }
      return string;
   }

   private List<Puntuacion> leerJSon(String string) {
      List<Puntuacion> puntuaciones = new ArrayList<>();
      try {
         JSONArray json_array = new JSONArray(string);
         for (int i = 0; i < json_array.length(); i++) {
            JSONObject objeto = json_array.getJSONObject(i);
            puntuaciones.add(new Puntuacion(objeto.getInt("puntos"),
                objeto.getString("nombre"), objeto.getLong("fecha")));
         }
      } catch (JSONException e) {
         e.printStackTrace();
      }
      return puntuaciones;
   }
}

2.     Modifica el código necesario para que se pueda seleccionar este tipo de almacenamiento.

3.     Si has realizado la práctica anterior, introduce los métodos guardarString() leerString().

4.     Ejecuta el proyecto y verifica su funcionamiento.

NOTA: Acabamos de ver dos alternativas para serializar los datos contenidos en un objeto, XML y JSON. Existe otra alternativa aportada por en lenguaje Java, que consiste en implementar el interface Serializable [1]. Aunque resulta muy sencillo de utilizar, también presenta algunos inconvenientes: La serialización ocupa mucho espacio[2] (demasiado para transacciones por Internet), el formato obtenido es binario (no es visible o editable por un usuario) y solo se implementa en Java (no podemos interoperar con servidores con otros lenguajes). 

Preguntas de repaso: Trabajando con JSON

Bases de datos

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.

video[Tutorial] Ciclo de vida de una aplicación en Android

Ejercicio: Utilizando una base de datos para guardar puntuaciones.

Pasemos a demostrar cómo guardar las puntuaciones obtenidas en Asteroides en una base de datos. Si comparas la solución propuesta con las anteriores verás como el código necesario es menor. Además, una base de datos te da mucha más potencia; puedes por ejemplo ordenar la salida por puntuación, eliminar filas antiguas, etc. Todo esto sin aumentar apenas el uso de recursos.

1.     Crea la clase AlmacenPuntuacionesSQLite en el proyecto Asteroides y escribe el siguiente código:

public class AlmacenPuntuacionesSQLite extends SQLiteOpenHelper
                                         implements AlmacenPuntuaciones{
   public AlmacenPuntuacionesSQLite(Context context) {
      super(context, "puntuaciones", null, 1);
   }

   //Métodos de SQLiteOpenHelper
   @Override public void onCreate(SQLiteDatabase db) {
      db.execSQL("CREATE TABLE puntuaciones ("+
            "_id INTEGER PRIMARY KEY AUTOINCREMENT, "+
            "puntos INTEGER, nombre TEXT, fecha BIGINT)");
   }

   @Override    public void onUpgrade(SQLiteDatabase db,
                                 int oldVersion, int newVersion) {
   // En caso de una nueva versión habría que actualizar las tablas
   }

   //Métodos de AlmacenPuntuaciones
   public void guardarPuntuacion(int puntos, String nombre,
                                                   long fecha) {
      SQLiteDatabase db = getWritableDatabase();
      db.execSQL("INSERT INTO puntuaciones VALUES ( null, "+
            puntos+", '"+nombre+"', "+fecha+")");
      db.close();
   }
   
   public Vector<String> listaPuntuaciones(int cantidad) {
      Vector<String> result = new Vector<String>();
      SQLiteDatabase db = getReadableDatabase();
      Cursor cursor = db.rawQuery("SELECT puntos, nombre FROM " +
       "puntuaciones ORDER BY puntos DESC LIMIT " +cantidad, null);
      while (cursor.moveToNext()){
            result.add(cursor.getInt(0)+" " +cursor.getString(1));
      }
      cursor.close();
      db.close();
      return result;
   }   
}

El constructor de la clase se limita a llamar al constructor heredado con el perfil:

SQLiteOpenHelper(Context contexto, String nombre, SQLiteDatabase.CursorFactory cursor, int version).

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. En nuestra aplicación necesitamos solo la tabla puntuaciones que se crea por medio del comando SQL CREATE TABLE puntuaciones… El primer campo tiene por nombre _idy 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.

 

El método onUpgrade() está vacío. Si más adelante decidimos crear una nueva estructura para la base de datos, tendremos que indicar un número de versión superior, por ejemplo la 2. Cuando se ejecute el código sobre un sistema donde se disponga de una base de datos con la versión 1,  se incocará 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.

Pasemos a describir los dos métodos de la interfaz  AlmacenaPuntuaciones. El método guardarPuntuacion() comienza obteniendo una referencia a nuestra base de datos utilizando getWritableDatabase(),  mediante la cual ejecuta el comando SQL para almacenar un nueva fila en la tabla INSERT INTO puntuaciones … El método listaPuntuaciones() comienza obteniendo una referencia a nuestra base de datos utilizando getReadableDatabase(). Realiza una consulta utilizando el método rawQuery(), con la que obtiene un cursor que utiliza para leer todas las filas devueltas en la consulta.

2.     Modifica el código correspondiente para que este método pueda ser seleccionado para almacenar las puntuaciones.

3.     Verifica su funcionamiento.

Ejercicio: Verificación de los ficheros creados

1.     Abre la vista File Explorer. Pulsa el botón Android  Device Monitor () de la barra de herramientas y selecciona la lengüeta  File Explorer

2.     Busca la ruta data/data/org.example.asteroides

3.     Observa los ficheros creados y compara el tamaño de cada uno.

 

 

Los métodos query() y rawQuery()

En el ejemplo anterior hemos utilizado el método rawQuery() para hacer una consulta. Este método tiene una versión alternativa con la misma función el método query(). El método query() es el usado por defecto en la documentación oficial y además es el único disponible en otras clases (por ejemplo para hacer una consulta en un ContentProvider). Sin embargo, tiene un inconveniente respecto al método rawQuery(), has de rellenar gran cantidad de parámetros para controlar la búsqueda, lo que lo hace confuso de utilizar. Si estás acostumbrado a trabajar con SQL es posible que este método te resulte incómodo. A continuación se describen los parámetros de ambos métodos:

Cursor SQLiteDatabase.query (
      Stringtable,         
//tabla a consultar (FROM)
      String[] columns,      //columnas a devolver (SELECT)
      Stringselection,      //consulta (WHERE)
      String[] selectionArgs,//reemplaza “?” de la consulta
      StringgroupBy,        //agrupado por (GROUPBY)
      Stringhaving,         //condición para agrupación
      StringorderBy,        //ordenado por
      Stringlimit)          //cantidad máx. de registros

Cursor SQLiteDatabase.rawQuery(
      Stringsql,            
//comando SQL
      String[] selectionArgs)//reemplaza “?”de la consulta

Veamos un ejemplo de cómo se podrían utilizar estos métodos. Supongamos que hemos creado la tabla, tabla, y que tiene las columnas texto, entero y numero. Si quisiéramos seleccionar las columnas texto y entero de las filas con el valor de numero mayor que 2, ordenados según el valor de entero y que además el número de filas seleccionadas estuviera limitado a un máximo de cantidad. (donde cantidad ha de ser una variable de tipo entero previamente definida), escribiríamos:

Cursor cursor = db.rawQuery("SELECT texto, entero FROM tabla"+
   " WHERE numero>2 ORDER BY entero LIMIT "+ cantidad,null);

Cuando uno está acostumbrado al lenguaje SQL esta puede ser la forma más sencilla de hacer la consulta. De forma alternativa podemos hacer uso del segundo parámetro. Este ha de ser un array de String, de forma que estos Strings reemplazan cada una de las apariciones del carácter “?” en la cadena del primer parámetro. Veamos un ejemplo que sería equivalente al anterior:

String[] param = new String[1];

   param[0]= Integer.toString(cantidad,10);

   Cursor cursor = db.rawQuery("SELECT texto, entero FROM tabla" +
                 " WHERE numero>2 ORDER BY entero LIMIT ?", param);

Si en lugar de método rawQuery() queremos utilizar el método query() usaríamos el siguiente código equivalente a los dos anteriores:

String[] CAMPOS = {"texto", "entero"};

Cursor cursor = db.query("tabla", CAMPOS, "numero>2, null,
          null, null, "entero", Integer.toString(cantidad));

Ejercicio paso a paso: Utilización del método query() para guardar puntuaciones

1.      Remplaza la llamada al método rawQuery()  del ejercicio anterior por el siguiente código:

String[] CAMPOS = {"puntos", "nombre"};
Cursor cursor=db.query("puntuaciones", CAMPOS, null, null,
       null, null, "puntos DESC", Integer.toString(cantidad));

2.     Verifica que el funcionamiento es idéntico.

Preguntas de repaso:  SQLite I                                                      

 

Uso de bases de datos en Mis Lugares

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:

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. En el explorador de paquetes pulsa con el botón derecho del ratón sobre el proyecto Mis Lugares. Selecciona la opción Copy. Vuelve a pulsar con el botón derecho y selecciona Paste. Introduce un nuevo nombre de 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) {
   }
}

El constructor de la clase se limita a llamar al constructor heredado con el perfil:

SQLiteOpenHelper(Context contexto, String nombre,

       SQLiteDatabase.CursorFactory cursor, int version).

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()) si esta 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 Lugares {

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 metods”, selecciona todos los métodos y pulsa OK. Observa cómo 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 trabajemos 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 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();
    }
}
 

Operaciones con bases de datos en Mis Lugares

 

 


En los apartados anteriores hemos aprendido a crear una base de datos y a realizar consultas en una tabla. En este apartado vamos a continuar aprendiendo las operaciones básicas cuando trabajamos con datos. Estas son: altas, bajas y modificaciones y consultas.

Ejercicio: Consulta de un elemento en Mis Lugares

1.     Reemplaza en la clase lugaresnBD en el método Melemento()con el siguiente código. Su finalidad es buscar el lugar correspondiente a un id y devolverlo.

@Override
public Lugar elemento(int id) {
   Lugar lugar = null;
   SQLiteDatabase bd = getReadableDatabase();
   Cursor cursor = bd.rawQuery("SELECT * FROM lugares WHERE _id = " + id, 
            null);
   if (cursor.moveToNext()) {
      lugar = extraeLugar(cursor);
   }
   cursor.close();
   bd.close();
   return lugar;
}

Comenzamos inicializando el valor del lugar a devolver a null, para que corresponda con el valor devuelto en caso de no encontrarse el id. Luego se obtiene el objeto bd llamando a getReadableDatabase(). Este objeto nos permitirá hacer consultas en la base de datos. Por medio del método rawQuery() se realiza una consulta en la tabla lugares usando el comando SQL SELECT * FROM lugares WHERE _id =…. Este comando podría interpretarse como “selecciona todas las columnas de la tabla lugares, para la fila con el id indicado”. El resultado de una consulta es un Cursor con la fila, si es encontrado, o en caso contrario, un Cursor vacío.

En la siguiente línea llamamos a cursor.moveToNext() para que el cursor pase a la siguiente fila encontrada. Como es la primera llamada estamos hablando del primer elemento. Devuelve  true  si lo encuentra y false si no. En caso de encontrarlo, llamamos a extraeLugar() para actualizar todos los atributos de lugar con los valores de la fila apuntada por el cursor. Es importante cerrar lo antes posibles los cursores y bases de datos por tener mucho consumo de memoria. El método termina devolviendo el lugar.

2.     Este método no es usado por la aplicación. Ha sido añadido por pertenecer a la interfaz Lugares.

Ejercicio: Modificación de un lugar

Si tratas de modificar cualquiera de los lugares, observarás que los cambios no tienen efecto. Para que la base de datos sea actualizada, realiza el siguiente ejercicio:

1.     Añade en la clase LugaresBD, en el método actualizaLugar(), el siguiente código. Su finalidad es reemplazar el lugar correspondiente al id indicado por un nuevo lugar.

@Override
public void actualiza(int id, Lugar lugar) {
    SQLiteDatabase bd = getWritableDatabase();
    bd.execSQL("UPDATE lugares SET nombre = '" + lugar.getNombre() +
            "', direccion = '" + lugar.getDireccion() +
            "', longitud = " + lugar.getPosicion().getLongitud() +
            " , latitud = " + lugar.getPosicion().getLatitud() +
            " , tipo = " + lugar.getTipo().ordinal() +
            " , foto = '" + lugar.getFoto() +
            "', telefono = " + lugar.getTelefono() +
            " , url = '" + lugar.getUrl() +
            "', comentario = '" + lugar.getComentario() +
            "', fecha = " + lugar.getFecha() +
            " , valoracion = " + lugar.getValoracion() +
            " WHERE _id = " + id);
    bd.close();
}

2.   En la clase EdicionLugarActivity, el método onOptionsItemSelected() reemplaza:

MainActivity.lugares.actualiza((int) id,lugar);

por:

int _id = MainActivity.adaptador.idPosicion((int) id);
MainActivity.lugares.actualiza(_id,lugar);

La variable id corresponde a un indicador de posición dentro de la lista. Para utilizar correctamente el método actualiza() de LugaresBD, hemos de obtener el id correspondiente a la primera columna de la tabla. Este cambio lo realiza el método idPosicion() de adaptador.

3.     Ejecuta la aplicación y trata de modificar algún lugar. Observa que, cuando realizas un cambio en un lugar, estos parecen que no se almacenan. Realmente sí que se han almacenado, el problema está en que la variable cursor no se ha actualizado. Para verificar que los cambios sí que se han almacenado, has de salir de la aplicación y volver a lanzarla.

4.     Para resolver este problema has de añadir la siguiente línea tras las que acabas de añadir:

MainActivity.adaptador.setCursor(MainActivity.lugares.extraeCursor());

5.    Ejecuta de nuevo la aplicación. Tras editar un lugar, los cambios se reflejan en VistaLugarActivity pero no en el RecyclerView de MainActivity. Has de salir de la aplicación y volver a lanzarla para que se actualice la lista.

6.     Para resolver este nuevo problema has de añadir la siguiente línea tras la que acabas de añadir:

MainActivity.adaptador.notifyItemChanged((int) id);

Estamos notificando al adaptador que ha sido cambiado el elemento de una determinada posición. Esto provocará una llamada al método onBindViewHolder() donde se refrescará la vista.

7.     Ejecuta de nuevo la aplicación. Edita un lugar y verifica que los cambios se reflejan en el RecyclerView.

8.     Algunos de los campos de un lugar no se modifican en la actividad EdicionLugarActivity, si no que se hacen directamente en VistaLugarActivity. En concreto la valoración, la fotografía y más adelante añadiremos fecha y hora. Cuando se modifiquen estos campos, también habrá que almacenarlos de forma permanente en la base de datos. Empezaremos por la valoración. Añade en el método onCreate() de VistaLugarActivity el código subrayado.

@Override
public void onRatingChanged(RatingBar ratingBar,
                                   float valor, boolean fromUser) {
    lugar.setValoracion(valor);
    actualizaLugar();
}

Cuando el usuario cambie la valoración de un lugar se llamará a onRatingChanged(). Tras cambiar el valor del objeto lugar, llamamos a actualizaLugar() para almacenar en la base de datos este objeto.

9.     Añade el siguiente método:

void actualizaLugar(){
    int _id = MainActivity.adaptador.idPosicion((int) id);
    MainActivity.lugares.actualiza(_id, lugar);
    MainActivity.adaptador.setCursor(MainActivity.lugares.extraeCursor());
    MainActivity.adaptador.notifyItemChanged((int) id);
}

Primero obtenemos en la variable _id el identificador del lugar. Para ello, vamos a usar la posición que el lugar ocupa en el listado almacenado en la variable id. Una vez obtenido _id, ya podemos actualizar la base de datos. Como hemos visto, siempre que cambie algún contenido es importante que el adaptador actualice el Cursor. Además, si queremos que la vista correspondiente sea creada de nuevo hemos de llamar a notifyItemChanged().  

10.     Para que los cambios en las fotografías se actualicen también has de hacer llamar a actualizaLugar(). En concreto en onActivityResult(), tras las dos apariciones de ponerFoto() y al final de eliminarFoto().

11.     Verifica que tanto los cambios de valoración como de fotografía se almacenan correctamente.

Ejercicio: Alta de un lugar

En este ejercicio aprenderemos a añadir nuevos registros a la base de datos.

1.     Reemplaza en la clase LugaresBD el método nuevo() por el siguiente. Su finalidad es crear un nuevo lugar en blanco y devolver el id del nuevo lugar.

@Override
public int nuevo() {
    int _id = -1;
    Lugar lugar = new Lugar();
    SQLiteDatabase bd = getWritableDatabase();
    bd.execSQL("INSERT INTO lugares (longitud, latitud, tipo, fecha) "+
            "VALUES ( " + lugar.getPosicion().getLongitud()+","+
            lugar.getPosicion().getLatitud()+", "+
            lugar.getTipo().ordinal()+", "+lugar.getFecha()+")");
    Cursor c = bd.rawQuery("SELECT _id FROM lugares WHERE fecha = " +
            lugar.getFecha(), null);
    if (c.moveToNext()){
        _id = c.getInt(0);
    }
    c.close();
    bd.close();
    return _id;
}

Comenzamos inicializando el valor del _id a devolver a -1. De esta manera, si hay algún problema este será el valor devuelto. Luego se crea un nuevo objeto Lugar. Si consultas el constructor de la clase, observarás que solo se inicializan posicion, tipo y fecha. El resto de los valores serán una cadena vacía para String y 0 para valores numéricos. Acto seguido, se crea una nueva fila con esta información. Los valores de texto y numéricos tampoco se indican, al inicializarse de la misma manera.

El método ha de devolver el _id del elemento añadido. Para conseguirlo se realiza una consulta buscando una fila con la misma fecha que acabamos de introducir.

2.     Para la acción de añadir vamos a utilizar el botón flotante que tenemos desde la primera versión de la aplicación. Abre el fichero res/layout/activity_main.xml y reemplaza el icono aplicado a este botón:

<android.support.design.widget.FloatingActionButton
    … 
    android:src="@android:drawable/ic_input_add"
    … />

3.     Abre la clase MainActivity y dentro de onCreate() comenta el código tachado y añade el subrayado, para que se ejecute al pulsar el botón flotante:

fab.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View view) {
      Snackbar.make(view,"Replace with your own action",Snackbar.LENGTH_LONG)
              .setAction("Action", null).show();
      long _id = lugares.nuevo();
      Intent i = new Intent(MainActivity.this,EdicionLugarActivity.class);
      i.putExtra("_id", _id);
      startActivity(i);
   }
});

Comenzamos creando un nuevo lugar en la base e datos cuyo identificaor va a ser _id. A continuación vamos a lanzar la actividad EdicionLugarActivity para que el usuario rellene los datos del lugar. Hasta ahora hemos utilizado el extra “id” para indicar la posición en la lista del objeto a editar. Pero ahora esto no es posible, dado que este nuevo lugar no ha sido añadido a la lista. Para resolver el problema vamos a crear un nuevo extra, “_id”, que usaremos para identificar el lugar a editar por medio de su campo _id.  

4.     En la clase EdicionLugarActivity añade el código subrayado:

private long _id;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.edicion_lugar);
    Bundle extras = getIntent().getExtras();
    id = extras.getLong("id", -1);
    _id = extras.getLong("_id", -1);
    if (_id!=-1) {
        lugar = MainActivity.lugares.elemento((int) _id);
    } else {
        lugar = MainActivity.adaptador.lugarPosicion((int) id);
    }
    …

Esta actividad va a poder ser llamada de dos formas alternativas: usando el extra “id” para indicar que el lugar a modificar ha de extraerse de una posición del adaptador; o usando “_id” en este caso el lugar será extraido de la base de datos usando su identificador. Observa como se han definido dos variables globales, id e _id. Aunque solo una se va a inicializar y la otra valdrá -1. 

5.     Cuando el usuario pulse la opción guardar se llamará al método onOptionsItemSelected(). Para almacenar la información tendremos que verificar cual de las dos variables ha sido inicialzada. Añade el código subrallado en este método:

case R.id.accion_guardar:
    …
    if (_id==-1) {
       int _id = MainActivity.adaptador.idPosicion((int) id);
    }
    MainActivity.lugares.actualiza((int) _id, lugar);
    MainActivity.adaptador.setCursor(MainActivity.lugares.extraeCursor());
    if (id!=-1) {
        MainActivity.adaptador.notifyItemChanged((int) id);
    } else {
        MainActivity.adaptador.notifyDataSetChanged();
    }
    finish();
    return true;

El primer if es añadido dado que si nos han pasado el identificador _id ya no tiene sentido obtenerlo a partir de la posición. El segundo if permite hacer la siguiente distinción: si nos han pasado una posición nodemos notificar al adaptador que obtenga la nueva vista de una determinada posición. En caso contrario, no conocemos la posición, por lo que tendremos que notificar al adaptador que todos los datos han cambiado.

6.     Verifica que los cambios introducidos funcionan correctamente.

Ejercicio: Baja de un lugar