viernes, 19 de diciembre de 2014

Construcción de Content Providers en Android



En este nuevo apartado del curso vamos a tratar el temido [o a veces incomprendido] tema de los Content Providers.

Un Content Provider no es más que el mecanismo proporcionado por la plataforma Android para permitir compartir información entre aplicaciones. Una aplicación que desee que todo o parte de la información que almacena esté disponible de una forma controlada para el resto de aplicaciones del sistema deberá proporcionar un content provider a través del cual se pueda realizar el acceso a dicha información. Este mecanismo es utilizado por muchas de las aplicaciones estándar de un dispositivo Android, como por ejemplo la lista de contactos, la aplicación de SMS, o el calendario/agenda. Esto quiere decir que podríamos acceder a los datos gestionados por estas aplicaciones desde nuestras propias aplicaciones Android haciendo uso de los content providers correspondientes.

Son por tanto dos temas los que debemos tratar en este apartado, por un lado a construir nuevos content providers personalizados para nuestras aplicaciones, y por otro utilizar un content provider ya existente para acceder a los datos publicados por otras aplicaciones.

En gran parte de la bibliografía sobre programación en Android se suele tratar primero el tema del acceso a content providers ya existentes (como por ejemplo el acceso a la lista de contactos de Android) para después pasar a la construcción de nuevos content providers personalizados. Yo sin embargo voy a tratar de hacerlo en orden inverso, ya que me parece importante conocer un poco el funcionamiento interno de un content provider antes de pasar a utilizarlo sin más dentro de nuestras aplicaciones. Así, en este primer apartado sobre el tema veremos cómo crear nuestro propio content provider para compartir datos con otras aplicaciones, y en el próximo apartado veremos cómo utilizar este mecanismo para acceder directamente a datos de terceros.

Empecemos a entrar en materia. Para añadir un content provider a nuestra aplicación tendremos que:
      1. Crear una nueva clase que extienda a la clase android ContentProvider.
      2. Declarar el nuevo content provider en nuestro fichero AndroidManifest.xml

Por supuesto nuestra aplicación tendrá que contar previamente con algún método de almacenamiento interno para la información que queremos compartir. Lo más común será disponer de una base de datos SQLite, por lo que será esto lo que utilizaré para todos los ejemplos de este apartado, pero internamente podríamos tener los datos almacenados de cualquier otra forma, por ejemplo en ficheros de texto, ficheros XML, etc. El content provider será el mecanismo que nos permita publicar esos datos a terceros de una forma homogénea y a través de una interfaz estandarizada.

Un primer detalle a tener en cuenta es que los registros de datos proporcionados por un content provider deben contar siempre con un campo llamado _ID que los identifique de forma unívoca del resto de registros. Como ejemplo, los registros devueltos por un content provider de clientes podría tener este aspecto:


Sabiendo esto, es interesante que nuestros datos también cuenten internamente con este campo _ID (no tiene por qué llamarse igual) de forma que nos sea más sencillo después generar los resultados del content provider.

Con todo esto, y para tener algo desde lo que partir, vamos a construir en primer lugar una aplicación de ejemplo muy sencilla con una base de datos SQLite que almacene los datos de una serie de clientes con una estructura similar a la tabla anterior. Para ello seguiremos los mismos pasos que ya comentamos en los apartados dedicados al tratamiento de bases de datos SQLite en Android (consultar índice del curso).

Por volver a recordarlo muy brevemente, lo que haremos será crear una nueva clase que extienda a SQLiteOpenHelper, definiremos las sentencias SQL para crear nuestra tabla de clientes, e implementaremos finalmente los métodos onCreate() y onUpgrade(). El código de esta nueva clase, que yo he llamado ClientesSqliteHelper, quedaría como sigue:


Como notas relevantes del código anterior:

*Nótese el campo "_id" que hemos incluido en la base de datos de clientes por lo motivos indicados un poco más arriba. Este campo lo declaramos como INTEGER PRIMARY KEY AUTOINCREMENT, de forma que se incremente automáticamente cada vez que insertamos un nuevo registro en la base de datos.

* En el método onCreate(), además de ejecutar la sentencia SQL para crear la tabla Clientes, también inserta varios registros de ejemplo.
* Para simplificar el ejemplo, el método onUpgrade() se limita a eliminar la tabla actual y crear una nueva con la nueva estructura. En una aplicación real habría que hacer probablemente la migración de los datos a la nueva base de datos.

Dado que la clase anterior ya se ocupa de todo, incluso de insertar algunos registro de ejemplo con los que podamos hacer pruebas, la aplicación principal de ejemplo no mostrará en principio nada en pantalla ni hará nada con la información. Esto lo he decidido así para no complicar el código de la aplicación innecesariamente, ya que no nos va a interesar el tratamiento directo de los datos por parte de la aplicación principal, sino su utilización a través del content provider que vamos a construir.

Una vez que ya contamos con nuestra aplicación de ejemplo y su base de datos, es hora de empezar a construir el nuevo content provider que nos permitirá compartir sus datos con otras aplicaciones.

