Tutorial Android: Crear Un Content Provider Personalizado

¿Aún no sabes crear Content Providers personalizados en android? ¿Deseas que otras aplicaciones accedan a los datos de la tuya?, si es así, este tutorial es para ti.

Aprende a crear un content provider y añade la estructura de datos que desees.

Este artículo te ayudará a implementar un Content Provider que envuelva la base de datos de tu aplicación para su uso externo o Local.

Veremos cómo crear los métodos de consulta de datos, inserción, modificación y eliminación a través de un Content Resolver.

Aprenderemos a establecer tipos MIME para tus datos, crear URIs de contenido, generar los permisos necesarios de acceso.

Incluso veremos porque es posible usar un Content Provider como fachada para la construcción de modelos vista controlador de Red. Además usaremos la clase CursorLoader para cargar los datos de una base de datos sobre una lista en segundo plano.

Descargar Proyecto Android Studio «My Techs»

Para guiarte por todo el temario, he creado un ejemplo llamado «My Techs».

Esta aplicación de ejemplo muestra como envolver una base de datos de los técnicos de una empresa con un content provider y cómo es posible cargar los datos en segundo plano sobre un ListView.

Para desbloquear el link de descarga del código completo, sigue estas instrucciones:

[sociallocker id=»7121″]Descargar Gratis[/sociallocker]

¿Qué Es Un Content Provider En Android?

El concepto de Content Provider ya lo habíamos visto antes en los artículos sobre componentes de una aplicación Android y la Content Provider: Diagrama De La Arquitectura

En otras palabras, es una clase que encapsula tu implementación SQLite a través de un mecanismo de códigos secretos (URIs) para que otras aplicaciones no sepan nada sobre su estructura. Sin embargo se proporcionan las modalidades necesarias para consultar, insertar, eliminar y actualizar los datos.

Obviamente esto requiere la creación de un permiso personalizado que debe ser incluido en la aplicación externa. Esa sería la única forma de brindar autorización al acceso de los datos.

Sin embargo el Content Provider no actúa solo. Este requiere de otro componente mediador entre procesos llamado Content Resolver…

Funciones de un Content Resolver

Este componente es el encargado de acceder a los content providers que se encuentran en las aplicaciones de tu dispositivo Android. Su trabajo es revisar la URI de contenido que se le brinda. Con ese dato realiza una búsqueda para llegar hasta la base de datos especificada.

Por esa razón siempre usarás un Content Resolver para consultar, insertar, modificar y eliminar los datos de un Content Provider.

Desarrollo De La Aplicación Android My Techs

My Techs es una aplicación sencilla con una vista de lista que contiene los registros de cada una de las actividades asignadas a los técnicos de una empresa de telecomunicaciones.

La idea es crear un Content Provider que envuelva la peticiones HTTP y la sincronización, este artículo cumple la misión de mostrarte como un Content Provider puede beneficiarnos en la carga de datos en segundo plano y la actualización de la interfaz en tiempo real.

Para crear el Content Provider personalizado realizaremos las siguientes tareas:

  1. Crear una Api pública (clase Contract) para el consumo de datos
  2. Implementar los métodos del content provider
  3. Registrar el content provider en el Android Manifest

Una vez esté lista nuestro andamiaje, veremos que propósito tiene la clase CursorLoader y cómo puede ayudarte operar datos en segundo plano.

Diseño de la base de datos

La base de datos establecida simplemente tiene una tabla llamada actividad. Esta contiene solo seis columnas:

  • _id: Es el identificador de cada actividad
  • descripción: Breve descripción que especifica la acción a realizar
  • categoría: Es la etiqueta que emplea la empresa para clasificar los diferentes tipos de actividad
  • estado: Representa el curso actual de la actividad. Pueden ser los siguientes valores: “Cerrada”, “En Curso”, “Abierta”.
  • tecnico: Funcionario al que se asignará la actividad.

Con este diseño conceptual preliminar estamos reduciendo lo que podría ser una base de datos más elaborada. Esto con el fin de simplificar el artículo para una comprensión general.

Si te fijas, es necesario que exista una tabla Tecnico para la relación directa, sin embargo la evitaremos para mantener corto el ejercicio.

Crear Content Provider Personalizado

Tarea #1: Crear API pública para el consumo de datos

En primera instancia crea un nuevo proyecto en Android Studio llamado “My Techs”. Luego crea dos paquetes Java llamados «ui» y «modelo». El primero contendrá todas nuestras actividades y fragmentos; y el segundo las clases asociadas a la creación del Content Provider.

En un inicio debe contener una actividad con fragmento en su interior. Por lo que puedes seleccionar “Blank Activity with Fragment” .

Android Studio: Crear Nueva Actividad Con Fragmento

El siguiente paso es declarar las constantes necesarias para la publicación de la URI de contenido de nuestro Content Provider. Esto posibilitará a la aplicación cliente para que obtenga los datos de forma estructurada y estandarizada.

Básicamente crearemos una clase tipo Contract como se hace en la escritura de implementaciones SQLite. Como su nombre lo indica, es un tipo de contrato que acuerda entre el Content Provider y las aplicaciones externas, como se accederá a la información.

Crea una nueva clase en Android Studio y nómbrala “TechsContract”. Su objetivo es contener el nombre de las tablas, los nombres de las columnas, las URI de contenido y los tipos MIME de cada dato.

/**
 * Contract Class entre el provider y las aplicaciones
 */
public class TechsContract {
    
}

Paso #1: Define las URI de contenido del Content Provider

Como ya hemos visto en artículos pasados, una URI de contenido representa una dirección lógica e intuitiva para que las aplicaciones cliente puedan acceder a los datos de un Content Provider.

Esto permite crear estructuras que faciliten la obtención de datos a través de patrones específicos.

En Android una URI de contenido debe construirse con las siguientes partes:

  • Esquema “content://”: Cadena que determina el esquema usado para indicarle al framework de Android que la URI se refiere a un content provider.
  • Autoridad: Representa la identificación única del Content Provider sobre otros. Se usa el nombre del paquete donde se encuentra el componente para asegurar unicidad.
  • Ruta: Sección que especifica los datos que se desean obtener. Para ello se usarán barras oblicuas ‘/’ que guíen al Content Resolver a la información. Dependiendo del patrón podremos obtener todos los registros de una tabla específica, obtener un solo registro, filtrar por características, etc. Normalmente la primera subdivisión es el nombre de la tabla y la siguiente es el identificador de un registro específico.
  • Identificador: Sección que contiene un número único que corresponde a un registro.

Por ejemplo…

La URI de contenido de nuestro Content Provider para obtener las actividades será:

content://com.herprogramacion.mytechs.modelo.TechsProvider/actividad

Ahora, si deseas la actividad con el id 3, la URI se vería así:

content://com.herprogramacion.mytechs.modelo.TechsProvider/actividad/3

Con esto en mente, declara una constante para la autoridad y otra la uri de contenido:

/**
 * Autoridad del Content Provider
 */
public final static String AUTHORITY = "com.herprogramacion.mytechs.modelo.TechsProvider";
/**
 * URI de contenido principal
 */
public final static Uri CONTENT_URI =
        Uri.parse("content://" + AUTHORITY + "/" + ACTIVIDAD);

Comparar patrones de contenido con UriMatcher

La clase UriMatcher es un asistente que nos ayuda a distinguir entre una URI de contenido que retorna múltiples filas y aquellas que retornan una sola fila. Esto permite tomar decisiones (frecuentemente con un switch) para ejecutar las consultas correctamente o retornar tipos MIME adecuados.

