Poblar RecyclerView Con Cursor En Android

Este tutorial te enseñará a crear un adaptador para un RecyclerView con Cursor, que genera una lista desde una base de datos SQLite.

Mi idea es mostrarte la forma en que las listas de datos simples pueden ser reemplazadas por un cursor, donde sobrescribiremos los método vitales del Recycler View como onBindViewHolder(), onCreateViewHolder() y getCount().

Sin embargo verás también otros métodos personalizados adicionales requeridos para soportar la actualización de datos e identificación de cada ítem.

Descargar Proyecto En Android Studio

Al terminar de aplicar los conocimientos que te describo podrás obtener la siguiente app Android:

Para desbloquear el link de descarga, suscríbete al boletín informativo de Hermosa Programación:

¿Cómo Poblar Un RecyclerView Con Un Cursor?

Ya sabes que cuando se habla de la clase Cursor, inmediatamente se hace referencia a los resultados de queries en una base de datos local SQLite. Por lo que la necesidad nace de poblar una lista con un modelo de datos relacional.

Siendo así, ten en cuenta estos dos factores:

  • Tiempo de carga de datos variante: Es necesario usar tareas asíncronas para evitar interferencia con el hilo principal. Pero recuerda que existen los Loaders para simplificar la carga de datos.
  • Actualización de datos: Evita cargar los datos manualmente desde la interfaz y mejor delega esta función a un ContentProvider. Este componente protege la definición de datos a través de URIs y se ajusta muy bien con los loaders.

Para solucionar este inconveniente usaremos una app con la siguiente descripción:

Alquileres es una aplicación Android que reúne en un solo espacio gran variedad de anuncios de propiedades en alquiler. La información se despliega en una lista donde los ítems muestran al usuario información sobre el nombre de la oferta, la ubicación, una breve descripción, el precio mensual y una foto que evidencia la calidad del bien.

App Android para alquileres en Colombia

Teniendo en cuenta este ejemplo, ya podemos codificar.

#1. Configurar Nuevo Proyecto En Android Studio

Paso 1. Abre Android Studio, dirígete a File > New > New Project… y crea un proyecto llamado “Alquileres”. Añade una actividad en blanco al proyecto con el nombre de ActividadListaAlquileres y corrige el nombre del layout a actividad_lista_alquileres.xml.

Paso 2. Ahora elige como primer esquema de colores las variaciones 100, 500 y 700 de purpura. Una forma fácil de hacerlo es yendo a Material Design Colors y copiar los códigos HEX al seleccionar cada elemento.

Paleta purpura en Material Design Colors

La segunda paleta para los acentos puedes elegirla en azul 500 para contrastar un poco. Si todo salió bien, tendrás los siguientes valores en tu archivo colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#7B1FA2</color>
    <color name="colorPrimaryDark">#9C27B0</color>
    <color name="colorAccent">#2196F3</color>

    <color name="colorContraste">#EDEDED</color>
</resources>

El elemento colorContraste será el color del background de la actividad principal. Puedes aplicarlo en los estilos con el atributo android:windowBackground:

styles.xml

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>

        <item name="android:windowBackground">@color/colorContraste</item>
    </style>

    <style name="AppTheme.NoActionBar">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
    </style>

    <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />

    <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />

</resources>

Paso 3. De acuerdo al primer screenshot que viste al inicio de la app Alquileres, es necesario agregar la librería de cardviews y Glide para carga de imágenes desde la web. Obviamente también necesitaremos la librería de soporte para el recycler view.

Así que abre tu arhivo build.gradle e insertar las siguientes dependencias.

build.gradle

dependencies {
    ...
    compile 'com.android.support:cardview-v7:23.1.1'
    compile 'com.android.support:recyclerview-v7:23.1.1'
    compile 'com.github.bumptech.glide:glide:3.6.1'
}

Paso 4. Por último agrega las siguientes dimensiones a tu archivo dimens.xml.

<resources>
    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="activity_horizontal_margin">16dp</dimen>
    <dimen name="activity_vertical_margin">16dp</dimen>
    <dimen name="fab_margin">16dp</dimen>

    <dimen name="margen_superior_lista">8dp</dimen>
    <dimen name="altura_imagen_item">192dp</dimen>