Lo primero que vamos a comentar es la forma con que se hace referencia en Android a los content providers. El acceso a un content provider se realiza siempre mediante una URI. Una URI no es más que una cadena de texto similar a cualquiera de las direcciones web que utilizamos en nuestro navegador. Al igual que para acceder a mi blog lo hacemos mediante la dirección "http://www.sgoliver.net", para acceder a un content provider utilizaremos una dirección similar a:

"content://net.sgoliver.android.contentproviders/clientes".

Las direcciones URI de los content providers están formadas por 3 partes. En primer lugar el prefijo "content://" que indica que dicho recurso deberá ser tratado por un content provider. En segundo lugar se indica el identificador en sí del content provider, también llamado authority. Dado que este dato debe ser único es una buena práctica utilizar un authority de tipo "nombre de clase java invertido", por ejemplo en mi caso "net.sgoliver.android.contentproviders". Por último, se indica la entidad concreta a la que queremos acceder dentro de los datos que proporciona el content provider. En nuestro caso será simplemente la tabla de "clientes", ya que será la única existente, pero dado que un content provider puede contener los datos de varias entidades distintas en este último tramo de la URI habrá que especificarlo.
Indicar por último que en una URI se puede hacer referencia directamente a un registro concreto de la entidad seleccionada. Esto se haría indicando al final de la URI el ID de dicho registro. Por ejemplo la uri "content://net.sgoliver.android.contentproviders/clientes/23" haría referencia directa al cliente con _ID = 23.

Todo esto es importante ya que será nuestro content provider el encargado de interpretar/parsear la URI completa para determinar los datos que se le están solicitando. Esto lo veremos un poco más adelante.

Sigamos. El siguiente paso será extender a la clase ContentProvider.

Si echamos un vistazo a los métodos abstractos que tendremos que implementar veremos que tenemos los siguientes:

> onCreate()
> query()
> insert()
> update()
> delete()
> getType()

El primero de ellos nos servirá para inicializar todos los recursos necesarios para el funcionamiento del nuevo content provider. Los cuatro siguientes serán los métodos que permitirán acceder a los datos (consulta, inserción, modificación y eliminación, respectivamente) y por último, el método getType() permitirá conocer el tipo de datos devueltos por el content provider (más tarde intentaremos explicar algo mejor esto último).

Además de implementar estos métodos, también definiremos una serie de constantes dentro de nuestra nueva clase provider, que ayudarán posteriormente a su utilización. Veamos esto paso a paso. Vamos a crear una nueva clase ClientesProvider que extienda de ContentProvider

Lo primero que vamos a definir es la URI con la que se accederá a nuestro content provider. En mi caso he elegido la siguiente:

content://net.sgoliver.android.contentproviders/clientes

Además, para seguir la práctica habitual de todos los content providers de Android, encapsularemos además esta dirección en un objeto estático de tipo Uri llamado CONTENT_URI.


A continuación vamos a definir varias constantes con los nombres de las columnas de los datos proporcionados por nuestro content provider. Como ya dijimos antes existen columnas predefinidas que deben tener todos los content providers, por ejemplo la columna _ID. Estas columnas estándar están definidas en la clase BaseColumns, por lo que para añadir las nuevas columnas de nuestro content provider definiremos una clase interna pública tomando como base la clase BaseColumns y añadiremos nuestras nuevas columnas


Por último, vamos a definir varios atributos privados auxiliares para almacenar el nombre de la base de datos, la versión, y la tabla a la que accederá nuestro content provider.


Como se indicó anteriormente, la primera tarea que nuestro content provider deberá hacer cuando se acceda a él será interpretar la URI utilizada. Para facilitar esta tarea Android proporciona una clase llamada UriMatcher, capaz de interpretar determinados patrones en una URI. Esto nos será útil para determinar por ejemplo si una URI hace referencia a una tabla genérica o a un registro concreto a través de su ID. Por ejemplo:

* content://net.sgoliver.android.contentproviders/clientes –> Acceso genérico a tabla de clientes

* content://net.sgoliver.android.contentproviders/clientes/17 –> Acceso directo al cliente con ID = 17

Para conseguir esto definiremos también como miembro de la clase un objeto UriMatcher y dos nuevas constantes que representen los dos tipos de URI que hemos indicado: acceso genérico a tabla (lo llamaré CLIENTES) o acceso a cliente por ID (lo llamaré CLIENTES_ID). A continuación inicializaremos el objeto UriMatcher indicándole el formato de ambos tipos de URI, de forma que pueda diferenciarlos y devolvernos su tipo (una de las dos constantes definidas, CLIENTES o CLIENTES_ID).


En el código anterior vemos como mediante el método addUri() indicamos el authority de nuestra URI, el formato de la entidad que estamos solicitando, y el tipo con el que queremos identificar dicho formato. Más tarde veremos cómo utilizar esto de forma práctica.

