lunes, 16 de abril de 2012

Persistencia de datos con SQLite sobre Android: parte 3.


Desarrollo sobre la plataforma Android:


Hola a todos nuevamente, en este caso vamos a seguir un poco mas sobre el tema de persistencia de datos, utilizando el motor de base de datos SQLite, pero a esto le sumaremos Content Provider ya que en el anterior post sobre SQLite no pude hablar sobre ello ;).

Ahora porque y para que vamos a utilizar Content Provider???

En el modelo de seguridad de Android, los archivos escritos por una solo aplicación, no se pueden leer o escribir por cualquier otra aplicación. Cada programa tiene su propio ID de usuario Linux y directorio de datos (data/data/packagename) y además su propio espacio de memoria protegida.
Por este motivo los programas pueden comunicarse entre si solo de dos maneras:

  • Inter-Process Communication (IPC) o comunicación entre procesos : Uno de los procesos declara una API arbitraria utilizando el Android Interface Definition Language (AIDL) y la interfaz IBinder. Los parámetros calculan las referencias de forma segura y eficiente entre los procesos cuando la API se llama. Esta técnica avanzada se utiliza para las llamadas a procedimientos remotos de hilos que tienen service en background. (Como explica este tema es sobre programación avanzada y va a escapar los temas que vamos a trabajar por ahora, así que lo voy a dejar para mas adelante ;) ).
  • ContentProvider: Los procesos se registran en el sistema como proveedores de cierto tipo de datos. Cuando esa información es solicitada, las llama Android atraves de una API fija para consultar o modificar el contenido de la manera que mejor les parezca. Y esta es la técnica que vamos a ver y utilizar en este post ;)

Cualquier pieza de información gestionado por una ContentProvider se dirige a través de un URI que tiene este aspecto:

content://authority/path /id

Donde:

  • content:// es el prefijo estándar que se requiere.
  • Authority: es el nombre del proveedor, es recomendado utilizar el nombre completo del paquete para prevenir cualquier conflicto de nombres.
  • Path: es un directorio virtual donde se identifica al proveedor de tipo de datos que se solicita.
  • ID: es la llave primaria de un registro especifico que se solicita. Al solicitar todos los registros de un tipo particular, omite este y el ultimo es recortado.

Android ya viene con varios proveedores incluidos estos:

  • content://browser
  • content://contacts
  • content://media
  • content://setting

Para realizar una demostración del uso de Content Provider vamos a modificar el ejemplo utilizado anteriores post. Por ejemplo para ese ejemplo, estos podrían ser URIs validos:

content://com.prueba.gabriel.sqlite3/PruebasSQLite3/5 - un evento simple con un _ID = 5
content://com.prueba.gabriel.sqlite3/PruebasSQLite3 – todos los eventos

Ahora vamos a ver un poco de código, lo primero que vamos a hacer, es agregar un par de constantes al archivo Constantes.java, para que quede de esta manera:

package com.prueba.gabriel.sqlite3;


import android.net.Uri;

import android.provider.BaseColumns;

public interface Constantes extends BaseColumns {
public static final String TABLE_NAME = "PruebasSQLite3";
public static final String AUTHORITY = "com.prueba.gabriel.sqlite3";
public static final Uri CONTENT_URI = Uri.parse("content://"
+ AUTHORITY + "/" + TABLE_NAME);

public static final String TIME = "tiempo";
public static final String TITLE = "titulo";
}


Los archivos main.xml e item.xml no van a necesitar modificaciones, asi que ahora vamos a modificar el archivo PruebasSQLite3Activity.java, para que quede de la siguiente manera:

package com.prueba.gabriel.sqlite3;

import static android.provider.BaseColumns._ID;
import static com.prueba.gabriel.sqlite3.Constantes.CONTENT_URI;
import static com.prueba.gabriel.sqlite3.Constantes.TIME;
import static com.prueba.gabriel.sqlite3.Constantes.TITLE;

import com.prueba.gabriel.sqlite3.R;

import android.app.ListActivity;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Bundle;
import android.widget.SimpleCursorAdapter;