</resources>

Los tres primeros elementos vienen por defecto en tu proyecto. Los otros dos se refieren a la margen superior que tiene la lista en el layout padre y a la altura de la imagen en cada alquiler.

#2 Crear Contrato De La Base De Datos

Tanto la base de datos como el content provider requieren que sustentes la estructura de las tablas y uris de contenido. Es aquí donde entra la clase auxiliar de contrato para guardar todas estas referencias para facilitar el orden y cambios de información futuros.

Crea un nuevo paquete con el nombre de provider y luego añade una nueva clase Java llamada Contrato.java. Copia el siguiente código:

Contrato.java

import android.net.Uri;

import java.util.UUID;

/**
 * Contrato con la estructura de la base de datos y forma de las URIs
 */
public class Contrato {

    interface ColumnasAlquiler {
        String ID_ALQUILER = "idAlquiler"; // Pk
        String NOMBRE  = "nombre";
        String UBICACION = "ubicacion";
        String DESCRIPCION = "descripcion";
        String PRECIO = "precio";
        String URL_IMAGEN ="urlImagen";
    }


    // Autoridad del Content Provider
    public final static String AUTORIDAD = "com.herprogramacion.alquileres";

    // Uri base
    public final static Uri URI_CONTENIDO_BASE = Uri.parse("content://" + AUTORIDAD);


    /**
     * Controlador de la tabla "alquiler"
     */
    public static class Alquileres implements ColumnasAlquiler {

        public static final Uri URI_CONTENIDO =
                URI_CONTENIDO_BASE.buildUpon().appendPath(RECURSO_ALQUILERES).build();

        public final static String MIME_RECURSO =
                "vnd.android.cursor.item/vnd." + AUTORIDAD + "/" + RECURSO_ALQUILERES;

        public final static String MIME_COLECCION =
                "vnd.android.cursor.dir/vnd." + AUTORIDAD + "/" + RECURSO_ALQUILERES;


        /**
         * Construye una {@link Uri} para el {@link #ID_ALQUILER} solicitado.
         */
        public static Uri construirUriAlquiler(String idApartamento) {
            return URI_CONTENIDO.buildUpon().appendPath(idApartamento).build();
        }

        public static String generarIdAlquiler() {
            return "A-" + UUID.randomUUID();
        }

        public static String obtenerIdAlquiler(Uri uri) {
            return uri.getLastPathSegment();
        }
    }

    // Recursos
    public final static String RECURSO_ALQUILERES = "alquileres";

}

Cada registro de alquiler tendrá los campos especificados en la interfaz ColumnasAlquiler: nombre, ubicación, descripción, precio e imagen. Además añadí un identificador llamado idAlquiler por si la base de datos será sincronizada.

El controlador Alquileres contiene la uri de contenido general del recurso y los tipos mimes necesarios para el content provider. También tiene los siguientes métodos auxiliares:

  • construirUriAlquiler(): Recibe el id de un alquiler para construir una uri de contenido particular de la forma "alquileres/:id" por si requieres consultar cada item.
  • generarIdAlquiler(): Genera un identificador único para un alquiler basado en el estándar UUID.
  • obtenerIdAlquiler(): Extra el identificador de una uri particular de un alquiler.

#3 Crear Instancia De SQLiteOpenHelper

El siguiente paso es crear una nueva clase Java llamada BaseDatos.java que extienda de SQLiteOpenHelper. Luego sobrescribe los controladores onCreate() para crear la tabla 'alquiler' y añadir 8 registros de ejemplos. No olvides sobrescribir onUpgrade() con la eliminación de la tabla por defecto.

BaseDatos.java

ent.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.provider.BaseColumns;

import com.herprogramacion.alquileres.provider.Contrato.Alquileres;

/**
 * Clase auxiliar para controlar accesos a la base de datos SQLite
 */
public class BaseDatos extends SQLiteOpenHelper {

    static final int VERSION = 1;

    static final String NOMBRE_BD = "alquileres.db";


    interface Tablas {
        String APARTAMENTO = "alquiler";
    }

