Vous êtes sur la page 1sur 27

CURSO DE DESARROLLO DE APLICACIONES ANDROID

Tema 13

Almacenamiento de datos
TEMA 13. ALMACENAMIENTO DE DATOS

Introducción

En Android existen diversas opciones para almacenar los datos de aplicaciones. La elección de
una u otra opción dependerá de las necesidades de la aplicación como, por ejemplo, la
cantidad de espacio que se necesite, así como el nivel de privacidad de los datos:

• Shared Preferences. Almacenará los datos de forma privada con el formato clave-
valor.
• Almacenamiento interno. Almacenará los datos en la memoria interna del dispositivo.
• Almacenamiento externo. Almacenará los datos en la memoria externa y compartida
del dispositivo.
• Bases de datos SQLite. Almacenará los datos de forma estructurada en una base de
datos privada.
• Conexión de red. Almacenará los datos en un servidor web.

De forma adicional, Android da la posibilidad de exponer incluso los datos privados de una
aplicación a otras aplicaciones a través de los proveedores de contenido, componentes que
proporcionan acceso de lectura y escritura a los datos de una aplicación, en función de las
restricciones que se deseen imponer.

Shared Preferences

A través de la clase SharedPreferences se pueden almacenar y obtener datos primitivos 1 en


la forma clave-valor, los cuales persistirán pese a que la aplicación sea destruida.

Se pueden gestionar diferentes archivos 2 de preferencias en la misma aplicación, o solo uno.


Para obtener un objeto SharedPreferences que gestione múltiples archivos de preferencias,
se utilizará el método getSharedPreferences(String nombre, int modo). El primer
parámetro, name, determinará el nombre del archivo de preferencias, mientras que el segundo
parámetro determinará el modo de operación sobre el archivo que se establecerá para otras
aplicaciones (privado, lectura o escritura, fundamentalmente).

1
Los datos primitivos en Java son boolean, float, int, long, y string (estos tipos de datos, por ser primitivos,
comienzan por minúscula, para diferenciarlos de las clases Java “análogas”).
2
Las SharedPreferences se almacenan en un archivo xml, cuya ubicación es:
/data/data/nombre.paquete.aplicacion/shared_prefs/nombre.paquete.aplicacion_preferences.xml.
Este archivo será borrado al desinstalar la aplicación.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 2


TEMA 13. ALMACENAMIENTO DE DATOS

Si solo se desea gestiona un único archivo de preferencias, se podrá utilizar el método


getPreferences(int modo) que invocará al método getSharedPreferences() pasando
como primer parámetro, el nombre de la actividad.

El sistema devolverá la misma instancia del objeto SharedPreferences del mismo nombre,
por lo que los cambios que un componente realice serán inmediatamente visibles para el resto
de componentes que hayan invocado al mismo objeto SharedPreferences.

Para obtener valores de un objeto SharedPreferences previamente grabado, bastará con


invocar a los métodos “get” como, por ejemplo, getBoolean(String nombre, boolean
valorPorDefecto), getInt(String nombre, int valorPorDefecto), getString(String
nombre, String valorPorDefecto). El parámetro valorPorDefecto será el valor que
adopte la preferencia en caso de no existir al ser invocada.

Para escribir valores en el objeto SharedPreferences se invocará a su método edit() para


obtener un editor SharedPreferences.Editor a través del cual se podrán invocar a los
métodos “put” como, por ejemplo, putBoolean(String nombre), putInt(String nombre),
putString(String nombre). Para finalizar la edición del objeto, se deberá invocar al método
commit().

Almacén interno del dispositivo

Se pueden guardar archivos en el espacio de almacenamiento interno del dispositivo. Estos


archivos serán privados para la aplicación y serán borrados cuando la aplicación sea
desinstalada. Los archivos serán grabados en la carpeta
3
/data/data/nombre.paquete.aplicacion/ .

Se puede crear un archivo siguiendo tres sencillos pasos:

1. Obtención de un objeto FileOutputStream con el cual se podrá escribir el archivo,


invocando al método de contexto openFileOutput(String nombre, int modo) y
pasando como parámetros el nombre del archivo así como el modo de operación. Los
diferentes modos son:
• MODE_PRIVATE: creará o sustituirá el archivo que será privado, únicamente
accesible por la aplicación.
• MODE_APPEND: si el archivo ya existe, escribirá los nuevos datos al final del
mismo, sin borrar el contenido previo.
• MODE_WORLD_READABLE: permitirá que otras aplicaciones puedan acceder al
archivo en modo sólo lectura.

3
Esta ruta se crea, lógicamente, en la memoria interna del dispositivo.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 3


TEMA 13. ALMACENAMIENTO DE DATOS

• MODE_WORLD_WRITEABLE: permitirá que otras aplicaciones puedan acceder al


archivo en modo escritura.
2. Escritura del archivo usando FileOutputStream.write(byte[] buffer), y
convirtiendo, por ejemplo, un String a escribir en bytes (string.getBytes()).
3. Cierre del objeto FileOutputStream, a través de su método close().

Por ejemplo:

try {
// Se escribe el archivo
FileOutputStream fos = openFileOutput(NOMBRE_ARCHIVO,
Context.MODE_PRIVATE);
fos.write(textoArchivo.getBytes());
fos.close();
} catch (FileNotFoundException e) {
// Avisar al usuario con Toast
e.printStackTrace();
} catch (IOException e) {
// Avisar al usuario con Toast
e.printStackTrace();
}

De forma análoga, se podrá leer un archivo previamente grabado en la memoria interna del
dispositivo con estos tres pasos:

1. Obtención de un objeto FileInputStream con el cual se podrá leer el archivo,


invocando al método de contexto openFileInput(String nombre) y pasando como
parámetro el nombre del archivo.
2. Lectura de los bytes del archivo a través de su método read(), u otro método
equivalente.
3. Cierre del objeto FileInputStream, a través de su método close().