public class PruebasSQLite3Activity extends ListActivity {
private static String[] FROM = { _ID, TIME, TITLE, };
private static int[] TO = { R.id.rowid, R.id.time, R.id.title, };
private static String ORDER_BY = TIME + " DESC";

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
addEvent("Content Provider desde aprendiendodeandroidymas.blogspot!!!");
Cursor cursor = getEvents();
showEvents(cursor);
}

private void addEvent(String string) {
ContentValues values = new ContentValues();
values.put(TIME, System.currentTimeMillis());
values.put(TITLE, string);
getContentResolver().insert(CONTENT_URI, values);
}

private Cursor getEvents() {
return managedQuery(CONTENT_URI, FROM, null, null, ORDER_BY);
}

private void showEvents(Cursor cursor) {
SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,
R.layout.item, cursor, FROM, TO);
setListAdapter(adapter);
}
}


El método onCreate() se pone simple porque no hay ningún objeto de base de datos al que debamos realizarle un seguimiento. Por eso no vamos a necesitar tampoco un bloque try/catch/finally también vamos a poder eliminar las referencias hacia PruebaSQLiteDataHelper.
También como podrán observar para agregar una linea debemos modificar dos lineas de código en el método addEvent().

La llamada a getWritableDatabase() se ha ido, y la llamada a insertOrThrow () se sustituye por getContentResolver().insert (). En lugar de un manejador de base de datos, vamos a utilizar una URI de proveedor de contenido.

Tambien podemos ver como el médoto getEvent() se simplifica mucho cuando utilizamos un ContentProvider. Aquí vamos a utilizar el método Activity.managedQuery( ), donde pasamos la URI del contenido, de la lista de columnas que nos interesa y el orden en que deben ser ordenados.
Al eliminar todas las referencias a la base de datos, hemos desacoplado el cliente del proveedor de base de datos. El cliente es bastante mas sencillo, pero ahora debemos implementar una pieza que no teníamos antes.

Ahora vamos a lo bueno ;) vamos a implementar el Content Provider:

Un Content Provider es un objeto de alto nivel como una actividad, que debe ser declarado en el sistema. Así, el primer paso a realizar es añadirlo a su archivo AndroidManifest.xml antes de la etiqueta <activity> (como un hijo de <application>) quedándonos de esta manera:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.prueba.gabriel.sqlite3"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="3" android:targetSdkVersion="10" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<provider android:name=".PruebaSQLite3ProveedorEventos"
android:authorities="com.prueba.gabriel.sqlite3"/>
<activity android:name=".PruebasSQLite3Activity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />ç
</intent-filter>
</activity>
</application>

</manifest>

  • android:name es el nombre de la clase que vamos a utilizar como proveedor de contenido.
  • Y android:authorities es la cadena string que se utiliza como URI de donde estara el contenido.

Lo siguiente sera crear la clase PruebaSQLite3ProveedorEventos que debe extenderse de Content Provider. En este caso vamos a crear el esquema básico, que se vera de la siguiente manera:

package com.prueba.gabriel.sqlite3;

import static android.provider.BaseColumns._ID;
import static com.prueba.gabriel.sqlite3.Constantes.AUTHORITY;
import static com.prueba.gabriel.sqlite3.Constantes.CONTENT_URI;
import static com.prueba.gabriel.sqlite3.Constantes.TABLE_NAME;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.text.TextUtils;

public class PruebaSQLite3ProveedorEventos extends ContentProvider {
private static final int EVENTS = 1;
private static final int EVENTS_ID = 2;

private static final String CONTENT_TYPE
= "vnd.android.cursor.dir/vnd.prueba.gabriel.sqlite3/PruebasSQLite3";

private static final String CONTENT_ITEM_TYPE
= "vnd.android.cursor.item/vnd.prueba.gabriel.sqlite3/PruebasSQLite3";

private PruebaSQLiteDataHelper pruebassqlite;
private UriMatcher uriMatcher;

@Override
public boolean onCreate() {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, "PruebasSQLite3", EVENTS);
uriMatcher.addURI(AUTHORITY, "PruebasSQLite3/#", EVENTS_ID);
pruebassqlite = new PruebaSQLiteDataHelper(getContext());
return true;
}