    public BaseDatos(Context context) {
        super(context, NOMBRE_BD, null, VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(
                "CREATE TABLE " + Tablas.APARTAMENTO + "("
                        + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
                        + Alquileres.ID_ALQUILER + " TEXT UNIQUE,"
                        + Alquileres.NOMBRE + " TEXT NOT NULL,"
                        + Alquileres.UBICACION + " TEXT NOT NULL,"
                        + Alquileres.DESCRIPCION + " TEXT NOT NULL,"
                        + Alquileres.PRECIO + " REAL NOT NULL,"
                        + Alquileres.URL_IMAGEN + " TEXT NOT NULL)");

        // Registro ejemplo #1
        ContentValues valores = new ContentValues();
        valores.put(Alquileres.ID_ALQUILER, Alquileres.generarIdAlquiler());
        valores.put(Alquileres.NOMBRE, "Inmejorable vivienda en Cali");
        valores.put(Alquileres.UBICACION, "Cali");
        valores.put(Alquileres.DESCRIPCION, "Apartamento amplio, cerca a centros comerciales, garaje");
        valores.put(Alquileres.PRECIO, "200");
        valores.put(Alquileres.URL_IMAGEN, "http://www.hermosaprogramacion.com/wp-content/uploads/2016/01/apto1.jpg");
        db.insertOrThrow(Tablas.APARTAMENTO, null, valores);

        // Registro ejemplo #2
        valores.put(Alquileres.ID_ALQUILER, Alquileres.generarIdAlquiler());
        valores.put(Alquileres.NOMBRE, "3 habitaciones en Villa Real");
        valores.put(Alquileres.UBICACION, "Barranquilla");
        valores.put(Alquileres.DESCRIPCION, "Casa buena, bonita y barata");
        valores.put(Alquileres.PRECIO, "240");
        valores.put(Alquileres.URL_IMAGEN, "http://www.hermosaprogramacion.com/wp-content/uploads/2016/01/apto2.jpg");
        db.insertOrThrow(Tablas.APARTAMENTO, null, valores);

        // Registro ejemplo #3
        valores.put(Alquileres.ID_ALQUILER, Alquileres.generarIdAlquiler());
        valores.put(Alquileres.NOMBRE, "Calle la dorada en Palmira");
        valores.put(Alquileres.UBICACION, "Palmira");
        valores.put(Alquileres.DESCRIPCION, "Apartamento en buen barrio, iglesia cercana, dos pisos");
        valores.put(Alquileres.PRECIO, "300");
        valores.put(Alquileres.URL_IMAGEN, "http://www.hermosaprogramacion.com/wp-content/uploads/2016/01/apto3.jpg");
        db.insertOrThrow(Tablas.APARTAMENTO, null, valores);

        // Registro ejemplo #4
        valores.put(Alquileres.ID_ALQUILER, Alquileres.generarIdAlquiler());
        valores.put(Alquileres.NOMBRE, "2 habitaciones en Nápoles");
        valores.put(Alquileres.UBICACION, "Cali");
        valores.put(Alquileres.DESCRIPCION, "Apartamento recien terminado, con terraza, 2 baños");
        valores.put(Alquileres.PRECIO, "325");
        valores.put(Alquileres.URL_IMAGEN, "http://www.hermosaprogramacion.com/wp-content/uploads/2016/01/apto4.jpg");
        db.insertOrThrow(Tablas.APARTAMENTO, null, valores);

        // Registro ejemplo #5
        valores.put(Alquileres.ID_ALQUILER, Alquileres.generarIdAlquiler());
        valores.put(Alquileres.NOMBRE, "Calle Sparta, Manizales");
        valores.put(Alquileres.UBICACION, "Manizales");
        valores.put(Alquileres.DESCRIPCION, "Barrio la Ceremonia, 3er piso + terraza");
        valores.put(Alquileres.PRECIO, "200");
        valores.put(Alquileres.URL_IMAGEN, "http://www.hermosaprogramacion.com/wp-content/uploads/2016/01/apto5.jpg");
        db.insertOrThrow(Tablas.APARTAMENTO, null, valores);

        // Registro ejemplo #6
        valores.put(Alquileres.ID_ALQUILER, Alquileres.generarIdAlquiler());
        valores.put(Alquileres.NOMBRE, "Calle La Costa - Lechería");
        valores.put(Alquileres.UBICACION, "Barranquilla");
        valores.put(Alquileres.DESCRIPCION, "86m2 en ubicación privilegiada");
        valores.put(Alquileres.PRECIO, "500");
        valores.put(Alquileres.URL_IMAGEN, "http://www.hermosaprogramacion.com/wp-content/uploads/2016/01/apto6.jpg");
        db.insertOrThrow(Tablas.APARTAMENTO, null, valores);

        // Registro ejemplo #7
        valores.put(Alquileres.ID_ALQUILER, Alquileres.generarIdAlquiler());
        valores.put(Alquileres.NOMBRE, "Manzanare Norte - Manzanares - Caracas - Baruta (sur)");
        valores.put(Alquileres.UBICACION, "Cali");
        valores.put(Alquileres.DESCRIPCION, "Parqueadero, piscina, vigilancia privada");
        valores.put(Alquileres.PRECIO, "540");
        valores.put(Alquileres.URL_IMAGEN, "http://www.hermosaprogramacion.com/wp-content/uploads/2016/01/apto7.jpg");
        db.insertOrThrow(Tablas.APARTAMENTO, null, valores);

        // Registro ejemplo #8
        valores.put(Alquileres.ID_ALQUILER, Alquileres.generarIdAlquiler());
        valores.put(Alquileres.NOMBRE, "Casa, alquiler");
        valores.put(Alquileres.UBICACION, "Cali");
        valores.put(Alquileres.DESCRIPCION, "Las Granjas, 2alcobas, 2baños,...");
        valores.put(Alquileres.PRECIO, "310");
        valores.put(Alquileres.URL_IMAGEN, "http://www.hermosaprogramacion.com/wp-content/uploads/2016/01/apto8.jpg");
        db.insertOrThrow(Tablas.APARTAMENTO, null, valores);

    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int i, int i1) {
        try {
            db.execSQL("DROP TABLE IF EXISTS " + Tablas.APARTAMENTO);
        } catch (SQLiteException e) {
            // Manejo de excepciones
        }
        onCreate(db);
    }
}

No te preocupes por las imágenes de cada alquiler, he guardado los archivos correspondientes en mi servidor para que no tengamos problemas.

#4 Crear ContentProvider Personalizado De Alquileres

El paso final para terminar el modelo es crear tu propio content provider personalizado con la capacidad de arrojar registros de la tabla alquiler.

Debido a que en este ejemplo no insertaremos, modificaremos ni eliminaremos, entonces podemos dejar los métodos insert(), update() y delete() escritos por defecto.

Por otro lado, query() si debe leer las uris de contenido de los alquileres y determinar si se consulta la colección completa o un registro único (usa la clase UriMatcher para determinar ambos casos).

Teniendo esto claro crea un nuevo content provider llamado ProviderAlquileres.java y define su cuerpo de la siguiente forma:

ProviderAlquileres.java

import android.content.ContentProvider;
import android.content.ContentResolver;
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;

import com.herprogramacion.alquileres.provider.BaseDatos.Tablas;
import com.herprogramacion.alquileres.provider.Contrato.Alquileres;


/**
 * {@link ContentProvider} que encapsula el acceso a la base de datos de apartamentos
 */
public class ProviderApartamentos extends ContentProvider {