Por ejemplo:

try {
String textoArchivoInterno = "";
String linea = "";
FileInputStream fis = openFileInput(NOMBRE_ARCHIVO);

// Esta es una forma usual en Java de leer un archivo línea por


// línea hasta el final.
// Si se usara directamente fis.read(), habría que limitar el
// número de bytes que se extraen del archivo.
InputStreamReader in = new InputStreamReader(fis);
BufferedReader br = new BufferedReader(in);
while ((linea = br.readLine()) != null) {

CURSO DE DESARROLLO DE APLICACIONES ANDROID 4


TEMA 13. ALMACENAMIENTO DE DATOS

textoArchivoInterno += linea + "\n";


}

((TextView) findViewById(R.id.txtPrueba))
.setText(textoArchivoInterno);

} catch (FileNotFoundException e) {
Avisar al usuario con Toast
e.printStackTrace();
} catch (IOException e) {
Avisar al usuario con Toast
e.printStackTrace();
}

Existen cuatro métodos útiles para gestionar archivos:

• getFilesDir(): devuelve la ruta absoluta donde serán guardados los archivos.


• getDir(): crea o abre un directorio dentro del espacio de almacenamiento interno.
• deleteFile(): borra un archivo del almacenamiento interno.
• fileList(): devuelve la lista de archivos guardados por la aplicación.

Debido a que todos los archivos guardados en el espacio de almacenamiento interno del
dispositivo serán borrados cuando la aplicación sea desinstalada, si se quiere conservar algún
tipo de archivo, se deberá guardar en el espacio da almacenamiento externo, en los directorios
públicos (como se verá siguiente sección).

Existe también la posibilidad de cachear cierta información, en vez de grabarla de forma


permanente, invocando al método getCacheDir() que abrirá un objeto File que
representará el directorio interno donde poder guardar los archivos temporales. Cuando el
sistema necesite espacio de almacenamiento interno, podrá borrar los archivos guardados en
este tipo de directorios para recuperar espacio. No obstante, y como buena práctica
programática, será la propia aplicación la que deberá ser responsable de limitar el espacio que
ocupen dichos archivos temporales propios.

Por último, también es posible guardar archivos estáticos en tiempo de compilación, para que
sean usados por la aplicación. Estos archivos deberán ser guardados en la carpeta /res/raw/ y
podrán ser obtenidos invocando al método de contexto openRawResource(int id), pasando
como parámetro el identificador del archivo generado en la clase R: R.raw.nombre_archivo.
Dicho método devolverá un InputStream que podrá ser usado, únicamente, para leer el
archivo.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 5


TEMA 13. ALMACENAMIENTO DE DATOS

Almacén externo del dispositivo

Además del almacén interno del dispositivo, cualquier aplicación podrá guardar archivos en el
almacén externo del dispositivo, que podrá ser tanto una tarjeta SD como espacio de
almacenamiento no extraíble dentro del dispositivo.

Esta característica deberá ser añadida como permiso en el manifiesto de la aplicación,


añadiendo lo siguiente:

<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Los archivos que sean guardados en este almacén serán accesibles en modo lectura por
cualquier aplicación instalada en el dispositivo así como por el propio usuario, cuando active la
transferencia de archivos al ordenador vía USB. Debido a esto, no es recomendable guardar en
este almacén información privada y esencial para la aplicación, puesto que podrá ser borrada o
modificada por otras aplicaciones o por el usuario.

Antes de acceder al almacén externo, se deberá comprobar su disponibilidad, invocando al


método estático de la clase Environment, getExternalStorageState(). Este método
devolverá un String que dará información sobre el estado del almacén externo (que podrá
estar montado en un ordenador, no encontrarse, ser de solo lectura, etc.). Esta información
será útil a la hora de gestionar los archivos, así como para avisar al usuario sobre la necesidad
de acceso al almacén externo en caso de que se encuentre inaccesible, proporcionándole
información concreta sobre el problema.

Por ejemplo:

String estadoAlmacenExterno = Environment.getExternalStorageState();