Básicamente UriMatcher asigna un identificador numérico a cada patrón que posea el Content Provider, a través de los siguientes wildcards:

  • *: Representa a todas las cadenas con cualquier tipo de caracteres y cualquier tamaño.
  • #: Solo representa a las cadenas que tengan caracteres numéricos y cualquier tamaño.

Un buen ejemplo del uso de estos wildcards sería…

Si quisieras referirte a todas las URIs de contenido del Content Provider se usa la siguiente expresión:

content://com.herprogramacion.mytechs.modelo.TechsProvider/*

Si tuviéramos mas tablas, esta expresión sería como decir: «Todas las uris de contenido que tu provider tenga»

Ahora, si desearas referirte solo a los registros por identificador podría escribir:

content://com.herprogramacion.mytechs.modelo.TechsProvider/actividad/#

Esta vez se entendería como: «Todas las uris de contenido con identificador numérico asociadas a la tabla actividad»

Para nuestro caso simplemente necesitamos distinguir entre una consulta de múltiples registros de actividades o una consulta de registros individuales. Esto quiere decir que solo usaremos dos identificadores:

/**
 * Código para URIs de multiples registros
 */
public static final int ALLROWS = 1;
/**
 * Código para URIS de un solo registro
 */
public static final int SINGLE_ROW = 2;

El siguiente paso es añadir los patrones con el método addUri() dentro de nuestro UriMatcher.

/**
 * Comparador de URIs de contenido
 */
public static final UriMatcher uriMatcher;

// Asignación de URIs
static {
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    uriMatcher.addURI(AUTHORITY, ACTIVIDAD, ALLROWS);
    uriMatcher.addURI(AUTHORITY, ACTIVIDAD + "/#", SINGLE_ROW);
}

El método addUri() recibe los siguientes parámetros:

  • authority: La autoridad del Content Provider a comparar.
  • path: La ruta a comparar. Aquí se añaden los wildcards necesarios.
  • code: El código o identificador que distingue la URI de contenido.

El primer caso representa la URI de contenido principal, donde el cliente consulta múltiples filas. El segundo caso representa el conjunto de todas las URIs que consulten una actividad por su identificador.

En los próximos apartados veremos cómo usar el UriMatcher para tomar algunas decisiones.

Paso #2: Definir los nombres de las columnas

Debido a que usaremos una base de datos SQLite como estructura de almacenamiento, es necesario proveer los nombres de las columnas que el Content Provider usará para la gestión de los datos.

No olvides incluir el nombre estándar de las llaves primarias "_ID" para que el framework de Android lea correctamente nuestros cursores.

Veamos que tal queda:

/**
 * Representación de la tabla a consultar
 */
public static final String ACTIVIDAD = "actividad";

/**
 * Estructura de la tabla
 */
public static class Columnas implements BaseColumns {

    private Columnas() {
        // Sin instancias
    }

    /**
     * Categoría de la actividad
     */
    public final static String CATEGORIA = "categoria";
    /**
     * Descripción de la actividad
     */
    public final static String DESCRIPCION = "descripcion";
    /**
     * Técnico asignado a la actividad
     */
    public final static String TECNICO = "tecnico";
    /**
     * Estado en que se encuentra la actividad
     */
    public final static String ESTADO = "estado";
    /**
     * Prioridad de realización de la actividad
     */
    public final static String PRIORIDAD = "prioridad";

}

Paso #3: Definir los tipos MIME

Un tipo MIME es un identificador de formato estándar, que representa la estructura de un flujo de datos que será transmitido. Su uso es indispensable en las comunicaciones de la web.

Para usar este tipo de referencias se usa un tipo y un subtipo.

tipo/subtipo

Por ejemplo…

Si deseas enviar un flujo de datos con formato Json, la cabecera Content-Type de la petición HTTP debe asignársele el tipo MIME “application/json”.

Para nuestros Content Providers que usen tablas usaremos el formato MIME "vendor-specific". Este permite la obtención de datos a través de strings personalizadas implementando un tipo, subtipo y una parte específica para el Content Provider.

Tipo: Se usa la cadena "vnd" antepuesta al tipo.

Subtipo: En este caso el tipo del formato MIME definido para el framework de Android depende del número de elementos a retornar. Si son múltiples elementos, entonces se usa la siguiente señal:

vnd.android.cursor.dir

Si es un solo ítem, entonces usas:

vnd.android.cursor.item

Parte Provider: Esta sección del formato MIME depende de la forma de tu autoridad. Donde se usará el prefijo "vnd", el nombre del paquete y el tipo es una cadena que distinga tu tabla:

vnd.<nombre>.<tipo>

Con estas pautas claras, declararemos en TechsContract un tipo MIME para la obtención de múltiples elementos y otro para un solo elemento. Veamos:

/**
 * Tipo MIME que retorna la consulta de una sola fila
 */
public final static String SINGLE_MIME =
        "vnd.android.cursor.item/vnd." + AUTHORITY + ACTIVIDAD;
/**
 * Tipo MIME que retorna la consulta de {@link CONTENT_URI}
 */
public final static String MULTIPLE_MIME =
        "vnd.android.cursor.dir/vnd." + AUTHORITY + ACTIVIDAD;

Tarea #2: Implementar Métodos Del Content Provider

Hasta el momento ya hemos definido las características necesarias de la API que podrán consumir los clientes.

Ahora vamos a crear una nueva clase que extienda de Content Provider. Esto requiere que implementemos los métodos onCreate(), query(), insert(), update(), delete() y getType().

Paso #1: Crear nueva clase que extienda de Content Provider

Ve a tu paquete java y crea una nueva clase: Click derecho > New… > Java Class File. Asígnale el nombre “TechsProvider” y acepta.

Android Studio: Crear Nueva Clase Java

Ahora extiéndela de ContentProvider:

import android.content.ContentProvider;

/**
 * Content Provider 
 */
public class TechsProvider extends ContentProvider {
    // ...
}

Luego presiona ALT + INSERT sobre el editor. Este comando despliega un menú donde podremos elegir construcciones automáticas de código. Donde seleccionarás la opción “Override Methods…”

Android Studio: Override Methods

En el asistente que se acaba de proyectar están todos los métodos que podemos sobrescribir de las superclases. Selecciona los métodos mencionados anteriormente que se deben emplear sobre el Content Provider y presiona “OK”:

Android Studio: Implementar Override Methods

Con este procedimiento tendremos armado el esqueleto de nuestro Content Provider.

Paso #2: Crear la base de datos del Content Provider

Ya hemos visto que la clase API que se creó posteriormente puede actuar como Script o Contract Class para nuestra base de datos. Por lo que omitiremos la creación de una nueva clase para ello.

Nos concentraremos en crear la clase gestora de SQLite que extienda de SQLiteOpenHelper. La idea es crear simplemente la tabla actividad e insertar 5 registros de prueba.

import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

/**
 * Clase envoltura para el gestor de Bases de datos
 */
class DatabaseHelper extends SQLiteOpenHelper {


    public DatabaseHelper(Context context,
                          String name,
                          SQLiteDatabase.CursorFactory factory,
                          int version) {
        super(context, name, factory, version);
    }

    public void onCreate(SQLiteDatabase database) {
        createTable(database); // Crear la tabla "actividad"
        loadDummyData(database); // Cargar 5 datos de prueba
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // Actualizaciones
    }