    // Comparador de URIs
    public static final UriMatcher uriMatcher;

    // Casos
    public static final int ALQUILERES = 100;
    public static final int ALQUILERES_ID = 101;

    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(Contrato.AUTORIDAD, "alquileres", ALQUILERES);
        uriMatcher.addURI(Contrato.AUTORIDAD, "alquileres/*", ALQUILERES_ID);
    }

    private BaseDatos bd;
    private ContentResolver resolver;


    @Override
    public boolean onCreate() {
        bd = new BaseDatos(getContext());
        resolver = getContext().getContentResolver();
        return true;
    }

    @Override
    public String getType(Uri uri) {
        switch (uriMatcher.match(uri)) {
            case ALQUILERES:
                return Alquileres.MIME_COLECCION;
            case ALQUILERES_ID:
                return Alquileres.MIME_RECURSO;
            default:
                throw new IllegalArgumentException("Tipo desconocido: " + uri);
        }
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        // Obtener base de datos
        SQLiteDatabase db = bd.getWritableDatabase();
        // Comparar Uri
        int match = uriMatcher.match(uri);

        Cursor c;

        switch (match) {
            case ALQUILERES:
                // Consultando todos los registros
                c = db.query(Tablas.APARTAMENTO, projection,
                        selection, selectionArgs,
                        null, null, sortOrder);
                c.setNotificationUri(resolver, Alquileres.URI_CONTENIDO);
                break;
            case ALQUILERES_ID:
                // Consultando un solo registro basado en el Id del Uri
                String idApartamento = Alquileres.obtenerIdAlquiler(uri);
                c = db.query(Tablas.APARTAMENTO, projection,
                        Alquileres.ID_ALQUILER + "=" + "\'" + idApartamento + "\'"
                                + (!TextUtils.isEmpty(selection) ?
                                " AND (" + selection + ')' : ""),
                        selectionArgs, null, null, sortOrder);
                c.setNotificationUri(resolver, uri);
                break;
            default:
                throw new IllegalArgumentException("URI no soportada: " + uri);
        }
        return c;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }

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

#5 Definir Layout De La Actividad Principal

Paso 1. Por defecto, Android Studio ha creado una plantilla con una appbar, un contenido principal y un float action button como se muestra en el layout actividad_lista_alquileres.xml.

actividad_lista_alquileres.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.herprogramacion.alquileres.ActividadListaAlquileres">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/contenido_actividad_lista_alquileres" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:src="@drawable/icono_filtro" />

</android.support.design.widget.CoordinatorLayout>

Puedes cambiar el icono de fab a uno alusivo a un filtro. Un buen ejemplo es este elemento de Material Design Icons.

La sección contenido_actividad_lista_alquileres.xml la modificaremos para que contenga un nodo <RecyclerView> para representar la lista de alquileres.

contenido_actividad_lista_alquileres.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/lista"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingTop="@dimen/margen_superior_lista"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.herprogramacion.alquileres.ActividadListaAlquileres"
    tools:showIn="@layout/actividad_lista_alquileres" />

Paso 2. Ahora debes crear el diseño de cada ítem de la lista. Para ello puedes basarte en las siguientes indicaciones.

Ejemplo Card En Material Design

En esencia usé las especificaciones de tarjetas del material design para mantener una presentación fina y uniforme. Esto permite resaltar los atributos de cada alquiler y relacionarlos con una acción inferior hipotética de contacto.

La ilustración muestra una sección superior de media para la foto del alquiler, luego sigue el contenido compuesto por el nombre como título, el precio como subtítulo, la ubicación como texto auxiliar y la descripción como texto de soporte. Al final va una acción “CONTACTAR” para simular la comunicación con el arrendador.

A continuación te mostraré la configuración de views que usé dentro del layout para el ítem (llamado item_lista_alquiler.xml).

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginBottom="8dp"
    card_view:cardCornerRadius="@dimen/cardview_default_radius"
    card_view:cardElevation="@dimen/cardview_default_elevation"
    card_view:cardUseCompatPadding="true">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <!-- Media -->
        <ImageView
            android:id="@+id/foto"
            android:layout_width="match_parent"
            android:layout_height="@dimen/altura_imagen_item" />