if (Environment.MEDIA_MOUNTED.equals(estadoAlmacenExterno)) {
// Se puede leer y escribir
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 6


TEMA 13. ALMACENAMIENTO DE DATOS

Para guardar archivos en el almacén externo del dispositivo, se realizarán los siguientes pasos:

1. Se comprobará la disponibilidad del almacenamiento externo.


2. Se invocará al método getExternalFilesDir(String type) 4 para obtener un
objeto File que representará la ruta del directorio donde se almacenará el archivo. Si
se pasa como parámetro null, se obtendrá la raíz del directorio de la aplicación:
Android/data/nombre.paquete.aplicacion/. Los tipos que se podrán pasar como
parámetro son (los directorios obtenidos se referencian desde la raíz absoluta del
almacén externo):
• Environment.DIRECTORY_ALARMS: se obtendrá el directorio Alarms/
• Environment.DIRECTORY_DOWNLOAD: se obtendrá el directorio Download/
• Environment.DIRECTORY_MOVIES: se obtendrá el directorio Movies/
• Environment.DIRECTORY_MUSIC: se obtendrá el directorio Music/
• Environment.DIRECTORY_NOTIFICATIONS: se obtendrá el directorio
Notifications/
• Environment.DIRECTORY_PICTURES: se obtendrá el directorio Pictures/
• Environment.DIRECTORY_PODCASTS: se obtendrá el directorio Podcasts/
• Environment.DIRECTORY_RINGTONES: se obtendrá el directorio Ringtones/
3. Una vez obtenido el objeto File, se instanciará un objeto FileOutputStream,
construyéndolo con el objeto File previamente obtenido y con el nombre del archivo,
para invocar a su método write(byte[] buffer), convirtiendo, por ejemplo, un
String a escribir en bytes ( string.getBytes()).
4. Se cerrará el objeto FileOutputStream a través de su método close().

Por ejemplo:

if (mAlmacenamientoExternoEscritura) {

try {
// Se crea la ruta para almacenar el archivo en la raíz
// del almacenamiento externo (desde donde la aplicación
// tiene permisos)
File file = new File(getExternalFilesDir(null), NOMBRE_ARCHIVO);
// Se escribe el archivo
OutputStream os = new FileOutputStream(file);
os.write(textoArchivo.getBytes());
os.close();
} catch (FileNotFoundException e) {
// Avisar al usuario con Toast
e.printStackTrace();
} catch (IOException e) {

4
Si se usa el nivel de API 7, se deberá invocar a getExternalStrorageDirectoy() que devolverá un objeto File
representará la raíz del almacén externo. Se deberán guardar los archivos bajo la ruta
Android/data/nombre.paquete.aplicacion/files/

CURSO DE DESARROLLO DE APLICACIONES ANDROID 7


TEMA 13. ALMACENAMIENTO DE DATOS

// Avisar al usuario con Toast


e.printStackTrace();
}
}

De forma análoga, se podrá leer un archivo previamente grabado en la memoria externa del
dispositivo con estos pasos:

1. Se invocará a getExternalFilesDir(String type) para obtener un objeto File


que representará la ruta del directorio donde se almacenará el archivo.
2. Se obtendrá un objeto FileInputStream con el cual se podrá leer el archivo,
construyéndolo con el objeto File previamente obtenido y con el nombre del archivo.
3. Se leerán los bytes del archivo a través de su método read() u otro método
equivalente.
4. Se cerrará el objeto FileInputStream a través de su método close().

Por ejemplo:

try {
String textoArchivoExterno = "";
String linea = "";
// Se obtiene el archivo
File file = new File(getExternalFilesDir(null), NOMBRE_ARCHIVO);
FileInputStream fis = new FileInputStream(file);

// Esta es una forma usual en Java de leer un archivo línea por


// línea al final.
// Si se usara directamente fis.read(), habría que limitar el
// número de bytes que se extraen del archivo.
InputStreamReader in = new InputStreamReader(fis);
BufferedReader br = new BufferedReader(in);
while ((linea = br.readLine()) != null) {
textoArchivoInterno += linea + "\n";
}

((TextView) findViewById(R.id.txtPrueba))
.setText(textoArchivoInterno);

} catch (FileNotFoundException e) {
Avisar al usuario con Toast
e.printStackTrace();
} catch (IOException e) {
Avisar al usuario con Toast
e.printStackTrace();
}

Si, cuando se invoque a getExternalFilesDir(String type), se pasa uno de los tipos


enumerados más arriba, se obtendrá uno de los directorios preestablecidos en el sistema para

CURSO DE DESARROLLO DE APLICACIONES ANDROID 8


TEMA 13. ALMACENAMIENTO DE DATOS

compartir archivos. Tal y como ya se ha mencionado, estos directorios son públicos y, a


diferencia del resto de directorios (privados) que puede gestionar una aplicación, los archivos
creados ahí no serán borrados cuando la aplicación sea desinstalada.

Existe también la posibilidad de cachear cierta información, en vez de grabarla de forma


permanente, invocando al método getExernalCacheDir() 5 que abrirá un objeto File que
representará el directorio externo donde poder guardar los archivos temporales. Si la
aplicación es desinstalada, estos archivos serán borrados No obstante, y como buena práctica
programática, será la propia aplicación la que deberá ser responsable de limitar el espacio que
ocupen dichos archivos temporales propios.

Por último, es interesante comentar que se podrán ocultar al Media Scanner los archivos
guardados en el almacenamiento externo, si se añade un archivo vacío con el nombre
“.nomedia”.

5
Si se usa el nivel de API 7, se deberá invocar a getExternalStrorageDirectoy() que devolverá un objeto File
representará la raíz del almacén externo. Se deberán guardar los archivos temporales bajo la ruta
Android/data/nombre.paquete.aplicacion/cache/

CURSO DE DESARROLLO DE APLICACIONES ANDROID 9


TEMA 13. ALMACENAMIENTO DE DATOS

Base de datos SQLite

Android incluye soporte completo para bases de datos SQLite 6. Las bases de datos creadas son
propiedad de las aplicaciones cuyos componentes podrán acceder a las mismas a través de sus
nombres.

Las bases de datos creadas en los dispositivos serán automáticamente gestionadas por
Android. Solo será necesario definir las sentencias para crear y actualizar cada base de datos.

Debido a que las bases de datos SQLite se almacenan en archivos, el acceso a las mismas
puede llegar a ser lento, por lo que es recomendable realizar las consultas de forma asíncrona,
por ejemplo, a través de la clase AsyncTask.

Para crear o actualizar una base de datos SQLite desde código, se deberá implementar una
subclase de SQLiteOpenHelper y sobrescribir su método onCreate(SQLiteDatabase db),
dentro del cual se deberán escribir los comandos que creen las tablas en la base de datos, ya
que el sistema invocará a este método en caso de que la base de datos aún no haya sido
creada.

Una vez creada la base de datos, se podrá obtener una instancia de la subclase de
SQLiteOpenHelper, a través del constructor que se haya definido. Este constructor deberá
invocar a super() pasando el nombre de la base de datos (que será el nombre del archivo que
la contenga) así como la versión de la misma. Si se incrementa la versión de la base de datos
vía código, se invocará al método onUpgrade(SQLiteDatabase db) el cual será responsable
de actualizar el esquema de la base de datos.

Para acceder a la base de datos en modo lectura o escritura, SQLiteOpenHelper proporciona


dos métodos, getReadableDatabase() y getWritableDatabase() los cuales devolverán un
objeto SQLiteDatabase que representará la base de datos y que proporcionará los métodos
adecuados para realizar operaciones contra la misma.

Es importante tener en cuenta que todas las tablas creadas en la base de datos deberán tener
una columna _id, que almacenará la clave primaria de la tabla. Existen diversos componentes
de Android que utilizarán por defecto esta columna. Además, cada tabla deberá tener asociada
una clase que la represente (con un atributo por cada columna, con sus métodos accesores
get/set) que implemente los métodos estáticos onCreate() y onUpdate(), que serán
invocados en los correspondientes métodos de SQLiteOpenHelper y que harán que el código
sea más legible y mantenible.

Se podrán lanzar queries de SQLite usando los diferentes métodos query() de la clase
SQLiteDatabase 7, los cuales aceptan varias combinaciones de parámetros para definir la tabla

6
Pese a estar en inglés, este es un muy buen tutorial de SQLite:
http://souptonuts.sourceforge.net/readme_sqlite_tutorial.html

CURSO DE DESARROLLO DE APLICACIONES ANDROID 10


TEMA 13. ALMACENAMIENTO DE DATOS

de la cual extraer los datos, las columnas que se desean obtener, sus alias, el agrupamiento,
etc. Además se podrá usar la clase auxiliar SQLiteQueryBuilder para crear queries complejas
a través de sus métodos.

También está disponible el método rawQuery() que acepta como parámetros un String que
represente una consulta SQL directa, así como un String[] para resolver los “?” que
aparezcan en la cláusula where de la consulta:

Cursor cursor = getReadableDatabase().


rawQuery("SELECT * FROM usuario WHERE _id = ?",
new String[] {id});

Para insertar, actualizar y borrar registros, se utilizarán sus métodos insert(), update() y
delete(). Además, se podrá ejecutar SQL “en crudo” a través de su método execSQL().

Con la clase ContentValues se podrán definir pares clave-valor, que representarán el nombre
de una columna así como el contenido de dicho registro, respectivamente. Estos objetos serán
útiles cuando se realicen inserciones o actualizaciones.

Cada consulta a una base de datos SQLite, devolverá un Cursor que apuntará a las filas
devueltas en dicha consulta, con el cual se podrá navegar por ellas, para leer las filas y las
columnas. Gracias a los cursores, Android puede almacenar los resultados de una consulta de
forma eficiente, sin tener que cargar todos los datos en memoria.

Los siguientes métodos de la clase Cursor serán frecuentemente utilizados:

• getCount(): devolverá el número de filas (elementos) obtenidos en la consulta.


• moveToFirst(): moverá el cursor a la primera fila obtenida en la consulta.
• moveToNext(): moverá el cursor a la siguiente fila obtenida en la consulta.
• isAfterLast(): comprobará si se ha llegado al último elemento obtenido en la
consulta.
• getLong(columnIndex), getString(columnIndex)…: devolverá el dato de la
columna cuyo índice se pasa como parámetro.
• getColumnIndexOrThrow(String): devolverá el índice de una columna, a través de
su nombre.

Una forma común de mostrar los resultados de una consulta es a través de una ListView.
Existe una actividad, ListActivity que facilita el uso de la vista ListView.

Como adaptador común, se puede usar SimpleCursorAdapter el cual permite establecer un


layout para cada fila de la ListView. Además, se deberá definir un array que contenga los

7
La clase SQLiteDatabase es la clase base para acceder a SQLite en Android, y proporciona métodos para abrir y
cerrar la conexión con la base de datos, consultar, actualizar y borrar datos.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 11


TEMA 13. ALMACENAMIENTO DE DATOS

nombres de las columnas y otro que contenga los identificadores de las Views que serán
pobladas con los datos.

El adaptador SimpleCursorAdapter mapeará las columnas de las Views basándose en el


Cursor que se le haya pasado, el cual será obtenido utilizando un Loader para no bloquear la
interfaz de usuario.

Más abajo, se muestra un ejemplo de uso de estas clases.

Debug de bases de datos

Se puede introspeccionar cualquier base de datos SQLite gracias a la herramienta que incluye
la SDK de Android, llamada sqlite3. Esta herramienta permite, a través de la consola, mostrar
las tablas de una base de datos, ejecutar comandos SQL y realizar otros tipos de operaciones
típicas sobre bases de datos como, por ejemplo, .dump para imprimir en un archivo el
contenido de una tabla, o .schema para imprimir la sentencia completa CREATE de una tabla
concreta. También permite la posibilidad de ejecutar comandos SQLite simultáneamente.

Para usar esta herramienta, se deberá abrir una consola de comandos y conectar con el
dispositivo virtual (AVD) o físico (conectado generalmente vía USB y que deberá estar
rooteado) donde resida la base de datos, utilizando del comando “adb –s nombre_emulador
shell” 8.

Una vez conectado vía adb, se podrá invocar la herramienta “sqlite3” especificando, de
forma opcional, la base de datos (con la ruta completa) que se quiere explorar. Las bases de
datos residen en /data/data/nombre.paquete.aplicacion/databases/.

Una vez se haya invocado a la herramienta, se podrán utilizar sus comandos en la consola. Una
lista de dichos comandos puede ser obtenida escribiendo “.help”.

Por ejemplo, para obtener un listado de las bases de datos de un emulador, bastará con
escribir los comandos en negrita 9:

8
adb = Android Debug Bridge. Herramienta de línea de comandos que abre una shell (consola de comandos Linux)
para comunicarse con un dispositivo Android (virtual o físico).
9
“emulator-5554" es, generalmente, el nombre del dispositivo virtual que está iniciado. Para averiguar el nombre
se podrá escribir el comando adb devices que mostrará una lista de los dispositivos virtuales iniciados y
dispositivos físicos conectados.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 12


TEMA 13. ALMACENAMIENTO DE DATOS

C:\Users\usario\Desktop>adb -s emulator-5554 shell


# sqlite3
sqlite3
SQLite version 3.6.22
Enter ".help" for instructions
Enter SQL statements terminated with a “;”
sqlite> .databases
.databases
seq name file
−−− −−−−−−−−−−−−−− −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
0 main

Ejemplo práctico

Se va a realizar una aplicación que será capaz de añadir filas en una tabla de base de datos, así
como borrarlas o editarlas, y que mostrará una lista con los datos que contenga dicha tabla.
Para ello, se utilizará el patrón DAO (Data Access Object) que permite simplificar la gestión de
los datos, ya que el DAO será responsable de la gestión de la conexión con la base de datos, así
como del acceso y modificación de los datos contenidos en las tablas. Además, convertirá los
objetos de base de datos (las tablas y sus filas) en objetos “reales” Java de forma que se evita
tener que acceder directamente a la capa de persistencia (a la base de datos) a través de
sentencias SQL.

La arquitectura de la aplicación constará, grosso modo, de tres “capas”: las capas de negocio y
persistencia (clases que gestionan el acceso a la base de datos), y la capa de presentación
(interfaz de usuario: layout y actividad).

Capas de negocio y persistencia

Lo primero que se creará serán tanto la base de datos como su modelo de datos. Para ello, se
deberá crear una clase que extienda de SQLiteOpenHelper y que sobrescribirá los métodos
onCreate(SQLiteDatabase db) y onUpdate(SQLiteDatabase db). El primer método se
encargará de crear la base de datos en caso de que no exista, mientras que el segundo estará
encargado de actualizarla, borrando todos los datos y recreando las tablas.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 13


TEMA 13. ALMACENAMIENTO DE DATOS

IdeasDatabaseHelper.java:

package com.cursoandroid.sqlite.ideas;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

public class IdeasDatabaseHelper extends SQLiteOpenHelper {

// Nombre y versión de la base de datos (será también el nombre del


// archivo que se generará en el dispositivo)
private static final String DATABASE_NAME = "ideas.db";
private static final int DATABASE_VERSION = 1;

public static final String TABLE_IDEAS = "ideas";


public static final String COLUMN_ID = "_id";
public static final String COLUMN_TITULO_IDEA = "titulo_idea";
public static final String COLUMN_TEXTO_IDEA = "texto_idea";
public static final String COLUMN_IMPORTANCIA = "importancia";

// Sentencia para la creación de la base de datos (crea la tabla)


private static final String DATABASE_CREATE = "create table "
+ TABLE_IDEAS + "("
+ COLUMN_ID + " integer primary key autoincrement, "
+ COLUMN_TITULO_IDEA + " text not null, "
+ COLUMN_TEXTO_IDEA + " text not null, "
+ COLUMN_IMPORTANCIA + " integer not null);";

// Constructor que deberá invocar a super()


public IdeasDatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}

// Se han de sobrescribir los métodos onCreate() y onUpdate()


@Override
public void onCreate(SQLiteDatabase database) {
// Se lanza la sentencia SQL para la creación de la tabla, sobre
// el objeto "database"
database.execSQL(DATABASE_CREATE);
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion,
int newVersion) {
Log.w(IdeasDatabaseHelper.class.getName(),
"Actualizando la base de datos desde la versión " +
oldVersion + " a la " + newVersion + ". Se eliminarán todos
los datos antiguos.");
db.execSQL("DROP TABLE IF EXISTS " + TABLE_IDEAS);
onCreate(db);
}
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 14


TEMA 13. ALMACENAMIENTO DE DATOS

A continuación se deberá crear el bean, objeto que contendrá la información de una fila de la
base de datos.

Idea.java:

package com.cursoandroid.sqlite.ideas;

public class Idea {


private long id;
private String tituloIdea;
private String textoIdea;
private int importancia;

// Métodos accesores
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}

public String getTituloIdea() {


return tituloIdea;
}
public void setTituloIdea(String tituloIdea) {
this.tituloIdea = tituloIdea;
}

public String getTextoIdea() {


return textoIdea;
}
public void setTextoIdea(String idea) {
this.textoIdea = idea;
}

public int getImportancia() {


return importancia;
}
public void setImportancia(int importancia) {
this.importancia = importancia;
}

// Este método será invocado en el ArrayAdapter de la ListView


@Override
public String toString() {

String iconoImportancia = "";


if (importancia == 0) // Alta
iconoImportancia = "**";
else if (importancia == 1)
iconoImportancia = "*";

return tituloIdea + " - " + iconoImportancia;


}
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 15


TEMA 13. ALMACENAMIENTO DE DATOS

El DAO estará encargado de mantener la conexión con la base de datos, así como de añadir
nuevas ideas, modificarlas, borrarlas y obtener un listado de las que haya almacenadas en la
tabla de la base de datos.

IdeasDataSource.java:

package com.cursoandroid.sqlite.ideas;

import java.util.ArrayList;
import java.util.List;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;

public class IdeasDataSource {

// Campos relacionados con la base de datos


private SQLiteDatabase database;
private IdeasDatabaseHelper dbHelper;
private String[] todasLasColumnas = {
IdeasDatabaseHelper.COLUMN_ID,
IdeasDatabaseHelper.COLUMN_TITULO_IDEA,
IdeasDatabaseHelper.COLUMN_TEXTO_IDEA,
IdeasDatabaseHelper.COLUMN_IMPORTANCIA };

// Constructor que inicializa el DAO,


// Inicializa la conexión con la base de datos, que está encapsulada
// en el Helper
public IdeasDataSource(Context context) {
dbHelper = new IdeasDatabaseHelper(context);
}

// Crea (en caso de que aún no exista) o abre la base de datos


public void open() throws SQLException {
database = dbHelper.getWritableDatabase();
}

// Cierra la conexión con la base de datos


public void close() {
dbHelper.close();
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 16


TEMA 13. ALMACENAMIENTO DE DATOS

// Método que insertará una Idea en base de datos


public Idea createIdea(String idea, String textoIdea, int importancia)
{

// Esta clase se usa para almacenar un conjunto de valores que el


// ContentResolver podrá procesar.
ContentValues valores = new ContentValues();
valores.put(IdeasDatabaseHelper.COLUMN_TITULO_IDEA, tituloIdea);
valores.put(IdeasDatabaseHelper.COLUMN_TEXTO_IDEA, textoIdea);
valores.put(IdeasDatabaseHelper.COLUMN_IMPORTANCIA, importancia);

// Inserción en base de datos. El método insert() devolverá la


// primary key (columna _id) que se autogenera al insertar.
// El método insertOrThrow() es igual que insert() pero lanza una
// SQLException en caso de no poder insertar
long idInsertado =
database.insertOrThrow(IdeasDatabaseHelper.TABLE_IDEAS,
null, valores);

// Una vez insertada la nueva Idea, se extrae de base de datos a


// través del cursor para que este método devuelva el bean completo
Cursor cursor = database.query(IdeasDatabaseHelper.TABLE_IDEAS,
todasLasColumnas,
IdeasDatabaseHelper.COLUMN_ID + " = " +
idInsertado, null, null, null, null);

cursor.moveToFirst();
Idea nuevaIdea = cursorAIdea(cursor);

// Es importante cerrar el cursor para evitar consumir memoria


// innecesaria
cursor.close();
return nuevaIdea;
}

public void deleteIdea(Idea idea) {


// Se extrae el identificador (primary key o PK) para localizar la
// idea a borrar gracias a la cláusla where
long idIdea = idea.getId();

// Se borra la idea de la base de datos


database.delete(IdeasDatabaseHelper.TABLE_IDEAS,
IdeasDatabaseHelper.COLUMN_ID + " = " + idIdea,
null);

Log.i(IdeasDataSource.class.getName(),
"Idea con id: " + idIdea + " borrada.");
}

public void updateIdea(Idea idea) {


// Se extrae la PK para localizar la idea a actualizar
// y se crea un ContentValues con los nuevos valores de la idea
long idIdea = idea.getId();
ContentValues valores = new ContentValues();
valores.put(IdeasDatabaseHelper.COLUMN_TITULO_IDEA,
idea.getTituloIdea());
valores.put(IdeasDatabaseHelper.COLUMN_TEXTO_IDEA,
idea.getTextoIdea());

CURSO DE DESARROLLO DE APLICACIONES ANDROID 17


TEMA 13. ALMACENAMIENTO DE DATOS

valores.put(IdeasDatabaseHelper.COLUMN_IMPORTANCIA,
idea.getImportancia());

database.update(IdeasDatabaseHelper.TABLE_IDEAS, valores,
IdeasDatabaseHelper.COLUMN_ID + " = " + idIdea,
null);

Log.i(IdeasDataSource.class.getName(),
"Idea con id: " + idIdea + " actualizada correctamente.");
}

public Idea getIdea(long idIdea) {


// Se busca una Idea a través de su id
Cursor cursor = baseDeDatos.query(IdeasDatabaseHelper.TABLE_IDEAS,
todasLasColumnas,
IdeasDatabaseHelper.COLUMN_ID + " = " +
idIdea, null, null, null, null);

if (cursor.getCount() == 1) {

cursor.moveToFirst();
Idea idea = cursorAIdea(cursor);

// Es importante cerrar el cursor para evitar consumir memoria


// innecesaria
cursor.close();

Log.i(IdeasDataSource.class.getName(),
"Idea con id: " + idIdea + " obtenida correctamente.");
return idea;
}
else {

Log.w(IdeasDataSource.class.getName(),
"ATENCIÓN. No se ha encontrado Idea con id: " + idIdea);
return null;
}
}

public List<Idea> getAllIdeas() {


// Lista de Ideas
List<Idea> ideas = new ArrayList<Idea>();

// Consulta a la base de datos. El no añadir argumentos salvo la


// tabla y las columnas a obtener hará que se lance la query
// "select * from ideas;"
Cursor cursor = database.query(IdeasDatabaseHelper.TABLE_IDEAS,
todasLasColumnas, null, null, null, null, null);

// Se mueve el cursor al primer resultado


cursor.moveToFirst();

// Se recorre el cursor para ir llenando la List<Idea>


while (!cursor.isAfterLast()) {
Idea idea = cursorAIdea(cursor);
ideas.add(idea);
cursor.moveToNext();
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 18


TEMA 13. ALMACENAMIENTO DE DATOS

// Es importante cerrar el cursor para evitar consumir memoria


// innecesaria
cursor.close();
return ideas;
}

// Método auxiliar que permite convertir cursor (fila de la tabla


// ideas) en un objeto Idea
private Idea cursorAIdea(Cursor cursor) {
Idea idea = new Idea();
idea.setId(cursor.getLong(0));
idea.setTituloIdea(cursor.getString(1));
idea.setTextoIdea(cursor.getString(2));
idea.setImportancia(cursor.getInt(3));
return idea;
}
}

Capa de presentación

Se construirá una interfaz de usuario con dos pantallas. En la primera se mostrará un listado de
ideas y un menú con la opción para insertar una nueva idea. Si se mantiene pulsada una idea,
se mostrará un menú contextual que ofrecerá editar o borrar la idea. En la segunda pantalla se
mostrará un sencillo formulario que permitirá introducir una nueva idea o editar una ya
existente.

El layout de la primera pantalla básicamente será una TextView que será usada para
componer la lista, gracias a las funcionalidades extras que se heredan al extender de
ListActivity.

lista_ideas.xml:

<?xml version="1.0" encoding="utf-8"?>


<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/list_content"
android:layout_width="fill_parent"
android:layout_height="60dp"
android:gravity="center_vertical"
android:textAppearance="?android:attr/textAppearanceLarge">
</TextView>

El layout de la segunda pantalla mostrará el formulario para la edición de ideas.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 19


TEMA 13. ALMACENAMIENTO DE DATOS

detalle_idea.xml

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<EditText
android:id="@+id/tituloIdea"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/titulo_idea" >

<requestFocus />
</EditText>

<Spinner
android:id="@+id/spinnerImportanciaIdea"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

<EditText
android:id="@+id/textoIdea"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="start|top"
android:layout_weight="0.23"
android:ems="10"
android:hint="@string/introduce_texto_idea"
android:inputType="textMultiLine" />

<Button
android:id="@+id/btnGuardar"
style="@android:style/Widget.Holo.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:onClick="guardar"
android:text="@string/guardar" />

</LinearLayout>

En cuanto a las actividades, ambas utilizarán el DAO (IdeasDataSource) para establecer la


conexión con la capa de negocio. Cabe destacar la implementación de los métodos onPause()
y onResume() que cerrarán y abrirán la conexión con la base de datos, respectivamente, de
forma que se liberen recursos cuando la aplicación esté en segundo plano. Además, el método
onResume() será el encargado de cargar los datos de la lista (las ideas que ya contenga la base
de datos) puesto que es siempre invocado justo antes de que la actividad pase al primer plano.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 20


TEMA 13. ALMACENAMIENTO DE DATOS

Para cargar la lista de ideas, se ha utilizado un ArrayAdapter<Idea> aunque sería más


aconsejable realizar la carga de datos a través de un Loader (CursorLoader) ya que no se
bloquearía la interfaz de usuario mientras se cargan los datos (evidentemente, si se trata de
una lista con pocas decenas de elementos, el tiempo de carga es imperceptible). Se
desaconseja el uso de SimpleCursorLoader ya que obliga a un Cursor a realizar consultas
directas a la base de datos desde la interfaz de usuario, por lo que puede causar malas
respuestas de la interfaz de usuario e, incluso, errores ANR (Application Not Responding)

IdeasActivity.java:

package com.cursoandroid.ui;

import java.util.List;

import android.app.ListActivity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;

import com.cursoandroid.sqlite.ideas.Idea;
import com.cursoandroid.sqlite.ideas.IdeasDataSource;

public class IdeasActivity extends ListActivity {

private IdeasDataSource ideasDataSource;

@Override
public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

// La lista de items se carga en un array. Los elementos a cargar


// están en el archivo strings.xml
ideasDataSource = new IdeasDataSource(this);
ideasDataSource.open();
// Se obtiene la vista tipo lista
ListView lv = getListView();
// Se activa el filtrado de texto en la lista.
// Cuando el usuario comience a escribir, la lista se filtrará
// automáticamente
lv.setTextFilterEnabled(true);

CURSO DE DESARROLLO DE APLICACIONES ANDROID 21


TEMA 13. ALMACENAMIENTO DE DATOS

// Cuando se pulse un elemento de la lista, se mostrará el detalle


// de la Idea lanzando un Intent a la otra actividad.
lv.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
// El id NO es el identificador (PK) de la Idea en la base
// de datos. Es el identificador en la lista
editarIdea(id);
}
});

// Con este método se asigna el menú contextual (para editar y


// borrar la idea seleccionada) a la ListView
registerForContextMenu(lv);
}

private void cargarListaIdeas() {

List<Idea> ideas = ideasDataSource.getAllIdeas();


// Cada elemento de la lista será una TextView, definida en el
// layout lista_ideas.xml
setListAdapter(new ArrayAdapter<Idea>(this,
R.layout.lista_ideas,
ideas));
}

@Override
protected void onResume() {
ideasDataSource.open();
cargarListaIdeas();
super.onResume();
}

@Override
protected void onPause() {
ideasDataSource.close();
super.onPause();
}

@Override
public void onBackPressed() {
Intent intent = new Intent(this,
AlmacenamientoDatosMainActivity.class);
// Se vuelve a la actividad anterior, sin invocar a una nueva
// instancia de la misma
intent.setFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
startActivity(intent);
}

// Método que creará el menú contextual para la ListView


// El menú contextual está definido en menu_contextual_ideas.xml
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_contextual_ideas, menu);
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 22


