120. Proveedor de contenidos¶
Los proveedores de contenido en Android son básicamente una forma de compartir datos entre diferentes aplicaciones. Como un puente que permite a una app acceder a datos de otra app, siempre y cuando tenga los permisos necesarios. Por ejemplo, podrías acceder a las fotos del usuario que están gestionadas por la app de galería.
Ahora, cuando hablamos de Kotlin y Jetpack Compose no cambia la forma en que interactúas con los proveedores de contenido en Android.
120.1 Conceptos básicos de Proveedores de Contenido¶
Un proveedor de contenido presenta datos a aplicaciones externas en forma de una o más tablas que son similares a las tablas de una base de datos relacional. Una fila representa una instancia de algún tipo de datos que recopila el proveedor, y cada columna de la fila representa un ítem individual de los datos recopilados para una instancia.
El proveedor de contenido organiza el acceso a la capa de almacenamiento de los datos en tu aplicación para una serie de API y componentes diferentes e incluye lo siguiente:
- Compartir con otras aplicaciones el acceso a los datos de tu aplicación Enviar datos a un widget
- Mostrar sugerencias personalizadas de búsqueda para tu aplicación mediante el marco de trabajo de búsqueda usando SearchRecentSuggestionsProvider
- Sincronizar los datos de la aplicación con tu servidor mediante una * implementación de AbstractThreadedSyncAdapter
- Cargar datos en tu IU usando CursorLoader
120.2 Acceder a un Proveedor de Contenidos¶
Los clientes se conectan al Proveedor de Contenidos usando el objeto ContentResolver en el Context de la aplicación. ContentResolver
se comunica con una objeto de la clase que implemente el interfaz
ContentProvider que se comunica con el objeto de datos, realiza la acción solicitada y muestra los resultados.
Los métodos que proporciona el ContentResolver
son las funciones básicas "CRUD" (proveniente de los términos en inglés que equivalen a crear, recuperar, actualizar y borrar) del almacenamiento persistente.
Se suele acceder desde el IU al ContentProvider usando unCursorLoader que permite ejecutar una consulta asíncrona en segundo plano.
Un patrón común para acceder a un ContentProvider desde tu IU usa un para ejecutar una consulta asíncrona en segundo plano dejando disponible la IU
Ejemplo.
Uno de los proveedores integrados en la plataforma de Android es el diccionario del usuario, que guarda la ortografía de palabras no estándar que el usuario quiere conservar. En la tabla 1 se detalla cómo se verían los datos en la tabla de este proveedor:
palabra | ID de la app | frecuencia | configuración regional | _ID |
---|---|---|---|---|
mapreduce | user1 | 100 | es-419 | 1 |
precompiler | user14 | 200 | fr_FR | 2 |
applet | user2 | 225 | fr_CA | 3 |
const | user1 | 255 | pt_BR | 4 |
int | user5 | 100 | en_UK | 5 |
Cada fila es el regitro de una palabra del diccionario del usuaro. La columna _ID cumple las funciones de clave primaria.
Si quieres obtener una lista de palabras y sus configuraciones regionales del proveedor de diccionario del usuario, debes llamar a ContentResolver.query()
. El método query() llama al método ContentProvider.query()
definido por el proveedor de diccionario del usuario. Las siguientes líneas de código muestran una llamada ContentResolver.query():
// Queries the user dictionary and returns results
cursor = contentResolver.query(
UserDictionary.Words.CONTENT_URI, // The content URI of the words table
projection, // Las columnas para devolver en la consulta
selectionClause, // Criterio de selección
selectionArgs.toTypedArray(), // Criterio de selección
sortOrder // Orden de las filas devueltas
)
Argumento de query() | Palabra clave/parámetro SELECT | Notas | _ID |
---|---|---|---|
Uri | FROM table_name | Uri se asigna a la tabla del proveedor llamada table_name. | 1 |
projection | col,col,col,... | projection es una matriz de columnas que se debe incluir para cada fila recuperada. | 2 |
selection | WHERE col = value | selection especifica los criterios para seleccionar filas. | 3 |
selectionArgs | (Sin equivalente exacto; los argumentos de selección reemplazan a los marcadores de posición ? en la cláusula de selección). | 255 | 4 |
sortOrder | ORDER BY col,col,... | sortOrder especifica el orden en que aparece la fila en el Cursor que se muestra. | 5 |
En los siguientes apartados se describe como obtener los datos del diccionario de usuario usando el Proveedor de Contenidos
120.2.1 URI Del Content Provider¶
Un URI de contenido es un URI que identifica datos de un proveedor. Los URI de contenido incluyen el nombre simbólico de todo el proveedor (su autoridad) y un nombre que apunta a una tabla (una ruta de acceso). Cuando llamas al método del cliente para acceder a una tabla del proveedor, el URI de contenido de la tabla es uno de los argumentos.
En el ejemplo anterior la constante UserDictionary.Words.CONTENT_URI
representa:
content://user_dictionary/words
120.2.2 Solicitar Permisos de acceso de lectura¶
Si se va a acceder a datos que requieren permisos (como contactos o fotos), hay que asegurarse de solicitar esos permisos en tu AndroidManifest.xml y en tiempo de ejecución.
El proveedor de diccionario del usuario define el permiso android.permission.READ_USER_DICTIONARY
en su archivo de manifiesto, de modo que una aplicación que quiera leer desde el proveedor debe solicitar este permiso.
NOTA IMPORTANTE:
A partir de la versión 23 de la API de Android (Android 6.0 Marshmallow), hubo un cambio importante en cómo las aplicaciones pueden acceder al User Dictionary
Accesible Solo a través de IME y Corrector Ortográfico: IME significa "Input Method Editor" (Editor de Método de Entrada). Esto se refiere a las aplicaciones que proporcionan una forma de ingresar texto, como los teclados en pantalla. La nota indica que solo las aplicaciones que son IME (como los teclados) o los correctores ortográficos tienen permiso para acceder al User Dictionary de manera directa.
Implicaciones para los Desarrolladores: Si estás desarrollando una aplicación que no es un IME o un corrector ortográfico, no podrás acceder directamente al User Dictionary para leer o modificarlo a partir de la API 23.
<uses-permission android:name="android.permission.READ_USER_DICTIONARY"/>
120.2.3 Construir la consulta¶
Para poder usar el ContentResolver
creamos todos los argumentos necesarios para la consulta.
Buscamos las referencias necesarias para usar el diccionario de usuario en Android Developer Referencia UserDictionary
Projection
Array con las columnas de la selección :
// A "projection" defines the columns that will be returned for each row
private val mProjection: Array<String> = arrayOf(
UserDictionary.Words._ID, // Contract class constant for the _ID column name
UserDictionary.Words.WORD, // Contract class constant for the word column name
UserDictionary.Words.LOCALE // Contract class constant for the locale column name
)
// Defines a string to contain the selection clause
private var selectionClause: String? = null
// Declares an array to contain selection arguments
private lateinit var selectionArgs: Array<String>
La consulta es equivalente a la siguiente sentencia SQL:
SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;
ContentResolver.query()
que necesita:- Proyección (array de columnas ) anterior
- Expresión de busqueda formado por
- clausula de seleccion , expresión lógica con nombres de columnas valores (la variable mSelectionClause) Si especificas el parámetro reemplazable
?
en lugar de un valor, el método de consulta recupera el valor de la matriz de argumentos de selección (la variable mSelectionArgs). - argumentos de selección
- Usamos el Cursor para leer la respuesta. Tendremos en cuenta que en algunos casos el contenido puede ser
null
/*
* This declares String array to contain the selection arguments.
*/
private lateinit var selectionArgs: Array<String>
// suponemos que searchWord contiene la palabra que buscamos en
searchString = "electroencefalografista"
// Remember to insert code here to check for invalid or malicious input.
// Si la palabra es nula se obtienen todas
// Observar que inicializamos selectionArgs y selectionClause
// UserDictionary.Words.WORD es una cte
selectionArgs = searchString?.takeIf { it.isNotEmpty() }?.let {
selectionClause = "${UserDictionary.Words.WORD} = ?"
arrayOf(it)
} ?: run {
selectionClause = null
emptyArray<String>()
}
// Realiza la llavalmada y se devuelve un Cursor
val mCursor = contexto.contentResolver.query(
UserDictionary.Words.CONTENT_URI, // The content URI of the words table
mProjection, // The columns to return for each row
selectionClause, // null o la palabra buscada
selectionArgs, // Either empty, or the string the user entered
sortOrder // The sort order for the returned rows
)
// Some providers return null if an error occurs, others throw an exception
// con el comentario anterior , deberíamos capturar las excepciones en la
// sentencia anterior
when (mCursor?.count) {
null -> {
/*
* Insert code here to handle the error. Be sure not to use the cursor!
* You may want to call android.util.Log.e() to log this error.
*
*/
}
0 -> {
/*
* Insert code here to notify the user that the search was unsuccessful. This isn't
* necessarily an error. You may want to offer the user the option to insert a new
* row, or re-type the search term.
*/
}
else -> {
// Insert code here to do something with the results
}
}
120.2.4 Insertar , actualizar y borrar datos¶
120.2.4.1 Insertar¶
Para insertar datos en un proveedor, debes llamar al método ContentResolver.insert(). Este método inserta una nueva fila en el proveedor y devuelve un URI de contenido para esa fila. Este fragmento de código muestra cómo insertar una nueva palabra en el proveedor de diccionario del usuario:
// Defines a new Uri object that receives the result of the insertion
lateinit var newUri: Uri
...
// Defines an object to contain the new values to insert
val newValues = ContentValues().apply {
/*
* Sets the values of each column and inserts the word. The arguments to the "put"
* method are "column name" and "value"
*/
put(UserDictionary.Words.APP_ID, "example.user")
put(UserDictionary.Words.LOCALE, "en_US")
put(UserDictionary.Words.WORD, "insert")
put(UserDictionary.Words.FREQUENCY, "100")
}
newUri = contentResolver.insert(
UserDictionary.Words.CONTENT_URI, // the user dictionary content URI
newValues // the values to insert
)
El URI de contenido que se muestra en newUri identifica la fila nueva con el siguiente formato:
content://user_dictionary/words/<id_value>
Para obtener el valor de _ID del Uri que se muestra, llama a ContentUris.parseId().
120.2.4.2 Actualizar datos¶
Para actualizar una fila, debes usar un objeto ContentValues
con los valores actualizados, tal como lo haces con una inserción, y con criterios de selección como lo haces con una consulta. El método de cliente que usas es ContentResolver.update()
. Solo debes agregar valores al objeto ContentValues para las columnas que estés actualizando. Si quieres borrar el contenido de una columna, establece el valor en null.
// Defines an object to contain the updated values
val updateValues = ContentValues().apply {
/*
* Sets the updated value and updates the selected words.
*/
putNull(UserDictionary.Words.LOCALE)
}
// Defines selection criteria for the rows you want to update
val selectionClause: String = UserDictionary.Words.LOCALE + "LIKE ?"
val selectionArgs: Array<String> = arrayOf("en_%")
// Defines a variable to contain the number of updated rows
var rowsUpdated: Int = 0
...
rowsUpdated = contentResolver.update(
UserDictionary.Words.CONTENT_URI, // the user dictionary content URI
updateValues, // the columns to update
selectionClause, // the column to select on
selectionArgs // the value to compare to
)
120.2.4.3 BORRAR DATOS¶
Borrar filas es similar a recuperar datos de una fila: debes especificar criterios de selección para las filas que quieras borrar y el método de cliente muestra el número de filas borradas. El siguiente fragmento borra las filas cuyo ID de aplicación coincide con "user". El método muestra el número de filas borradas.
// Defines selection criteria for the rows you want to delete
val selectionClause = "${UserDictionary.Words.LOCALE} LIKE ?"
val selectionArgs: Array<String> = arrayOf("user")
// Defines a variable to contain the number of rows deleted
var rowsDeleted: Int = 0
...
// Deletes the words that match the selection criteria
rowsDeleted = contentResolver.delete(
UserDictionary.Words.CONTENT_URI, // the user dictionary content URI
selectionClause, // the column to select on
selectionArgs // the value to compare to
)
Cuando llamas a ContentResolver.delete()
, también debes depurar las entradas del usuario. Para obtener más información, consulta la sección Protección contra entradas malintencionadas.
120.2.5 Tipo de datos del proveedor¶
Los proveedores de contenido pueden ofrecer muchos tipos de datos diferentes. El proveedor de diccionario del usuario solo ofrece texto, pero en general los proveedores también pueden ofrecer los siguientes formatos:
- número entero
- entero largo (largo)
- punto flotante
- punto flotante largo (doble)
- Otro tipo de datos que los proveedores usan con frecuencia es el objeto binario grande (
BLOB
) implementado como una matriz de 64 KB. Si quieres ver los tipos de datos disponibles, puedes ver los métodos GET de la clase Cursor.
El tipo de datos para cada columna de un proveedor suele indicarse en su documentación. Los tipos de datos para el proveedor de diccionario del usuario se indican en la documentación de referencia para su clase de contratos UserDictionary.Words (las clases de contratos se describen en la sección Clases de contratos). También puedes determinar el tipo de datos llamando a Cursor.getType().
Los proveedores también mantienen información del tipo de datos MIME
para cada URI de contenido que definen. Usa la información del tipo de MIME para averiguar si tu aplicación puede administrar datos que ofrece el proveedor o seleccionar un tipo de administración en función del tipo de MIME. Generalmente necesitas el tipo de MIME cuando trabajas con un proveedor que contiene estructuras de datos o archivos complejos. Por ejemplo, la tabla ContactsContract.Data del proveedor de contactos usa tipos de MIME para etiquetar el tipo de datos de contacto guardado en cada fila. Para obtener el tipo de MIME correspondiente a un URI de contenido, llama a ContentResolver.getType().
La sección Referencia a tipos de MIME describe la sintaxis de los tipos de MIME estándar y personalizado.
120.3 Como crear un Proveedor de contenidos¶
https://developer.android.com/guide/topics/providers/content-provider-creating?hl=es-419
120.4 Pasos para trabajar con Proveedores de contenido:¶
-
Definir un Uri: Este es un identificador único que apunta a los datos que quieres en el proveedor de contenido.
-
Solicitar Permisos: Si se va a acceder a datos que requieren permisos (como contactos o fotos), hay que asegurarse de solicitar esos permisos en tu AndroidManifest.xml y en tiempo de ejecución.
-
Usar ContentResolver: En Kotlin, utilizarás un ContentResolver para consultar, insertar, actualizar o eliminar datos en un proveedor de contenido. El ContentResolver gestiona tu solicitud y comunica con el proveedor de contenido apropiado.
-
Integrar con Compose: Aunque la interacción con el proveedor de contenido se hace en Kotlin, puedes fácilmente integrar los datos obtenidos en tu UI Compose. Por ejemplo, puedes tener un @Composable que muestra una lista de contactos obtenidos de un proveedor de contenido.
-
Manejo de Datos: Trabaja con los datos obtenidos (Cursor) y adáptalos según sea necesario para mostrarlos en tu UI Compose.
120.5 Ejemplo Proveedor de Libros:¶
Vamos a crear un módulo que lleva el control de los libros de una biblioteca. Queremos que otras aplicaciones puedan acceder a esta información, pero de manera controlada y segura. Aquí es donde entra en juego el proveedor de contenidos
- Creamos el proveedor de contenidos en nuestra aplicación
class LibroProvider : ContentProvider() {
override fun onCreate(): Boolean {
// Inicializar el proveedor
return true
}
// Implementar los métodos query, insert, delete, y update
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? {
// Lógica para consultar los libros
return null
}
// Los otros métodos se implementan de forma similar
}
- Registramos el proveedor de contenidos en
AndroidManifest.xml
<provider
android:name=".LibroProvider"
android:authorities="com.miapp.biblioteca"
android:exported="true"/>
- Acceder al Proveedor de Contenidos Desde tu código Kotlin (por ejemplo, en tu ViewModel o en alguna función de tu UI Compose), puedes acceder al proveedor de contenidos para obtener los datos.
val uri = Uri.parse("content://com.miapp.biblioteca/books")
val cursor = context.contentResolver.query(uri, null, null, null, null)
// Convertir el cursor a una lista de libros y actualizar la UI
120.6 Apendice¶
Enlaces
- Conceptos básicos Content Provider
- Proveedor de calendario
- Proveedor de contactos
- Revisar tutorial lista de contactos
- REVISAR
- https://github.com/mtuenaydin/ContactViewModel/tree/master
Versión 0.5 18-12-23 Versión 0.8 7-1-24