    /**
     * Crear tabla en la base de datos
     *
     * @param database Instancia de la base de datos
     */
    private void createTable(SQLiteDatabase database) {
        String cmd = "CREATE TABLE " + TechsContract.ACTIVIDAD + " (" +
                TechsContract.Columnas._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                TechsContract.Columnas.CATEGORIA + " TEXT, " +
                TechsContract.Columnas.PRIORIDAD + " TEXT, " +
                TechsContract.Columnas.ESTADO + " TEXT, " +
                TechsContract.Columnas.TECNICO + " TEXT, " +
                TechsContract.Columnas.DESCRIPCION + " TEXT);";
        database.execSQL(cmd);
    }

    /**
     * Carga datos de ejemplo en la tabla
     * @param database Instancia de la base de datos
     */
    private void loadDummyData(SQLiteDatabase database) {
        ContentValues values = new ContentValues();
        values.put(TechsContract.Columnas.CATEGORIA, "Factibilidad");
        values.put(TechsContract.Columnas.PRIORIDAD, "Media");
        values.put(TechsContract.Columnas.ESTADO, "Abierta");
        values.put(TechsContract.Columnas.TECNICO, "Juan Pedrozo");
        values.put(TechsContract.Columnas.DESCRIPCION, "LLevar router MX230");
        database.insert(TechsContract.ACTIVIDAD, null, values);

        values = new ContentValues();
        values.put(TechsContract.Columnas.CATEGORIA, "Reparación");
        values.put(TechsContract.Columnas.PRIORIDAD, "Alta");
        values.put(TechsContract.Columnas.ESTADO, "En Curso");
        values.put(TechsContract.Columnas.TECNICO, "Mirta Gomez");
        values.put(TechsContract.Columnas.DESCRIPCION, "Internet intermitente");
        database.insert(TechsContract.ACTIVIDAD, null, values);

        values = new ContentValues();
        values.put(TechsContract.Columnas.CATEGORIA, "Traslado");
        values.put(TechsContract.Columnas.PRIORIDAD, "Baja");
        values.put(TechsContract.Columnas.ESTADO, "Cerrada");
        values.put(TechsContract.Columnas.TECNICO, "Carlos Gutierrez");
        values.put(TechsContract.Columnas.DESCRIPCION, "Nueva dirección: Cra 4 #2C-90");
        database.insert(TechsContract.ACTIVIDAD, null, values);

        values = new ContentValues();
        values.put(TechsContract.Columnas.CATEGORIA, "Migración");
        values.put(TechsContract.Columnas.PRIORIDAD, "Baja");
        values.put(TechsContract.Columnas.ESTADO, "Abierta");
        values.put(TechsContract.Columnas.TECNICO, "Gloria Quiñonez");
        values.put(TechsContract.Columnas.DESCRIPCION, "Sustitución cable soporte ipV6");
        database.insert(TechsContract.ACTIVIDAD, null, values);

        values = new ContentValues();
        values.put(TechsContract.Columnas.CATEGORIA, "Mantenimiento");
        values.put(TechsContract.Columnas.PRIORIDAD, "Media");
        values.put(TechsContract.Columnas.ESTADO, "En Curso");
        values.put(TechsContract.Columnas.TECNICO, "Julian Arreondo");
        values.put(TechsContract.Columnas.DESCRIPCION, "LLevar Lista de checkeo rendimiento");
        database.insert(TechsContract.ACTIVIDAD, null, values);
    }
}

Ahora simplemente declaras una instancia de DatabaseHelper en TechsProvider y luego la inicializas en el método onCreate():

/**
 * Content Provider personalizado para las actividades
 */
public class TechsProvider extends ContentProvider {
    /**
     * Nombre de la base de datos
     */
    private static final String DATABASE_NAME = "techs.db";
    /**
     * Versión actual de la base de datos
     */
    private static final int DATABASE_VERSION = 1;
    /**
     * Instancia del administrado de BD
     */
    private DatabaseHelper databaseHelper;

    @Override
    public boolean onCreate() {
        // Inicializando gestor BD
        databaseHelper = new DatabaseHelper(
                getContext(),
                DATABASE_NAME,
                null,
                DATABASE_VERSION
        );

        return true;
    }
}

Es buena práctica evitar crear la base de datos y realizar operaciones en el método onCreate() del Content Provider, ya que esto puede retrasar el inicio.

Afortunadamente SQLiteOpenHelper.onCreate() solo se llama cuando usamos getWritableDatabase(). El objetivo es retardar la llamada de este hasta el momento que se realice la primera operación de datos y así evitar demoras.

Paso #2: Implementar el método getType()

El método getType() permite obtener los tipos MIME correspondientes a una Uri que envíe el cliente como parámetro.

En nuestro caso, la implementación es sencilla. Solo usamos una estructura switch junto a la clase UriMatcher para determinar los posibles casos:

/**
 * Content Provider personalizado para las actividades
 */
public class TechsProvider extends ContentProvider {
    //...

    @Override
    public String getType(Uri uri) {
        switch (TechsContract.uriMatcher.match(uri)) {
            case TechsContract.ALLROWS:
                return TechsContract.MULTIPLE_MIME;
            case TechsContract.SINGLE_ROW:
                return TechsContract.SINGLE_MIME;
            default:
                throw new IllegalArgumentException("Tipo de actividad desconocida: " + uri);
        }
    }    
}

La dinámica consiste en comparar el parámetro uri que llega por getType() con los patrones que tenemos almacenados en uriMatcher. Según sea el caso así retornaremos el tipo MIME correspondiente.

Paso #3: Implementar el método query()

El siguiente paso es sobrescribir el método query() para retornar un cursor de datos hacia las aplicaciones cliente. Lo que quiere decir que usaremos a SQLiteDatabase.query()  dentro de ContentResolver.query(). Una simple delegación de métodos.

En este caso debemos asegurarnos de procesar la consulta de forma apropiada a través de la URI que nos envían, las columnas seleccionadas, el cuerpo de la sentencia WHERE y el orden de los registros establecido.

/**
 * Content Provider personalizado para las actividades
 */
public class TechsProvider extends ContentProvider {
    //...

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {


        // Obtener base de datos
        SQLiteDatabase db = databaseHelper.getWritableDatabase();
        // Comparar Uri
        int match = TechsContract.uriMatcher.match(uri);

        Cursor c;

        switch (match) {
            case TechsContract.ALLROWS:
                // Consultando todos los registros
                c = db.query(TechsContract.ACTIVIDAD, projection,
                        selection, selectionArgs,
                        null, null, sortOrder);
                c.setNotificationUri(
                        getContext().getContentResolver(),
                        TechsContract.CONTENT_URI);
                break;
            case TechsContract.SINGLE_ROW:
                // Consultando un solo registro basado en el Id del Uri
                long idActividad = ContentUris.parseId(uri);
                c = db.query(TechsContract.ACTIVIDAD, projection,
                        TechsContract.Columnas._ID + " = " + idActividad,
                        selectionArgs, null, null, sortOrder);
                c.setNotificationUri(
                        getContext().getContentResolver(),
                        TechsContract.CONTENT_URI);
                break;
            default:
                throw new IllegalArgumentException("URI no soportada: " + uri);
        }
        return c;

    }   
}

Como ves, simplemente usamos el uriMatcher para dividir el rumbo de la consulta. Es importante usar el método setNotificationUri() luego de obtener los resultados en el cursor. Este permite añadir un observador a la URI relacionada por si se requiere un rápido acceso posterior. Lo que te ahorrará tiempo.