TEMA 13. ALMACENAMIENTO DE DATOS

@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_idea, menu);
return true;
}

// Acciones que se realizarán al seleccionar el menú de la actividad


@Override
public boolean onOptionsItemSelected(MenuItem item) {

// Se gestiona la opción seleccionada


switch (item.getItemId()) {
case R.id.nueva:
nuevaIdea();
return true;
case R.id.buscar:
buscarIdea();
return true;
default:
return super.onOptionsItemSelected(item);
}
}

// Acciones que se realizarán al seleccionar un ítem del menú


// contextual
@Override
public boolean onContextItemSelected(MenuItem item) {

// Método (rocambolesco) para obtener la Idea seleccionada, lo cual


// puede ser útil para mostrar algún aviso o confirmación de la
// acción al usuario:

// 1. El menuItem recibido es sobre el que se ha mantenido la


// pulsación para mostrar el menú contextual.
// "info" contiene el identificador del menuItem seleccionado,
// sobre el que se realizará la acción del menú contextual.
AdapterContextMenuInfo info =
(AdapterContextMenuInfo) item.getMenuInfo();
// 2. Obtención del objeto de tipo Idea contenido en el menuItem
// seleccionado en la posición “info.id”
// Idea idea = (Idea)
// ((ListView) info.targetView.getParent())
// .getItemAtPosition((int) info.id);

switch (item.getItemId()) {
case R.id.editar:
editarIdea(info.id);
return true;
case R.id.borrar:
borrarIdea(info.id);
return true;
default:
return super.onContextItemSelected(item);
}
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 23


TEMA 13. ALMACENAMIENTO DE DATOS

private void nuevaIdea() {


Intent intent = new Intent(this, DetalleIdeaActivity.class);
Bundle bundle = new Bundle();
bundle.putInt(DetalleIdeaActivity.MODO,
DetalleIdeaActivity.MODO_NUEVA_IDEA);
intent.putExtras(bundle);
startActivity(intent);
}

private void buscarIdea() {


// Se muestra el teclado
InputMethodManager inputManager =
(InputMethodManager) this.getSystemService(
Context.INPUT_METHOD_SERVICE);
inputManager.showSoftInput(getListView(),
InputMethodManager.SHOW_FORCED);
}

private void editarIdea(long id) {

@SuppressWarnings("unchecked")
ArrayAdapter<Idea> adapter = (ArrayAdapter<Idea>) getListAdapter();
Idea idea = (Idea) adapter.getItem((int) id);

Intent intent = new Intent(this, DetalleIdeaActivity.class);


Bundle bundle = new Bundle();
bundle.putInt(DetalleIdeaActivity.MODO,
DetalleIdeaActivity.MODO_EDITAR_IDEA);
bundle.putLong(DetalleIdeaActivity.ID_IDEA, idea.getId());
intent.putExtras(bundle);
startActivity(intent);
}

private void borrarIdea(long id) {

@SuppressWarnings("unchecked")
ArrayAdapter<Idea> adapter = (ArrayAdapter<Idea>)
getListAdapter();
Idea idea = (Idea) adapter.getItem((int) id);

ideasDataSource.deleteIdea(idea);
adapter.remove(idea);
adapter.notifyDataSetChanged();
}
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 24


TEMA 13. ALMACENAMIENTO DE DATOS

DetalleIdeaActivity.java

package com.cursoandroid.ui;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.Toast;

import com.cursoandroid.sqlite.ideas.Idea;
import com.cursoandroid.sqlite.ideas.IdeasDataSource;

public class DetalleIdeaActivity extends Activity {

public final static String MODO = "MODO";


public final static String ID_IDEA = "_ID";
public final static int MODO_NUEVA_IDEA = 0;
public final static int MODO_EDITAR_IDEA = 1;

private IdeasDataSource ideasDataSource;


private int MODO_ACTUAL = 0;
private Intent intentIdeasActivity = null;

// Variable para "recordar" el identificador de la Idea que se está


// editando
private long idIdea;

@Override
public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
setContentView(R.layout.detalle_idea);

// Se inicializa el aquí intent que volverá a la actividad anterior,


// sin invocar a una nueva instancia de la misma.
intentIdeasActivity = new Intent(this, IdeasActivity.class);
intentIdeasActivity.setFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);

Spinner listaImportancia =
(Spinner) findViewById(R.id.spinnerImportanciaIdea);
String[] tipoAlmacen =
getResources().getStringArray(R.array.importancia_idea);

ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,


android.R.layout.simple_spinner_item, tipoAlmacen);
adapter.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item);
listaImportancia.setAdapter(adapter);