@Override
public Cursor query(Uri uri, String[] projection,
String selection, String[] selectionArgs, String orderBy) {
if (uriMatcher.match(uri) == EVENTS_ID) {
long id = Long.parseLong(uri.getPathSegments().get(1));
selection = appendRowId(selection, id);
}

SQLiteDatabase db = pruebassqlite.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, projection, selection,
selectionArgs, null, null, orderBy);

cursor.setNotificationUri(getContext().getContentResolver(),
uri);
return cursor;
}

@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case EVENTS:
return CONTENT_TYPE;
case EVENTS_ID:
return CONTENT_ITEM_TYPE;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
}

@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = pruebassqlite.getWritableDatabase();

if (uriMatcher.match(uri) != EVENTS) {
throw new IllegalArgumentException("Unknown URI " + uri);
}

long id = db.insertOrThrow(TABLE_NAME, null, values);

Uri newUri = ContentUris.withAppendedId(CONTENT_URI, id);
getContext().getContentResolver().notifyChange(newUri, null);
return newUri;
}

@Override
public int delete(Uri uri, String selection,
String[] selectionArgs) {
SQLiteDatabase db = pruebassqlite.getWritableDatabase();
int count;
switch (uriMatcher.match(uri)) {
case EVENTS:
count = db.delete(TABLE_NAME, selection, selectionArgs);
break;
case EVENTS_ID:
long id = Long.parseLong(uri.getPathSegments().get(1));
count = db.delete(TABLE_NAME, appendRowId(selection, id),
selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}

getContext().getContentResolver().notifyChange(uri, null);
return count;
}

@Override
public int update(Uri uri, ContentValues values,
String selection, String[] selectionArgs) {
SQLiteDatabase db = pruebassqlite.getWritableDatabase();
int count;
switch (uriMatcher.match(uri)) {
case EVENTS:
count = db.update(TABLE_NAME, values, selection,
selectionArgs);
break;
case EVENTS_ID:
long id = Long.parseLong(uri.getPathSegments().get(1));
count = db.update(TABLE_NAME, values, appendRowId(
selection, id), selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}

getContext().getContentResolver().notifyChange(uri, null);
return count;
}

private String appendRowId(String selection, long id) {
return _ID + "=" + id
+ (!TextUtils.isEmpty(selection)
? " AND (" + selection + ')'
: "");
}
}


PruebaSQLite3ProveedorEventos maneja dos tipos de eventos:


EVENTS (MIME type CONTENT_TYPE): un directorio o lista de eventos.
EVENTS_ID (MIME type CONTENT_ITEM_TYPE): un único evento.

Por las dudas aclaro:
Multipurpose Internet Mail Extensions (MIME) es un estándar de Internet para describir el tipo de cualquier tipo de contenido.

En termino de URI, la diferencia es que en el primero no se especifica un identificador o id, pero si en el segundo. Para esto vamos a utilizar la clase Android UriMatcher para analizar la URI y ver que cliente en especifico se trata. Y volvemos a utilizar la clase PruebaSQLiteDataHelper que ya creamos en los anteriores post, para gestionar la base de datos real en el interior del Provider ;)



Bueno esto es todo por ahora, en el tema del manejo de base de datos SQLite, tengan en cuanta que con esto no solo se puede mostrar texto. Se pueden mostrar imágenes o cualquier otra cosa que nosotros necesitemos. También, creo que es una herramienta muy potente, donde lo único que les puede llegar a traer algun tipo de problemas es al crear la URI, pero con un poquito de practica y ver como funciona UriMatcher se soluciona de manera sencilla. Espero que les allá gustado, en próximos post vamos a ver temas diferentes a este. Y como siempre, estos son los enlaces de la docu oficial y los enlaces de los otros lugares de donde recaude información.

Documentación oficial:



Otros Enlaces:




Saludos a todos, Gabriel E. Pozo