Paso #4: Implementar el método insert()

Si recuerdas bien, el método SQLiteDatabase.insert() recibe un conjunto de valores a través de una estructura ContentValues.

Siguiendo esta convención, el método ContentProvider.insert() debe asegurarse que los valores que vienen en este objeto sean acordes a la URI de contenido seleccionada por la aplicación cliente.

/**
 * Content Provider personalizado para las actividades
 */
public class TechsProvider extends ContentProvider {
    //...

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        // Validar la uri
        if (TechsContract.uriMatcher.match(uri) != TechsContract.ALLROWS) {
            throw new IllegalArgumentException("URI desconocida : " + uri);
        }
        ContentValues contentValues;
        if (values != null) {
            contentValues = new ContentValues(values);
        } else {
            contentValues = new ContentValues();
        }

        // Si es necesario, verifica los valores

        // Inserción de nueva fila
        SQLiteDatabase db = databaseHelper.getWritableDatabase();
        long rowId = db.insert(TechsContract.ACTIVIDAD,
                null, contentValues);
        if (rowId > 0) {
            Uri uri_actividad =
                    ContentUris.withAppendedId(
                            TechsContract.CONTENT_URI, rowId);
            getContext().getContentResolver().
                    notifyChange(uri_actividad, null);
            return uri_actividad;
        }
        throw new SQLException("Falla al insertar fila en : " + uri);
    }
}

insert() retorna en la nueva URI de contenido asociada al id del nuevo registro. Para construirla usa el método de utilidad ContentUris.withAppendedId(), el cual concatena la URI principal más el nuevo identificador.

Cuando hayas insertado el nuevo registro en la base de datos debes llamar el método notifyChange() para indicar al observador que los datos han cambiado. Esto es supremamente importante para un CursorAdapter y la actualización de la lista que alimente.

Paso #5: Implementar el método update()

La actualización es muy similar a la inserción, tanto que podemos reutilizar la mayor parte del código. Sin embargo este método retorna en las filas que fueron afectadas con el comando.

/**
 * Content Provider personalizado para las actividades
 */
public class TechsProvider extends ContentProvider {
    //...

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {

        SQLiteDatabase db = databaseHelper.getWritableDatabase();
        int affected;
        switch (TechsContract.uriMatcher.match(uri)) {
            case TechsContract.ALLROWS:
                affected = db.update(TechsContract.ACTIVIDAD, values,
                        selection, selectionArgs);
                break;
            case TechsContract.SINGLE_ROW:
                String idActividad = uri.getPathSegments().get(1);
                affected = db.update(TechsContract.ACTIVIDAD, values,
                        TechsContract.Columnas._ID + "=" + idActividad
                                + (!TextUtils.isEmpty(selection) ?
                                " AND (" + selection + ')' : ""),
                        selectionArgs);
                break;
            default:
                throw new IllegalArgumentException("URI desconocida: " + uri);
        }
        getContext().getContentResolver().notifyChange(uri, null);
        return affected;
    }
}

Paso #6: Implementar el método delete()

Eliminar uno o varios registros con delete() es parecido a actualizar. Debemos retornar la cantidad de filas que se afectaron dependiendo de la URI de contenido asociada como parámetro. Nada complicado:

/**
 * Content Provider personalizado para las actividades
 */
public class TechsProvider extends ContentProvider {
    //...
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {

        SQLiteDatabase db = databaseHelper.getWritableDatabase();

        int match = TechsContract.uriMatcher.match(uri);
        int affected;

        switch (match) {
            case TechsContract.ALLROWS:
                affected = db.delete(TechsContract.ACTIVIDAD,
                        selection,
                        selectionArgs);
                break;
            case TechsContract.SINGLE_ROW:
                long idActividad = ContentUris.parseId(uri);
                affected = db.delete(TechsContract.ACTIVIDAD,
                        TechsContract.Columnas._ID + "=" + idActividad
                                + (!TextUtils.isEmpty(selection) ?
                                " AND (" + selection + ')' : ""),
                        selectionArgs);
                // Notificar cambio asociado a la uri
                getContext().getContentResolver().
                        notifyChange(uri, null);
                break;
            default:
                throw new IllegalArgumentException("Elemento actividad desconocido: " +
                        uri);
        }
        return affected;
    }
}

Tarea #3: Registrar el Content Provider en el Android Manifest

Debido a que un Content Provider es un componente robusto de una aplicación Android, es necesario registrar su existencia en la etiqueta <application> del archivo Android Manifest.

Usa la siguiente sintaxis de declaración para incluir su registro:

<provider
    android:name=".modelo.TechsProvider"
    android:authorities="com.herprogramacion.mytechs.modelo.TechsProvider"
    android:exported="false" />

La etiqueta contenedora será <provider> y algunos de sus atributos son:

  • android:name: Es el nombre de la clase que se extiende de ContentProvider. En nuestro caso especificamos .modelo.TechsProvider que es equivalente a com.herprogramacion.modelo.TechsProvider. Su uso es obligatorio.
  • android:authorities: Determina las autoridades que proveerán los datos del Content Provider. Si son más de una, entonces usa punto y coma para su separación. Recuerda que una autoridad es una dirección que usa el Content Resolver para encontrar los datos del Content Provider.
  • android:exported: Habilita o deshabilita el acceso de otras aplicaciones al Content Provider. Usa "true" para permitir que otras aplicaciones consulten los datos, de lo contrario usa "false" para impedirlo.

Si deseas aprender más, puedes revisar la documentación oficial sobre los atributos de la etiqueta provider.

Usar El Content Provider Personalizado

Pongamos en marcha nuestro Content Provider para comprobar su resultado. La idea es crear una aplicación que tenga una lista de las actividades que se le han asignado a los técnicos, donde puede ver el detalle, insertar, modificar y eliminar.

Al implementar todas esas acciones usaremos al máximo las capacidades del Content Provider y así despejar cualquier duda. Este no compartirá datos con el exterior, ya que se requiere para uso interno.

Como te digo, los Content Providers tienen un gran potencial para conformar patrones MVC y sincronizar datos.

La interfaz de la aplicación se basa en la plantilla que utilizamos en el artículo sobre la , rojo es "En Curso" y amarillo será "Abierta".

Al lado derecho pondremos la categoría junto al nombre del técnico asignado. Y la prioridad irá menospreciada hacia el lado derecho. El resultado sería el siguiente:

item_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent"
    android:minHeight="?android:attr/listPreferredItemHeight"
    android:padding="@dimen/activity_horizontal_margin">

    <!-- Indicador circular -->
    <View android:layout_width="@dimen/indicator_size"
        android:layout_height="@dimen/indicator_size"
        android:id="@+id/indicator"
        android:background="@drawable/green_indicator"
        android:layout_centerVertical="true"
        android:layout_marginRight="28dp"
        android:layout_marginLeft="12dp" />

    <!-- Categoría de la actividad -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:text="Categoria"
        android:id="@+id/categoria_text"
        android:textColor="@android:color/black"
        android:layout_toRightOf="@+id/indicator" />

    <!-- Prioridad de la actividad -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:text="Prioridad"
        android:id="@+id/prioridad_text"
        android:layout_below="@+id/categoria_text"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true"
        android:textStyle="italic" />

    <!-- Técnico que realizará la activida -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:text="Técnico"
        android:id="@+id/tecnico_text"
        android:layout_below="@+id/categoria_text"
        android:layout_toRightOf="@+id/indicator" />

