-
Notifications
You must be signed in to change notification settings - Fork 3
Content provider
Un content provider es exactamente lo que su nombre dice, un proveedor de contenido o de datos. Ofrece un esquema estructurado para acceder, crear, borrar o actualizar los datos que se pongan a disposición a través de él. En definitiva es la forma en que tu aplicación ofrece sus datos para que puedan ser consumidos o editados por procesos externos a ella o por otras aplicaciones sin la necesidad de que esta esté abierta. Se pueden compartir los datos desde bases de datos o archivos, pero en este caso utilizaremos la primera.
Se pueden usar incluso como una forma de abstraer la implementación de un sistema de datos complejo frente al resto de los desarrolladores de una misma aplicación. De esta forma el resto de los integrantes del equipo solo deben estar al tanto de los estándares de comunicación con el content provider y no necesitan preocuparse de detalles de la implementación. De todas formas su principal objetivo es compartir los datos con otros procesos.
En esta sección cubriremos los siguientes temas:
-
Funcionamiento general
-
Content URIs
-
Clase Contrato
-
Clase Content Provider
-
Cómo utilizar el Content Provider
-
Permisos
Como se aprecia en la figura el flujo
parte desde tu aplicación obteniendo una instancia de
ContentResolver
[^2]. Luego, a través de esta se hace una consulta
pasando como primer parámetro una URI que hace referencia a un conjunto
de datos que ofrece un content provider específico. Es importante saber
que pueden haber muchos content providers disponibles, cada uno con su
conjunto de URIs (corresponde un URI para cada conjunto de datos
ofrecido en el provider). Por ejemplo, android provee Contacts
Provider[^3] y Calendar Provider[^4]. En la próxima sección se entrará
más en detalles en el diseño de las URIs.
Nuestra instancia de ContentResolver
decide a qué provider hacer la
consulta basado en la URI. Luego el content provider determina a qué
conjunto de datos debe afectar (también basado en la URI) e interactúa
con la base de datos para realizar la operación que fue invocada.
Un content URI es un identificador de un conjunto de datos en un
provider. Está formado por el identificador del provider (authority) y
por un nombre que indica a la tabla o al contenido que hace referencia.
Cuando se llama a un método para acceder a algún conjunto de datos se
debe incluir en los parámetros una URI determinada. Esto para que el
ContentResolver
pueda determinar a qué provider le corresponde manejar
el llamado, y para que el provider pueda determinar sobre que datos
realizar la operación.
La authority es el nombre de tu provider. Por lo general el formato de este viene dado por el package de la aplicación más ’provider’. Por ejemplo, en el caso de nuestra aplicación es com.example.andres.myapplication.provider. En el package (y en este caso, en la authority) se suele utilizar como primeras palabras un dominio de internet, al revés, al cual pertenece la aplicación. En este caso asumimos que hay un dominio con la ruta andres.example.com.
Para el content provider se puede diseñar una estructura nueva de los
datos de acuerdo a lo que se quiera ofrecer. En la clase contrato
definiremos nuevas tablas (que pueden ser iguales a las de tu base de
datos o no) que conformarán la estructura de rutas. En nuestro caso solo
definiremos una tabla Students
igual a la de nuestra base de datos.
Claramente se pueden hacer estructuras más complejas con tablas anidadas
y más esoterismos, pero no se cubrirán en este tutorial[^5].
Para nuestra aplicación definiremos dos URIs, una para obtener la lista completa de estudiantes y otra para obtener a un estudiante en particular basándonos en su ID. La primera es exactamente como la que está en la Figura [fig:estructura_content_URI]. La segunda debe poder soportar llamados del tipo
content://com.example.andres.myapplication.provider/students/3,
donde 3 es el id del estudiante a seleccionar. Para poder soportar esto debemos definir la URI de la siguiente manera:
content://com.example.andres.myapplication.provider/students/#
La clase contrato es la que contiene todos los URIs, MIME Types y constantes necesarias para poder tener un protocolo de comunicación fijo entre los distintos procesos que utilizan el provider. Esta clase es la que debes compartir con otros desarrolladores si quieres que usen tu provider, ya que con ella les proveerás todos las valores necesarios para que se puedan comunicar con él de manera correcta. Básicamente, en la aplicación que utilice el provider se debe copiar y pegar la clase contrato.
A continuación se presenta el código de la clase contrato de nuestra aplicación de ejemplo. Los conceptos involucrados se explicarán luego.
import android.content.ContentResolver;
import android.net.Uri;
import android.provider.BaseColumns;
/**
* Esta clase provee las constantes y URIs necesarias para trabajar con el StudentsProvider
*/
public final class StudentsContract {
public static final String AUTHORITY = "com.example.andres.myapplication.provider";
public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
public static final Uri STUDENTS_URI = Uri.withAppendedPath(StudentsContract.BASE_URI, "/students");
/*
MIME Types
Para listas se necesita 'vnd.android.cursor.dir/vnd.com.example.andres.provider.students
Para items se necesita 'vnd.android.cursor.item/vnd.com.example.andres.provider.students'
La primera parte viene esta definida en constantes de ContentResolver
*/
public static final String URI_TYPE_STUDENT_DIR = ContentResolver.CURSOR_DIR_BASE_TYPE +
"/vnd.com.example.andres.provider.students";
public static final String URI_TYPE_STUDENT_ITEM = ContentResolver.CURSOR_ITEM_BASE_TYPE +
"/vnd.com.example.andres.provider.students";
/*
Tabla definida en provider. Aca podria ser una distinta a la de la base de datos,
pero consideramos la misma.
*/
public static final class StudentsColumns implements BaseColumns{
private StudentsColumns(){}
public static final String NAMES = "names";
public static final String FIRST_LASTNAME = "firstlastname";
public static final String SECOND_LASTNAME = "secondlastname";
public static final String ID_CLOUD = "idcloud";
public static final String DEFAULT_SORT_ORDER = FIRST_LASTNAME + " ASC";
}
}
De lo que está presente en la clase contrato y no se ha hablado es de los MIME Types. Estos conforman una manera estandar de clasificar los tipos de archivos o datos. Estos tipos son los mismos siempre, independiente del sistema operativo o de cualquier otra variante que se presente. Un MIME Type tiene dos partes: un tipo y un sub-tipo que están separados por un slash (/). Por ejemplo, las imágenes de formato JPEG tienen el MIME Type image/jpeg.
En el caso del provider se especifican dos MIME Types principales. El primero es para items individuales, donde el tipo viene dado por vnd.android.cursor.item y el subtipo (para nuestro provider) por /vnd.com.example.andres.provider.students. El otro es para listas, donde el tipo viene dado por vnd.android.cursor.dir y el subtipo (para nuestro provider) por /vnd.com.example.andres.provider.students (igual que para el de items individuales). De esta forma los MIME Types completos quedan como se especifica en el código.
Los MIME Types son importantes debido a que con ellas el desarrollador puede determinar el tipo de dato que se le retornará si utiliza una URI determinada en una consulta. No hay que olvidar que esta clase es principalmente un apoyo para que otros puedan utilizar los datos que les provees, por lo tanto se debe ser consistente y claro en la implementación.
A continuación se creará una clase StudentProvider
que herede de la
clase abstracta ContentProvider
. Esta obliga a implementar una serie de
métodos especificados en el código que se mostrará a continuación. Por
ejemplo, se pide implementar el método query. Este método tiene el
mismo nombre que el que se usa desde ContentResolver. Es decir, si
llamamos a ContentResolver.query()
, el content resolver determinará a
qué provider llamar basado en la URI y llamará al método query()
de ese
provider.
import android.content.ContentProvider;
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;
/*
Clase que extiende ContentProvider y que interactua con la base de datos
*/
public class StudentsProvider extends ContentProvider {
public static final int STUDENT_LIST = 1;
public static final int STUDENT_ID = 2;
private static final UriMatcher sUriMatcher;
static{
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
/*
URI para todos los estudiantes.
Se setea que cuando se pregunta a UriMatcher por la URI:
content://com.example.andres.myapplication.provider/students
se devuelva un entero con el valor de 1.
*/
sUriMatcher.addURI(StudentsContract.AUTHORITY, "students", STUDENT_LIST);
/*
URI para un estudiante.
Se setea que cuando se pregunta a UriMatcher por la URI:
content://com.example.andres.myapplication.provider/students/#
se devuelva un entero con el valor de 2.
*/
sUriMatcher.addURI(StudentsContract.AUTHORITY, "students/#", STUDENT_ID);
}
/*
Instancia de StudentsDbHelper para interactuar con la base de datos
*/
private StudentsDbHelper mDbHelper;
public StudentsProvider() { }
@Override
public boolean onCreate() {
mDbHelper = StudentsDbHelper.getInstance(getContext());
return true;
}
/*
Llamado para borrar una o mas filas de una tabla
*/
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = mDbHelper.getWritableDatabase();
int rows = 0;
switch (sUriMatcher.match(uri)) {
case STUDENT_LIST:
// Se borran todas las filas
rows = db.delete(DatabaseContract.Students.TABLE_NAME, null, null);
break;
case STUDENT_ID:
// Se borra la fila del ID seleccionado
rows = db.delete(DatabaseContract.Students.TABLE_NAME, selection, selectionArgs);
}
// Se retorna el numero de filas eliminadas
return rows;
}
/*
Se determina el MIME Type del dato o conjunto de datos al que apunta la URI
*/
@Override
public String getType(Uri uri) {
switch (sUriMatcher.match(uri)){
case STUDENT_LIST:
return StudentsContract.URI_TYPE_STUDENT_DIR;
case STUDENT_ID:
return StudentsContract.URI_TYPE_STUDENT_ITEM;
default:
return null;
}
}
/*
Inserta nuevo estudiante
*/
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = mDbHelper.getWritableDatabase();
db.insert(DatabaseContract.Students.TABLE_NAME, null, values);
// Le avisa a los observadores
getContext().getContentResolver().notifyChange(uri, null);
return null;
}
/*
Retorna el o los datos que se le pida de acuerdo a la URI
*/
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = mDbHelper.getReadableDatabase();
switch (sUriMatcher.match(uri)){
// Se pide la lista completa de estudiantes
case STUDENT_LIST:
// Si no hay un orden especificado,
// lo ordenamos de manera ascendente de acuerdo a lo que diga el contrato
if (sortOrder == null || TextUtils.isEmpty(sortOrder))
sortOrder = StudentsContract.StudentsColumns.DEFAULT_SORT_ORDER;
break;
// Se pide un estudiante en particular
case STUDENT_ID:
// Se adjunta la ID del estudiante selecciondo en el filtro de la seleccion
if (selection == null)
selection = "";
selection = selection + "_ID = " + uri.getLastPathSegment();
break;
// La URI que se recibe no esta definida
default:
throw new IllegalArgumentException(
"Unsupported URI: " + uri);
}
Cursor cursor = db.query(DatabaseContract.Students.TABLE_NAME,
projection,
selection,
selectionArgs,
null,
null,
sortOrder);
// Se retorna un cursor sobre el cual se debe iterar para obtener los datos
return cursor;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// No se implemento un update
throw new UnsupportedOperationException("Not yet implemented");
}
}
Una clase importante que se utiliza en StudentProvider es UriMatcher. Esta clase se encarga de ayudarte a determinar que acción seguir para cada URI definida. Esto lo logra asociando cada URI a un entero que idealmente debes definir como constante. Además permite definir URIs genéricas, como se ve en el código para la elección de estudiantes por su ID.
Una vez que se tiene todo lo anterior definido correctamente podemos
hacer uso de nuestro ContentProvider
. Para probar si funciona
correctamente hay dos opciones: probarlo simplemente en tu aplicación o
crear otra aplicación de prueba que lo utilice. Ahora el uso será muy
sencillo, por ejemplo para seleccionar todos los estudiantes se debe
hacer lo siguiente:
Cursor c = mContentResolver.query(StudentsContract.STUDENTS_URI, null, null, null, null);
c.moveToFirst();
while (c.moveToNext()){
String names = c.getString(c.getColumnIndexOrThrow(StudentsContract.StudentsColumns.NAMES));
String firstLast = c.getString(c.getColumnIndexOrThrow(StudentsContract.StudentsColumns.FIRST_LASTNAME));
String secondLast = c.getString(c.getColumnIndexOrThrow(StudentsContract.StudentsColumns.SECOND_LASTNAME));
// Hacer lo que se necesite con los datos
}
c.close();
Para seleccionar un estudiante cuyo ID = 1
se podría hacer lo siguiente:
Cursor c = mContentResolver.query(Uri.withAppendedPath(StudentsContract.STUDENTS_URI, "1"), null, null, null, null);
c.moveToFirst();
String names = c.getString(c.getColumnIndexOrThrow(StudentsContract.StudentsColumns.NAMES));
String firstLast = c.getString(c.getColumnIndexOrThrow(StudentsContract.StudentsColumns.FIRST_LASTNAME));
String secondLast = c.getString(c.getColumnIndexOrThrow(StudentsContract.StudentsColumns.SECOND_LASTNAME));
// Hacer lo que se necesite con los datos
c.close();
Se deben setear ciertos permisos en el archivo AndroidManifest.xml
presente en toda aplicación android. Para nuestra aplicación agregaremos
solo lo fundamental, que es lo siguiente:´
...
<provider
android:name=".Provider.StudentsProvider"
android:authorities="com.example.andres.myapplication.provider"
android:enabled="true" // Permite al sistema iniciar el provider
android:exported="true" // Permite que otras apps usen el provider
android:syncable="true"> // Indica que los datos del provider son sincronizados con la nube
</provider>
...
Siguiente Authenticator.
Para más información sobre tipos de permisos más complejos se recomienda visitar http://developer.android.com/guide/topics/providers/content-provider-creating.html#Permissions.