        <!-- Cuerpo -->
        <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:paddingBottom="8dp"
            android:paddingLeft="@dimen/activity_horizontal_margin"
            android:paddingRight="@dimen/activity_horizontal_margin"
            android:paddingTop="24dp">


            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal">

                <TextView
                    android:id="@+id/nombre"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentLeft="true"
                    android:layout_toLeftOf="@+id/imageView"
                    android:paddingBottom="8dp"
                    android:text="Nombre"
                    android:textAppearance="@style/TextAppearance.AppCompat.Headline" />

                <ImageView
                    android:id="@+id/imageView"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignBottom="@+id/ubicacion"
                    android:layout_marginLeft="24dp"
                    android:layout_toLeftOf="@+id/ubicacion"
                    android:src="@drawable/icono_ubicacion" />

                <TextView
                    android:id="@+id/ubicacion"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignBaseline="@+id/nombre"
                    android:layout_alignParentRight="true"
                    android:layout_alignParentTop="false"
                    android:text="Ubicación" />
            </RelativeLayout>


            <TextView
                android:id="@+id/precio"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingBottom="@dimen/activity_vertical_margin"
                android:text="$ Precio"
                android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />

            <TextView
                android:id="@+id/descripcion"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." />

        </LinearLayout>

        <!-- Acciones -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:padding="8dp">