</RelativeLayout>

Desarrollo Android: Diseño de Layout para item de lista

Ahora solo implementamos el adaptador para que ubique los datos de las columnas en cada view:

ActivitiesAdapter.java

import android.content.Context;
import android.database.Cursor;
import android.support.v4.widget.CursorAdapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.herprogramacion.mytechs.modelo.TechsContract;

/**
 * {@link CursorAdapter} personalizado para las actividades
 * de los técnicos
 */
public class ActivitiesAdapter extends CursorAdapter {

    public ActivitiesAdapter(Context context) {
        super(context, null, 0);
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {

        TextView categoria = (TextView) view.findViewById(R.id.categoria_text);
        categoria.setText(cursor.getString(
                cursor.getColumnIndex(TechsContract.Columnas.CATEGORIA)));

        TextView prioridad = (TextView) view.findViewById(R.id.prioridad_text);
        prioridad.setText(cursor.getString(
                cursor.getColumnIndex(TechsContract.Columnas.PRIORIDAD)));

        TextView tecnico = (TextView) view.findViewById(R.id.tecnico_text);
        tecnico.setText(cursor.getString(
                cursor.getColumnIndex(TechsContract.Columnas.TECNICO)));

        String estado = cursor.getString(
                cursor.getColumnIndex(TechsContract.Columnas.ESTADO));

        View indicator = view.findViewById(R.id.indicator);

        switch (estado) {
            case "Cerrada":
                indicator.setBackgroundResource(R.drawable.green_indicator);
                break;
            case "En Curso":
                indicator.setBackgroundResource(R.drawable.red_indicator);
                break;
            case "Abierta":
                indicator.setBackgroundResource(R.drawable.yellow_indicator);
                break;
        }
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        return inflater.inflate(R.layout.item_layout, parent, false);
    }
}

Tarea #3: Cargar datos en segundo plano con Loaders

Los loaders son elementos creados para cargar datos en segundo plano y evitar la interferencia en nuestro hilo principal. Además están monitoreando constantemente si los datos han cambiado. Lo que evita la implementación de nuestros propios observadores de contenido.

Es buena práctica implementarlos cuando deseas extraer datos de un Content Provider en vez de realizar una consulta arbitraria.

La dinámica de su funcionamiento se basa en el uso de la clase LoaderManager. Un objeto asociado a una actividad o fragmento que proporciona el uso de un Loader. La idea es manipular el comportamiento del Loader con los manejadores que Loader Manager nos entrega.

Existe la ventaja de escribir nuestros propios loaders, sin embargo ya existe una subclase especializada para los content providers llamada CursorLoader. Con ella asignaremos gestionaremos creación de nuestro adaptador.

La API de Loaders no solo permite cargar en segundo plano datos almacenados en tablas. Es posible cargar varios tipos de información si creas tus propias implementaciones.

Los pasos a seguir para usar la clase CursorLoaderson los siguientes:

  1. Implementar la interfaz LoaderManager.LoaderCallbacks sobre la actividad o fragmento donde se manejarán los datos.
  2. Sobrescribir los manejadores onCreateLoader(), onLoadFinished() y onLoaderReset().
  3. Iniciar un nuevo CursorLoader con el método LoaderManager.initLoader().
  4. Destruir el loader en el manejador onDestroy() de la actividad o fragmento.

Sabiendo esto no queda más que comenzar…

Paso # 1: Implementar LoaderCallbacks sobre fragmento de lista

Este paso es sencillo, solo añadimos la interfaz a MainFragment. Adicionalmente creamos una nueva instancia del adaptador en onCreateView().

Si te fijas, nuestro adaptador no recibe un cursor en su constructor como debería ser, ya que es necesario evitar retrasar la carga de datos solo hasta cuando la consulta haya sido completada. Por esta razón postergaremos intencionalmente el evento.

/**
 * Fragmento principal con lista de actividades
 */
public class MainFragment extends ListFragment implements
        LoaderManager.LoaderCallbacks<Cursor> {
    /**
     * Adaptador
     */
    private ActivitiesAdapter adaptador;

    public MainFragment() {
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_main, container, false);
        // Otras acciones
        return view;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // Iniciar adaptador
        adaptador = new ActivitiesAdapter(getActivity());
        // Relacionar adaptador a la lista
        setListAdapter(adaptador);
    }
}

Paso #2: Sobrescribir onCreateLoader(), onLoadFinished() y onLoaderReset()

Antes de implementar estos métodos primero entandamos un poco cuando son invocados:

  • onCreateLoader(): Crea un nueva instancia de un CursorLoader para cargar los datos de un Content Provider. Solo se invoca si el Loader no existe.
  • onLoadFinished(): Es invocado cuando el LoaderManager ha terminado la consulta que el CursorLoader tenía asociada. Recibe como parámetro el cursor con los datos.
  • onLoaderReset(): Se llama cuando un loader previo ha sido restablecido con el método restartLoader(). Lo ideal es liberar dentro de él las referencias de datos para retornar al estado inicial.

Comencemos con onCreateLoader(). Simplemente incluiremos una línea donde se crea un nuevo CursorLoader. El constructor es muy similar al método ContentProvider.query(), ya que recibe la URI de contenido y las partes de una sentencia SELECT.

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    // Consultar todos los registros
    return new CursorLoader(
            getActivity(),
            TechsContract.CONTENT_URI,
            null, null, null, null);
}

Si eres buen observador, habrás notado que no hemos usado la instancia del Content Resolver para consultar nuestro provider. Esto se debe a que la clase CursorLoader nos libra de esta responsabilidad.

Si no usáramos loaders tendríamos que acceder al ContentResolver y consultar de la siguiente forma:

getActivity().getContentResolver().query(
        TechsContract.CONTENT_URI,
        null, null, null, null);

Lo siguiente es sobrescribir onLoadFinished(). Aquí intercambiaremos el cursor nulo que  tiene el adaptador por el cursor obtenido de la consulta  realizada por el loader:

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    adaptador.swapCursor(data);
}

swapCursor() intercambia por un nuevo cursor y retorna el cursor antiguo. El cursor retornado no es cerrado, por lo que puede ser aprovechado para tareas de comparación si se requiere.

En el caso de onLoaderReset() intercambiaremos el cursor antiguo por una zona nula. Esto eliminará la referencia de datos y reiniciaremos nuestro adaptador al estado inicial.

@Override
public void onLoaderReset(Loader<Cursor> loader) {
    adaptador.swapCursor(null);
}

Paso #3: Iniciar el CursorLoader

Inicia el cursor con el método LoaderManager.initLoader() luego de que hayas relacionado la lista con el adaptador. Para obtener la instancia del administrador solo usa el método getLoaderManager(). Si estás en una actividad debes usar la variación getSupportLoaderManager().

Desde el momento que inicias el loader se transfiere la carga de los datos del provider al segundo plano. Donde se empezarán a invocar los controladores que sobrescribimos en el paso anterior.

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    // Iniciar adaptador
    adaptador = new ActivitiesAdapter(getActivity());
    // Relacionar adaptador a la lista
    setListAdapter(adaptador);
    // Iniciar Loader
    getLoaderManager().initLoader(0, null, this);
}

Veamos un poco sobre los parámetros de initLoader():

  • id: Identificador único para el loader.
  • args: Argumentos extras que permitirán dirigir el flujo en el método onCreateLoader().
  • callback: Es la interfaz LoaderManager.LoaderCallbacks que gestionará la carga y monitoreo de datos del loader.