Bien, pues ya tenemos definidos todos los miembros necesarios para nuestro nuevo content provider. Ahora toca implementar los métodos comentados anteriormente.

El primero de ellos es onCreate(). En este método nos limitaremos simplemente a inicializar nuestra base de datos, a través de su nombre y versión, y utilizando para ello la clase ClientesSqliteHelper que creamos al principio del apartado.


La parte interesante llega con el método query(). Este método recibe como parámetros una URI, una lista de nombres de columna, un criterio de selección, una lista de valores para las variables utilizadas en el criterio anterior, y un criterio de ordenación. Todos estos datos son análogos a los que comentamos cuando tratamos la consulta de datos en SQLite para Android, apartado que recomiendo releer si no tenéis muy frescos estos conocimientos. El método query deberá devolver los datos solicitados según la URI indicada y los criterios de selección y ordenación pasados como parámetro. Así, si la URI hace referencia a un cliente concreto por su ID ése deberá ser el único registro devuelto. Si por el contrario es un acceso genérico a la tabla de clientes habrá que realizar la consulta SQL correspondiente a la base de datos respetando los criterios pasados como parámetro.

Para distinguir entre los dos tipos de URI posibles utilizaremos como ya hemos indicado el objeto uriMatcher, utilizando su método match(). Si el tipo devuelto es CLIENTES_ID, es decir, que se trata de un acceso a un cliente concreto, sustituiremos el criterio de selección por uno que acceda a la tabla de clientes sólo por el ID indicado en la URI. Para obtener este ID utilizaremos el método getLastPathSegment() del objeto uri que extrae el último elemento de la URI, en este caso el ID del cliente.

Hecho esto, ya tan sólo queda realizar la consulta a la base de datos mediante el método query() de SQLiteDatabase. Esto es sencillo ya que los parámetros son análogos a los recibidos en el método query() del content provider.


Como vemos, los resultados se devuelven en forma de objeto Cursor, una vez más exactamente igual a como lo hace el método query() de SQLiteDatabase.

Por su parte, los métodos update() y delete() son completamente análogos a éste, con la única diferencia de que devuelven el número de registros afectados en vez de un cursor a los resultados. Vemos directamente el código:


El método insert() sí es algo diferente, aunque igual de sencillo. La diferencia en este caso radica en que debe devolver la URI que hace referencia al nuevo registro insertado. Para ello, obtendremos el nuevo ID del elemento insertado como resultado del método insert() de SQLiteDatabase, y posteriormente construiremos la nueva URI mediante el método auxiliar ContentUris.withAppendedId() que recibe como parámetro la URI de nuestro content provider y el ID del nuevo elemento.


Por último, tan sólo nos queda implementar el método getType(). Este método se utiliza para identificar el tipo de datos que devuelve el content provider. Este tipo de datos se expresará como un MIME Type, al igual que hacen los navegadores web para determinar el tipo de datos que están recibiendo tras una petición a un servidor. Identificar el tipo de datos que devuelve un content provider ayudará por ejemplo a Android a determinar qué aplicaciones son capaces de procesar dichos datos.

Una vez más existirán dos tipos MIME distintos para cada entidad del content provider, uno de ellos destinado a cuando se devuelve una lista de registros como resultado, y otro para cuando se devuelve un registro único concreto. De esta forma, seguiremos los siguientes patrones para definir uno y otro tipo de datos:

> "vnd.android.cursor.item/vnd.xxxxxx" –> Registro único
> "vnd.android.cursor.dir/vnd.xxxxxx" –> Lista de registros

En mi caso de ejemplo, he definido los siguientes tipos:

> "vnd.android.cursor.item/vnd.sgoliver.cliente"
> "vnd.android.cursor.dir/vnd.sgoliver.cliente"

Con esto en cuenta, la implementación del método getType() quedaría como sigue:


Como se puede observar, utilizamos una vez más el objeto UriMatcher para determinar el tipo de URI que se está solicitando y en función de ésta devolvemos un tipo MIME u otro.

Pues bien, con esto ya hemos completado la implementación del nuevo content provider. Pero aún nos queda un paso más, como indicamos al principio del apartado. Debemos declarar el content provider en nuestro fichero AndroidManifest.xml de forma que una vez instalada la aplicación en el dispositivo Android conozca la existencia de dicho recurso. Para ello, bastará insertar un nuevo elemento <provider> dentro de <application> indicando el nombre del content provider y su authority.


Ahora sí hemos completado totalmente la construcción de nuestro nuevo content provider mediante el cual otras aplicaciones del sistema podrán acceder a los datos almacenados por nuestra aplicación.

En el siguiente apartado veremos cómo utilizar este nuevo content provider para acceder a los datos de nuestra aplicación de ejemplo, y también veremos cómo podemos utilizar alguno de los content provider predefinidos por Android para consultar datos del sistema, como por ejemplo la lista de contactos o la lista de llamadas realizadas.


Saludos compañeros, aprovechen la información.









No hay comentarios:

Publicar un comentario en la entrada