            <Button
                android:id="@+id/button"
                style="@style/Base.Widget.AppCompat.Button.Borderless.Colored"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Contactar" />
        </LinearLayout>
    </LinearLayout>
</android.support.v7.widget.CardView>

#6 Ligar RecyclerView Con Cursor

En esta sección veremos cómo crear el adaptador del Recycler View con un cursor que alimente la vista.

¿Que debes tener en cuenta?

  • Usa el método moveToPosition() dentro de onBindViewHolder() para acceder a la posición del cursor dependiendo del parámetro position.
  • Obtén la cantidad de ítems con el método getCount() del cursor.
  • Notifica que el cursor cambió con notifyDataSetChangeg().
  • Ten a la mano el índice de las columnas a consultar del cursor.

Con esto claro, no creo que tengas entiendo el siguiente código para el adaptador AdaptadorAlquileres.java:

import android.content.Context;
import android.database.Cursor;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import com.bumptech.glide.Glide;


/**
 * Adaptador con un cursor para poblar la lista de alquileres desde SQLite
 */
public class AdaptadorAlquileres extends RecyclerView.Adapter<AdaptadorAlquileres.ViewHolder> {
    private final Context contexto;
    private Cursor items;

    private OnItemClickListener escucha;

    interface OnItemClickListener {
        public void onClick(ViewHolder holder, int idPromocion);
    }

    public class ViewHolder extends RecyclerView.ViewHolder
            implements View.OnClickListener {
        // Referencias UI
        public TextView viewNombre;
        public TextView viewUbicacion;
        public TextView viewDescripcion;
        public TextView viewPrecio;
        public ImageView viewFoto;

        public ViewHolder(View v) {
            super(v);
            viewNombre = (TextView) v.findViewById(R.id.nombre);
            viewUbicacion = (TextView) v.findViewById(R.id.ubicacion);
            viewDescripcion = (TextView) v.findViewById(R.id.descripcion);
            viewPrecio = (TextView) v.findViewById(R.id.precio);
            viewFoto = (ImageView) v.findViewById(R.id.foto);
            v.setOnClickListener(this);
        }

        @Override
        public void onClick(View view) {
            escucha.onClick(this, obtenerIdAlquiler(getAdapterPosition()));
        }
    }

    private int obtenerIdAlquiler(int posicion) {
        if (items != null) {
            if (items.moveToPosition(posicion)) {
                return items.getInt(ConsultaAlquileres.ID_ALQUILER);
            } else {
                return -1;
            }
        } else {
            return -1;
        }
    }

    public AdaptadorAlquileres(Context contexto, OnItemClickListener escucha) {
        this.contexto = contexto;
        this.escucha = escucha;

    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_lista_alquiler, parent, false);
        return new ViewHolder(v);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        items.moveToPosition(position);

        String s;

        // Asignación UI
        s = items.getString(ConsultaAlquileres.NOMBRE);
        holder.viewNombre.setText(s);

        s = items.getString(ConsultaAlquileres.UBICACION);
        holder.viewUbicacion.setText(s);

        s = items.getString(ConsultaAlquileres.DESCRIPCION);
        holder.viewDescripcion.setText(s);

        s = items.getString(ConsultaAlquileres.PRECIO);
        holder.viewPrecio.setText(s);

        s = items.getString(ConsultaAlquileres.URL);
        Glide.with(contexto).load(s).centerCrop().into(holder.viewFoto);


    }

    @Override
    public int getItemCount() {
        if (items != null)
            return items.getCount();
        return 0;
    }