En el inicio usamos 0 como identificador de forma arbitraría. No usamos argumentos y como interfaz usamos el fragmento local.

Paso #4: Destruir el CursorLoader

En este paso eliminaremos la instancia del loader dentro del método onDestroy() del fragmento. Solo debes usar el método destroyLoader() con el identificador del loader. Adicionalmente puedes liberar el cursor del adaptador y el mismo adaptador.

@Override
public void onDestroy() {
    super.onDestroy();
    try {
        getLoaderManager().destroyLoader(0);
        if (adaptador != null) {
            adaptador.changeCursor(null);
            adaptador = null;
        }
    } catch (Throwable localThrowable) {
        // Proyectar la excepción
    }
}

Tarea #4: Crear actividad de detalle

Ahora es el turno de visualizar el detalle. Crea una nueva actividad llamada “DetailActivity.java” junto a un fragmento “DetailFragment.java”.

La actividad de detalle puede inflarse con el recurso activity_main.xml de la actividad principal. Por otro lado el fragmento debe contener un layout personalizado para mostrar los datos de la actividad del técnico.

Diseño De Una Actividad De Detalle Con Material Design

En mi caso creé el diseño simple mostrado en la ilustración anterior. La definición del layout sería la siguiente:

fragment_detail.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/detail_background"
    android:orientation="vertical">

    <!-- Parte superior -->
    <ImageView
        android:id="@+id/cabecera"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="50"
        android:scaleType="centerCrop"
        android:src="@drawable/google_maps_dummy" />

    <!-- Hero View -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/primary"
        android:elevation="2dp"
        android:orientation="vertical"
        android:padding="@dimen/activity_horizontal_margin">

        <TextView
            android:id="@+id/categoria_text"
            style="@style/TextAppearance.AppCompat.Title.Inverse"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:maxLines="2"
            android:text="Dummy"
            android:textIsSelectable="true" />

    </LinearLayout>

    <!-- Contenido -->
    <ScrollView
        android:id="@+id/scrollView"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:layout_weight="70">

        <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/detail"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="70"
            android:padding="@dimen/activity_horizontal_margin">

            <!-- Card 1 -->
            <android.support.v7.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
                android:id="@+id/card1"
                android:layout_width="match_parent"
                android:layout_height="150dp"
                card_view:cardElevation="@dimen/card_elevation"
                card_view:cardUseCompatPadding="true">

                <RelativeLayout
                    android:id="@+id/content1"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:padding="@dimen/activity_horizontal_margin">
                    <!-- Etiqueta de la estado -->
                    <TextView
                        android:id="@+id/estado_label"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_below="@+id/estado_text"
                        android:layout_centerVertical="true"
                        android:text="@string/estado_label"
                        android:textAppearance="?android:attr/textAppearanceSmall" />

                    <!-- Texto de la estado -->
                    <TextView
                        android:id="@+id/estado_text"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_alignParentLeft="true"
                        android:text="Dummy"
                        android:textAppearance="?android:attr/textAppearanceLarge"
                        android:textColor="@color/primary" />


                    <!-- Label del técnico -->
                    <TextView
                        android:id="@+id/tecnico_label"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_alignParentLeft="true"
                        android:layout_alignParentStart="true"
                        android:layout_below="@+id/tecnico_text"
                        android:text="@string/tecnico_label"
                        android:textAppearance="?android:attr/textAppearanceSmall" />

                    <TextView
                        android:id="@+id/tecnico_text"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_alignLeft="@+id/estado_label"
                        android:layout_alignStart="@+id/estado_label"
                        android:layout_below="@+id/estado_label"
                        android:layout_centerVertical="true"
                        android:layout_marginTop="16dp"
                        android:text="Dummy"
                        android:textAppearance="?android:attr/textAppearanceLarge"
                        android:textColor="@color/primary" />

                    <TextView
                        android:id="@+id/proridad_label"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_alignParentEnd="true"
                        android:layout_alignParentRight="true"
                        android:layout_alignTop="@+id/tecnico_label"
                        android:text="@string/prioridad_label"
                        android:textAppearance="?android:attr/textAppearanceSmall" />

                    <TextView
                        android:id="@+id/prioridad_text"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_alignParentRight="true"
                        android:layout_alignTop="@+id/tecnico_text"
                        android:text="Dummy"
                        android:textAppearance="?android:attr/textAppearanceLarge"
                        android:textColor="@color/primary" />

                </RelativeLayout>
            </android.support.v7.widget.CardView>

            <!-- Card 2 -->
            <android.support.v7.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
                android:id="@+id/card2"
                android:layout_width="match_parent"
                android:layout_height="150dp"
                android:layout_below="@+id/card1"
                android:layout_marginTop="@dimen/margin_top_between_cards"
                card_view:cardElevation="@dimen/card_elevation"
                card_view:cardUseCompatPadding="true">

                <RelativeLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:padding="@dimen/activity_horizontal_margin">

                    <!-- Etiqueta de la descripción -->
                    <TextView
                        android:id="@+id/descripcion_label"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_alignParentLeft="true"
                        android:layout_alignParentStart="true"
                        android:text="@string/descripcion_label"
                        android:textAppearance="?android:attr/textAppearanceLarge"
                        android:textColor="@color/primary" />

                    <!-- Texto de la descripción -->
                    <TextView
                        android:id="@+id/descripcion_text"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_alignParentLeft="true"
                        android:layout_alignParentStart="true"
                        android:layout_below="@+id/descripcion_label"
                        android:text="Dummy"
                        android:textAppearance="?android:attr/textAppearanceSmall" />
                </RelativeLayout>
            </android.support.v7.widget.CardView>
        </RelativeLayout>
    </ScrollView>
</LinearLayout>

Inicialmente se deben proyectar los datos de la actividad con el id enviado a través del Intent. Si analizas la situación, sería necesario consultar de nuevo la base de datos por el registro específico.

Esta vez podemos consultar directamente desde el Content Resolver debido al bajo volumen de datos. Pero si crees que tu base de datos podría demorar con este registro, entonces añade una tarea asíncrona para la carga.

import android.content.ContentUris;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.herprogramacion.mytechs.R;
import com.herprogramacion.mytechs.modelo.TechsContract;

/**
 * Fragmento para el detalle de la actividad
 */
public class DetailFragment extends Fragment {

    /**
     * Textos del layout
     */
    private TextView descripcion, categoria, entidad, prioridad, estado;

    /**
     * Identificador de la actividad
     */
    private long id;

    public DetailFragment() {
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_detail, container, false);

        // Obtención de views
        descripcion = (TextView) view.findViewById(R.id.descripcion_text);
        categoria = (TextView) view.findViewById(R.id.categoria_text);
        entidad = (TextView) view.findViewById(R.id.tecnico_text);
        prioridad = (TextView) view.findViewById(R.id.prioridad_text);
        estado = (TextView) view.findViewById(R.id.estado_text);