Bundle bundle = getIntent().getExtras();


MODO_ACTUAL = bundle.getInt(MODO);
ideasDataSource = new IdeasDataSource(this);

CURSO DE DESARROLLO DE APLICACIONES ANDROID 25


TEMA 13. ALMACENAMIENTO DE DATOS

ideasDataSource.open();

if (MODO_ACTUAL == MODO_EDITAR_IDEA)
cargarIdea(bundle.getLong(ID_IDEA));
}

@Override
protected void onResume() {
ideasDataSource.open();
super.onResume();
}

@Override
protected void onPause() {
ideasDataSource.close();
super.onPause();
}

private void cargarIdea(long idIdea) {


// Se obtiene la idea a editar
Idea idea = ideasDataSource.getIdea(idIdea);
this.idIdea = idea.getId();

// Se vuelca su información en el formulario


((EditText) findViewById(R.id.tituloIdea))
.setText(idea.getTituloIdea());
((EditText) findViewById(R.id.textoIdea))
.setText(idea.getTextoIdea());
((Spinner) findViewById(R.id.spinnerImportanciaIdea))
.setSelection(idea.getImportancia());
}

public void guardar(View view) {

Idea idea = new Idea();


String tituloIdea = ((EditText) findViewById(R.id.tituloIdea))
.getText().toString();
String textoIdea = ((EditText) findViewById(R.id.textoIdea))
.getText().toString();
int importancia = ((Spinner)
findViewById(R.id.spinnerImportanciaIdea))
.getSelectedItemPosition();

if (tituloIdea.equals("") || textoIdea.equals(""))
Toast.makeText(this, getString(R.string.rellena_campos_idea),
Toast.LENGTH_LONG).show();
else {
idea.setId(idIdea);
idea.setTituloIdea(tituloIdea);
idea.setTextoIdea(textoIdea);
idea.setImportancia(importancia);

switch (MODO_ACTUAL) {
case MODO_NUEVA_IDEA:
ideasDataSource.createIdea(tituloIdea, textoIdea,
importancia);
break;
case MODO_EDITAR_IDEA:

CURSO DE DESARROLLO DE APLICACIONES ANDROID 26


TEMA 13. ALMACENAMIENTO DE DATOS

ideasDataSource.updateIdea(idea);
break;
default:
break;
}

// Se vuelve a la actividad anterior, sin invocar a una


// nueva instancia de la misma.
// (Otra opción sería invocar a finish(), ya que esta
// actividad ya no se utilizará hasta que se vuelva a
// solicitar desde la lista de ideas, aunque sería menos
// eficiente si se consultan muchos detalles de ideas)
startActivity(intentIdeasActivity);
}
}
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 27

Vous aimerez peut-être aussi