Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Tema 13. Almacenamiento de Datos
Tema 13. Almacenamiento de Datos
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
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.
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.
3
Esta ruta se crea, lógicamente, en la memoria interna del dispositivo.
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:
Por ejemplo:
try {
String textoArchivoInterno = "";
String linea = "";
FileInputStream fis = openFileInput(NOMBRE_ARCHIVO);
((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();
}
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).
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.
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.
<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.
Por ejemplo:
if (Environment.MEDIA_MOUNTED.equals(estadoAlmacenExterno)) {
// Se puede leer y escribir
}
Para guardar archivos en el almacén externo del dispositivo, se realizarán los siguientes pasos:
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/
De forma análoga, se podrá leer un archivo previamente grabado en la memoria externa del
dispositivo con estos pasos:
Por ejemplo:
try {
String textoArchivoExterno = "";
String linea = "";
// Se obtiene el archivo
File file = new File(getExternalFilesDir(null), NOMBRE_ARCHIVO);
FileInputStream fis = new FileInputStream(file);
((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();
}
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/
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.
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
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:
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.
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.
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.
nombres de las columnas y otro que contenga los identificadores de las Views que serán
pobladas con los 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.
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).
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.
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;
@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);
}
}
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;
// Métodos accesores
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
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;
cursor.moveToFirst();
Idea nuevaIdea = cursorAIdea(cursor);
Log.i(IdeasDataSource.class.getName(),
"Idea con id: " + idIdea + " borrada.");
}
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.");
}
if (cursor.getCount() == 1) {
cursor.moveToFirst();
Idea idea = cursorAIdea(cursor);
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;
}
}
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:
detalle_idea.xml
<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>
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;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@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);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_idea, menu);
return true;
}
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);
}
}
@SuppressWarnings("unchecked")
ArrayAdapter<Idea> adapter = (ArrayAdapter<Idea>) getListAdapter();
Idea idea = (Idea) adapter.getItem((int) id);
@SuppressWarnings("unchecked")
ArrayAdapter<Idea> adapter = (ArrayAdapter<Idea>)
getListAdapter();
Idea idea = (Idea) adapter.getItem((int) id);
ideasDataSource.deleteIdea(idea);
adapter.remove(idea);
adapter.notifyDataSetChanged();
}
}
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;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.detalle_idea);
Spinner listaImportancia =
(Spinner) findViewById(R.id.spinnerImportanciaIdea);
String[] tipoAlmacen =
getResources().getStringArray(R.array.importancia_idea);
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();
}
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:
ideasDataSource.updateIdea(idea);
break;
default:
break;
}