        return view;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);
    }

    @Override
    public void onResume() {
        super.onResume();
        id = getActivity().getIntent().getLongExtra(TechsContract.Columnas._ID, -1);
        updateView(id);  // Actualzar la vista con los datos de la actividad
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        switch (id) {
            case R.id.action_edit:
                beginUpdate(); // Actualizar
                return true;
            case R.id.action_delete:
                deleteData(); // Eliminar
                getActivity().finish();
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }

    }   

    /**
     * Envía todos los datos de la actividad hacia el formulario
     * de actualización
     */
    private void beginUpdate() {
        getActivity()
                .startActivity(
                        new Intent(getActivity(), UpdateActivity.class)
                                .putExtra(TechsContract.Columnas._ID, id)
                                .putExtra(TechsContract.Columnas.DESCRIPCION, descripcion.getText())
                                .putExtra(TechsContract.Columnas.CATEGORIA, categoria.getText())
                                .putExtra(TechsContract.Columnas.TECNICO, entidad.getText())
                                .putExtra(TechsContract.Columnas.PRIORIDAD, prioridad.getText())
                                .putExtra(TechsContract.Columnas.ESTADO, estado.getText())
                );
    }


    /**
     * Actualiza los textos del layout
     *
     * @param id Identificador de la actividad
     */
    private void updateView(long id) {
        if (id == -1) {
            descripcion.setText("");
            categoria.setText("");
            entidad.setText("");
            prioridad.setText("");
            estado.setText("");

            return;
        }

        Uri uri = ContentUris.withAppendedId(TechsContract.CONTENT_URI, id);
        Cursor c = getActivity().getContentResolver().query(
                uri,
                null, null, null, null);

        if (!c.moveToFirst())
            return;

        String descripcion_text = c.getString(c.getColumnIndex(TechsContract.Columnas.DESCRIPCION));
        String categoria_text = c.getString(c.getColumnIndex(TechsContract.Columnas.CATEGORIA));
        String entidad_text = c.getString(c.getColumnIndex(TechsContract.Columnas.TECNICO));
        String prioridad_text = c.getString(c.getColumnIndex(TechsContract.Columnas.PRIORIDAD));
        String estado_text = c.getString(c.getColumnIndex(TechsContract.Columnas.ESTADO));

        descripcion.setText(descripcion_text);
        categoria.setText(categoria_text);
        entidad.setText(entidad_text);
        prioridad.setText(prioridad_text);
        estado.setText(estado_text);

        c.close(); // Liberar memoria del cursor
    }
}

Si te fijas, el método updateView() facilita la actualización de todos los datos del layout dependiendo de la consulta que se realiza con el Content Resolver.

También se han implementado las acciones de los action buttons en la action bar. El botón del lápiz representa la edición(R.id.action_edit) y la caneca de reciclaje es la eliminación (R.id.action_delete).

Observa el método onOptionsItemSelected() y verás que al momento de la edición se llama al método beginUpdate(). El objetivo de este método es iniciar la actividad de edición que veremos adelante.

Por otro lado, la eliminación es tratada con el método ContentResolver.delete(). Este recibe la uri de contenido con el identificador asociado.

Tarea #5: Insertar datos en un Content Provider

La siguiente tarea es crear nuestra actividad de inserción para probar el método insert() del Content Provider.

De nuevo crea una actividad con un fragmento para el proyecto. A la actividad llámala «InsertActivity» y al fragmento ponle «InsertFragment».

Material Design: Layout de formulario de inserción

Usaremos un Floating Action Button sobre la lista para permitir al usuario iniciar la inserción. Por ahora implementaremos la librería de nuestro amigo makovkastar para este fin, sin embargo espero que podamos ver mas a fondo la librería de diseño que Google ha revelado hace poco para usar la clase FloatingActionButton.

Por otro lado, esta actividad es tan simple que puede inflarse con el layout de nuestra actividad principal. El fragmento requiere que pongamos los campos necesarios para ingresar todos los datos de una actividad. Es aquí donde usas tu imaginación para diseñar un buen formulario.

Sin embargo puedes copiar el mío. A continuación te dejo el layout:

fragment_insert.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="@dimen/activity_horizontal_margin"
    tools:context="com.herprogramacion.mytechs.ui.InsertFragment">

    <!-- Etiqueta de la descripción -->
    <TextView
        android:id="@+id/descripcion_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:text="@string/descripcion_label"
        android:textAppearance="?android:attr/textAppearanceSmall" />

    <!-- Texto de la descripción -->
    <EditText
        android:id="@+id/descripcion_input"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/descripcion_label"
        android:hint="@string/descripcion_input"
        android:maxLength="120" />

    <!-- Divisor -->
    <View
        android:id="@+id/divider1"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_below="@+id/descripcion_input"
        android:layout_marginTop="@dimen/margin_divider"
        android:background="@color/divider_color" />

    <!-- Etiqueta del estado -->
    <TextView
        android:id="@+id/estado_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/tecnico_spinner"
        android:paddingTop="@dimen/padding_top_form"
        android:text="@string/estado_label"
        android:textAppearance="?android:attr/textAppearanceSmall" />

    <!-- Etiqueta del técnico asignado -->
    <TextView
        android:id="@+id/tecnico_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/divider1"
        android:paddingTop="@dimen/padding_top_form"
        android:text="@string/tecnico_label"
        android:textAppearance="?android:attr/textAppearanceSmall" />

    <!-- Etiqueta de la categoría -->
    <TextView
        android:id="@+id/categoria_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/prioridad_spinner"
        android:paddingTop="@dimen/padding_top_form"
        android:text="@string/categoria_label"
        android:textAppearance="?android:attr/textAppearanceSmall" />

    <!-- Etiqueta de la prioridad -->
    <TextView
        android:id="@+id/prioridad_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignTop="@+id/estado_label"
        android:layout_centerHorizontal="true"
        android:paddingTop="@dimen/padding_top_form"
        android:text="@string/prioridad_label"
        android:textAppearance="?android:attr/textAppearanceSmall" />

    <!-- Selección de prioridad -->
    <Spinner
        android:id="@+id/prioridad_spinner"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/prioridad_label"
        android:layout_alignStart="@+id/prioridad_label"
        android:layout_below="@+id/prioridad_label"
        android:entries="@array/prioridad" />

    <!-- Selección del técnico -->
    <Spinner
        android:id="@+id/tecnico_spinner"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/tecnico_label"
        android:entries="@array/tecnico" />

    <!-- Selección de la categoría -->
    <Spinner
        android:id="@+id/categoria_spinner"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/categoria_label"
        android:entries="@array/categoria" />

    <!-- Selección del estado -->
    <Spinner
        android:id="@+id/estado_spinner"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/estado_label"
        android:layout_marginRight="@dimen/margin_right_form"
        android:layout_toLeftOf="@+id/prioridad_label"
        android:entries="@array/estado" />
</RelativeLayout>

Ahora vamos a la parte que nos interesa. La inserción.

La idea es guardar los datos una vez el usuario presione el Up Button. Esto requiere que extraigas los datos de todos los views que reciben los datos y ponerlos en una estructura ContentValues. Luego obtienes una instancia del ContentResolver e invocas a su método insert(), el cual recibirá la uri de contenido principal y el conjunto de valores:

import android.content.ContentValues;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Spinner;

import com.herprogramacion.mytechs.R;
import com.herprogramacion.mytechs.modelo.TechsContract;


/**
 * Fragmento con formulario de inserción
 */
public class InsertFragment extends Fragment {

    /**
     * Views del formulario
     */
    private EditText descripcion;
    private Spinner prioridad;
    private Spinner entidad;
    private Spinner estado;
    private Spinner categoria;


    public InsertFragment() {
        // Required empty public constructor

    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_insert, container, false);