    public void swapCursor(Cursor nuevoCursor) {
        if (nuevoCursor != null) {
            items = nuevoCursor;
            notifyDataSetChanged();
        }
    }

    public Cursor getCursor() {
        return items;
    }

    interface ConsultaAlquileres {
        int ID_ALQUILER = 1;
        int NOMBRE = 2;
        int UBICACION = 3;
        int DESCRIPCION = 4;
        int PRECIO = 5;
        int URL = 6;
    }
}

Veamos de qué se tratan los métodos nuevos:

  • obtenerIdAlquiler(): Retorna en el valor de la columna "idAlquiler" de la posición actual. Este método es muy útil a la hora de leer los eventos de click y mostrar detalles.
  • swapCursor(): Intercambia el cursor actual por uno nuevo.
  • getCursor(): Retorna en el cursor actual para darle uso externo.

La interfaz de comunicación OnItemClickListener es un mecanismo para que la actividad o fragment escuche los clicks que escucha View.OnClickListener sobre los view holders. Con esta transición en cadena es posible saber el identificador y el view al que se clickeó a través de OnItemClickListener.onClick().

#7 Poblar Lista Con CursorLoader

Finalmente te queda obtener la instancia del recycler en la actividad para setear un LinearLayoutManager y el adaptador.

Para cargar la información con un loader es necesario que implementes la interfaz LoaderManager.LoaderCallback<Cursor> y sobrescribas los métodos onCreateLoader(), onLoadFinished() y onResetLoader().

Aunque el adaptador se asignará en onCreate() de la actividad, este no tendrá datos hasta que la carga de registros no esté lista, es decir, hasta cuando onLoadFinished() se ejecute. Justo allí debes usar el método swapCursor() para mostrar los resultados de la consulta.

También implementa AdaptadorAlquileres.OnItemClickListener sobre la actividad y sobrescribe el método onClick(). Aunque lo ideal sería cargar el detalle del ítem de la lista dentro de él, por ahora solo desplegaré un mensaje con el identificador del ítem.

Veamos:

ActividadListaAlquileres.java

import android.database.Cursor;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;

import com.herprogramacion.alquileres.provider.Contrato.Alquileres;

public class ActividadListaAlquileres extends AppCompatActivity implements AdaptadorAlquileres.OnItemClickListener, LoaderManager.LoaderCallbacks<Cursor> {

    private RecyclerView listaUI;
    private LinearLayoutManager linearLayoutManager;
    private AdaptadorAlquileres adaptador;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.actividad_lista_alquileres);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Filtro...", Snackbar.LENGTH_LONG)
                        .setAction("Acción", null).show();
            }
        });

        // Preparar lista
        listaUI = (RecyclerView) findViewById(R.id.lista);
        listaUI.setHasFixedSize(true);

        linearLayoutManager = new LinearLayoutManager(this);
        listaUI.setLayoutManager(linearLayoutManager);

        adaptador = new AdaptadorAlquileres(this, this);
        listaUI.setAdapter(adaptador);

        // Iniciar loader
        getSupportLoaderManager().restartLoader(1, null, this);

    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_actividad_lista_alquileres, menu);
        return true;
    }

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

        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onClick(AdaptadorAlquileres.ViewHolder holder, String idAlquiler) {
        Snackbar.make(findViewById(android.R.id.content), ":id = " + idAlquiler,
                Snackbar.LENGTH_LONG).show();
    }


    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        return new CursorLoader(this, Alquileres.URI_CONTENIDO, null, null, null, null);
    }

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

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
    }
}

Ahora solo ejecuta la aplicación y podrás desplegar los registros de SQLite sobre el RecyclerView y clickear cada ítem de la lista:

App Android Alquileres

Conclusión

El RecyclerView con Cursor es un caso supremamente popular en los desarrollos para mostrar la información de SQLite en la vista. Sin embargo debido a su alto nivel de personalización, este no trae consigo una implementación estándar para poblar views.

Pero de cierta forma esta característica es la que hace que este componente sea más flexible que un ListView y optimice más el uso de recursos.

Recuerda que aún puedes usar la clase CursorAdapter y SimpleCursorAdapter por si deseas seguir usando list views con cursores. Estos son muy efectivos cuando la lista es sencilla.