        // Obtener views
        descripcion = (EditText) view.findViewById(R.id.descripcion_input);
        prioridad = (Spinner) view.findViewById(R.id.prioridad_spinner);
        entidad = (Spinner) view.findViewById(R.id.tecnico_spinner);
        estado = (Spinner) view.findViewById(R.id.estado_spinner);
        categoria = (Spinner) view.findViewById(R.id.categoria_spinner);

        return view;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        switch (id) {
            case android.R.id.home:
                saveData(); // Guardar datos
                getActivity().finish();
                return true;
            case R.id.action_discard:
                getActivity().finish();
                return true;
            default:
                return super.onOptionsItemSelected(item);

        }


    }

    private void saveData() {
        // Obtención de valores actuales
        ContentValues values = new ContentValues();
        values.put(TechsContract.Columnas.ESTADO, estado.getSelectedItem().toString());
        values.put(TechsContract.Columnas.PRIORIDAD, prioridad.getSelectedItem().toString());
        values.put(TechsContract.Columnas.CATEGORIA, categoria.getSelectedItem().toString());
        values.put(TechsContract.Columnas.TECNICO, entidad.getSelectedItem().toString());
        values.put(TechsContract.Columnas.DESCRIPCION, descripcion.getText().toString());

        getActivity().getContentResolver().insert(
                TechsContract.CONTENT_URI,
                values
        );
    }
}

El método saveData() es el encargado de obtener los valores de todos los views del layout y luego volcarlos sobre el Content Resolver a través del método insert().

Tarea #6: Actualizar datos de un Content Provider

Esta tarea es similar a la anterior. Simplemente actualizaremos los datos que se modifiquen en el formulario de edición.

El formulario de edición implementa el mismo layout del fragmento de inserción. Sin embargo crearemos otra actividad llamada «UpdateActivity» más un fragmento hijo denominado «UpdateFragment».

Este ejemplo no está optimizado, recuerda que el objetivo es entender el funcionamiento del Content Provider. Es tu obligación reducir al máximo el uso de actividades y optimizar las señales de experiencia de usuario.

Veamos el código:

UpdateFragment.java

import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Spinner;

import com.herprogramacion.mytechs.R;
import com.herprogramacion.mytechs.modelo.TechsContract;

/**
 * Fragmento con un formulario de actualización
 */
public class UpdateFragment extends Fragment {
    /**
     * Identificador de la actividad
     */
    private long id;
    /**
     * Views del layout
     */
    private EditText descripcion;
    private Spinner entidad;
    private Spinner prioridad;
    private Spinner estado;
    private Spinner categoria;

    public UpdateFragment() {
        // Required empty public constructor
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_insert, container, false);

        // Obtener views
        descripcion = (EditText) view.findViewById(R.id.descripcion_input);
        prioridad = (Spinner) view.findViewById(R.id.prioridad_spinner);
        entidad = (Spinner) view.findViewById(R.id.tecnico_spinner);
        estado = (Spinner) view.findViewById(R.id.estado_spinner);
        categoria = (Spinner) view.findViewById(R.id.categoria_spinner);

        return view;
    }

    @Override
    public void onResume() {
        super.onResume();
        id = getActivity().getIntent().getLongExtra(TechsContract.Columnas._ID, -1);
        updateView(); // Cargar datos iniciales
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        switch (id) {
            case android.R.id.home:
                updateData(); // Actualizar
                getActivity().finish();
                return true;
            case R.id.action_discard:
                getActivity().finish();
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }

    }

    /**
     * Actualizar datos de la actividad
     */
    private void updateData() {

        // Unir Uri principal con identificador
        Uri uri = ContentUris.withAppendedId(TechsContract.CONTENT_URI, id);

        ContentValues values = new ContentValues();
        values.put(TechsContract.Columnas.DESCRIPCION, descripcion.getText().toString());
        values.put(TechsContract.Columnas.PRIORIDAD, prioridad.getSelectedItem().toString());
        values.put(TechsContract.Columnas.TECNICO, entidad.getSelectedItem().toString());
        values.put(TechsContract.Columnas.ESTADO, estado.getSelectedItem().toString());
        values.put(TechsContract.Columnas.CATEGORIA, categoria.getSelectedItem().toString());

        // Actualiza datos del Content Provider
        getActivity().getContentResolver().update(
                uri,
                values,
                null,
                null
        );
    }

    /**
     * Carga los datos que provienen desde el detalle
     */
    private void updateView() {
        // Obtener datos del formulario
        Intent i = getActivity().getIntent();
        String descripcion_text = i.getStringExtra(TechsContract.Columnas.DESCRIPCION);
        String prioridad_text = i.getStringExtra(TechsContract.Columnas.PRIORIDAD);
        String entidad_text = i.getStringExtra(TechsContract.Columnas.TECNICO);
        String estado_text = i.getStringExtra(TechsContract.Columnas.ESTADO);
        String categoria_text = i.getStringExtra(TechsContract.Columnas.CATEGORIA);

        // Actualizar la vista
        descripcion.setText(descripcion_text);
        prioridad.setSelection(getIndex(prioridad, prioridad_text));
        entidad.setSelection(getIndex(entidad, entidad_text));
        estado.setSelection(getIndex(estado, estado_text));
        categoria.setSelection(getIndex(categoria, categoria_text));
    }

    /**
     * Obtiene el indice de un {@link Spinner} según el valor
     * de una cadena
     *
     * @param spinner Instancia del spinner
     * @param value   Cadena a buscar
     * @return Posición donde se encuentra
     */
    private int getIndex(Spinner spinner, String value) {
        int index = 0;

        for (int i = 0; i < spinner.getCount(); i++) {
            if (spinner.getItemAtPosition(i).toString().equalsIgnoreCase(value)) {
                index = i;
                break;
            }
        }
        return index;
    }
}

Esta vez se usa el método ContentResolver.update() cuando el Up Button es presionado. De esta manera es posible actualizar el registro con el identificador que se transmitió a través del Intent.

Tarea #7: Eliminar datos de un Content Provider

Como viste, la eliminación fue añadida en la actividad de detalle creada previamente. Sin embargo aún no se tiene implementado su funcionamiento.

Para ello crea un método que aísle las acciones de eliminación llamado deleteData() como lo hago en el siguiente código:

/**
 * Elimina la actividad actual
 */
private void deleteData() {
    Uri uri = ContentUris.withAppendedId(TechsContract.CONTENT_URI, id);
    getActivity().getContentResolver().delete(
            uri,
            null,
            null
    );
}

Dentro de él llamamos el método ContentResolver.delete() indicando la URI de contenido principal y la selección del ítem con el identificador que vino almacenado en el Intent.

Ejecutar la aplicación Android

Para finalizar ejecuta la aplicación MyTechs. Prueba todas las posibles operaciones sobre el Content Provider y analiza cómo puedes incluir estas definiciones en tus futuros proyectos.

Material Design: Aplicación Android Para La Gestión De Tareas

Conclusiones

  • Los Content Providers son elementos que permiten compartir datos de tu aplicación con otras aplicaciones de forma segura.
  • Un Content Provider también puede usarse como fachada para crear arquitecturas MVC debido a la virtualización de nombres con URIs de contenido y a la ayuda de los Loaders para el registro de Observadores de contenido que actualicen la interfaz.
  • El Content Resolver es quién accede al Content Provider. Recuerda que no es posible acceder directamente a un Content Provider.
  • Las URIs de contenido facilitan el acceso a los datos gracias a patrones de diseños intuitivos y estandarizados.

Icono de la aplicación MyTechs

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!