Tutorial Para Crear Un Lector Rss En Android

¿Andas buscando como crear un lector rss, para incluir contenidos de un sitio web en tu aplicación Android?

¿Necesitas ideas para crear una app lectora de Rss como Feedly, Flipboard o Flyne?

Pues bien, en este tutorial verás cómo alimentar una lista de elementos con las noticias del sitio web forbes.com desde su feed con formato RSS a través de las tecnologías Volley y Simple Framework XML.

Descargar Proyecto Android Studio De Feedky

Si sigues leyendo podrás obtener el siguiente resultado:

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

1. ¿Qué es un Feed?

Icono Rss Flat
Lo primero que debes comprender antes de iniciar este tutorial es el significado de feed. Un feed es un origen (fuente) de difusión para contenidos web.

Ellos proveen un resumen y actualizaciones continuas sobre el contenido que se emiten regularmente. Esto con el fin de que otras plataformas de información puedan acceder a él y presentarlo.

Por otro lado se encuentran los formatos de redifusión, los cuales son un conjunto de definiciones formales en texto plano, que contienen la jerarquía de los contenidos en un feed.

Supongo que ya has escuchado que actualmente existen dos formatos muy populares para difundir contenidos: RSS y Atom.

RSS (Really Simple Syndication) es un formato de redifusión basado en XML para estructurar los datos más importantes de una fuente web. Atom es exactamente lo mismo, simplemente que usa otro tipo de convenciones en su estructura.

Actualmente se usa la versión RSS 2.0 y Atom 1.0. Las ventajas del uso de cada una no vienen al caso en este artículo, así que no las tendré en cuenta.

1.1 Estructura XML Del Formato RSS 2.0

Para poder convertir un flujo de información XML a objetos Java es imprescindible que comprendas la jerarquía y la sintaxis que usa RSS 2.0.

Por ejemplo…el archivo Rss del feed de Forbes tiene el siguiente aspecto:

<rss xmlns:atom="http://www.w3.org/2005/Atom" 
xmlns:dc="http://purl.org/dc/elements/1.1/" 
xmlns:media="http://search.yahoo.com/mrss/" 
xmlns:content="http://purl.org/rss/1.0/modules/content/" 
version="2.0">
    <channel>
        <link>http://www.forbes.com/most-popular/</link>
        <atom:link href="http://www.forbes.com/most-popular/feed" rel="self" type="application/rss+xml"/>
        <title>Forbes.com: Most popular stories</title>
        <description>Most popular stories from Forbes.com</description>
        <item>...</item>
        <item>...</item>
        <item>...</item>
        <item>...</item>
        <item>...</item>
        <item>...</item>
        <item>...</item>
        <item>...</item>
        <item>...</item>
        <item>...</item>
    </channel>
</rss>

La etiqueta raíz se denomina <rss>. Dentro de ella se incluye todo el contenido necesario para estructurar el contenido. Por obligación debe llevar el atributo versión, el cual representa la versión RSS, que comúnmente será "2.0".

La etiqueta <channel> representa una sección individual del feed por si el contenido web viene dividido en categorías. Algunos de sus elementos hijos son:

  • <title>: Es el nombre del feed. En mi caso elegí el canal Most popular stories (Historias más populares).
  • <link>: Contiene la url de la sección del canal.
  • <atom:link>: Contiene la url del feed.
  • <description>: Es una corta descripción del feed.

En su interior también encontraremos las etiquetas <item>. Estas son las que más nos interesan y también las que más trabajo nos darán a la hora de tratar información.

Veamos algunas de las etiquetas hijas de <item> que con frecuencia encontrarás:

  • <title>: Representa el título del artículo o noticia.
  • <description>: Se trata de un resumen introductorio del ítem generalmente representado por la metaetiqueta html description.
  • <link>: Es la url original del ítem tratado.
  • <pubDate>: Fecha en que se publicó el artículo.
  • <guid>: Un identificador único del ítem. En el ejemplo es la misma url.
  • <enclosure>: Representa un elemento multimedia incluido en el ítem.

Sin embargo habrá definiciones Rss que implementen namespaces para soportar módulos especiales que complementen las características de un elemento.

Por ejemplo, “http://search.yahoo.com/mrss/” representa al módulo Media RSS que es similar a la etiqueta <enclosure>, pero trae muchas más características que puedes indicar en un elemento multimedia.

Incluso si ves, se usa el namespace atom para acceder a la convención de los elementos del formato Atom.

2. Requerimientos Del Lector Rss

Antes del desarrollo veamos un poco sobre las características que debe tener la aplicación:

  • Como usuario de Feedky, deseo que la aplicación tenga una lista de artículos compuestos por el título, la descripción y una miniatura que lo acompañe.
  • Como usuario de Feedky, deseo ver en detalle el artículo que seleccioné en la lista.

La solución al primer comportamiento ya la hemos trabajado antes. Sabes que para la lista podemos usar la clase ListView o RecyclerView y para el detalle.

En cambio la visualización del contenido del artículo sin salir de nuestra aplicación requiere de un nuevo layout llamado WebView, el cual veremos en la fase de desarrollo.

3. Wireframing De La Aplicación Android

Analizando el alcance que tiene la aplicación notamos que solo existen dos actividades. La primera es la actividad principal donde veremos una lista de artículos y la segunda tiene el detalle del ítem seleccionado.

Solo basta con una interacción de toque del usuario para viajar de una actividad a otra:
Wireframing De Una Aplicación Android Lectora Rss

¿Quieres reducir el tiempo en creación de interfaz? Te recomiendo descargar la plantilla Universal – Full Multi-Purpose Android App de Codecanyon.

4. Creación De UI Para La Aplicación Android

El siguiente paso es construir las definiciones XML de los layouts para nuestra interfaz. Hasta el momento se pueden percibir tres layouts: La actividad principal, el diseño de los ítems de la lista y el de la actividad de detalle.

4.1 Diseñar Layout De La Actividad Principal

La actividad principal requiere el uso de una lista a través de un ListView. A continuación dirígete al layout de tu actividad principal (para mí es activity_main.xml) y añade como nodo raíz una etiqueta <ListView>:
activity_main.xml

<ListView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/lista"
    android:divider="@null"
    android:dividerHeight="0dp"
    android:background="#F1F5F8"
    android:padding="6dp"/>

Como viste en el video inicial, hubo un diseño de cards para los ítems, por lo que nuestro ListView no debe contener líneas divisorias entre ellos. Para eliminarlas setea @null al drawable del divisor con android:divider y reduce la altura a 0dp con android:dividerHeight.

4.2 Crear Layout De La Actividad Detalle

La actividad de detalle simplemente representa el contenido web del artículo que se ha seleccionado en la actividad principal.

Esta característica es bien cubierta por un WebView. Un tipo especial de layout que renderiza páginas web bajo la tecnología del motor open source WebKit.

Para implementar su definición XML se usa la etiqueta <WebView> de la siguiente manera:
activity_detail.xml

<WebView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/webview"/>

4.3 Crear Layout Personalizado De Los Items

El diseño de los ítems lo haremos en forma de fragmento enriquecido como se ve en la siguiente imagen:

Layout Personalizado Para Items Del ListView
En la parte superior añadiremos el ícono de Forbes junto a la palabra “Forbes”. En la sección del medio ubicaremos la descripción de la entrada. Y en la parte inferior pondremos la miniatura del artículo junto al título de este. La línea divisoria es opcional, pero si eres sofisticado puedes dejarla.

La idea es usar como raíz un Card View con un Relative Layout en su interior para la distribución de los elementos. Recuerda incluir la dependencia de los cards.

item_layout.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"
    card_view:cardCornerRadius="2dp"
    card_view:cardElevation="2dp"
    card_view:cardUseCompatPadding="true">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="16dp">

        <!-- MINIATURA -->
        <com.android.volley.toolbox.NetworkImageView
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:id="@+id/imagen"
            android:scaleType="centerCrop"
            android:layout_alignParentStart="true"
            android:layout_below="@+id/linea"
            android:layout_marginTop="16dp" />

        <!-- TITULO -->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceSmall"
            android:text="Título"
            android:id="@+id/titulo"
            android:layout_marginBottom="10dp"
            android:layout_toEndOf="@+id/imagen"
            android:layout_alignTop="@+id/imagen"
            android:layout_marginStart="16dp" />

        <!-- DESCRIPCION -->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceSmall"
            android:text="Descripción"
            android:id="@+id/descripcion"
            android:layout_marginBottom="16dp"
            android:layout_below="@+id/icon"
            android:layout_marginTop="16dp" />

        <!-- LINEA DIVISORIA -->
        <View
            android:layout_width="wrap_content"
            android:layout_height="1dp"
            android:id="@+id/linea"
            android:background="#ffe9e9e9"
            android:layout_below="@+id/descripcion" />

        <!-- ICONO FORBES-->
        <ImageView
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:id="@+id/icon"
            android:layout_alignParentTop="true"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:src="@drawable/forbes" />

        <!-- MARCA FORBES -->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceSmall"
            android:text="Forbes"
            android:id="@+id/publisher"
            android:layout_toEndOf="@+id/icon"
            android:textStyle="bold"
            android:layout_marginStart="16dp" />
    </RelativeLayout>
</android.support.v7.widget.CardView>

5. Arquitectura De La Aplicación Android

Antes de codificar he creado un bosquejo sobre los componentes que debemos coordinar para que nuestra aplicación funcione con un buen patrón de diseño.
Debido a que nuestra aplicación debe realizar una petición HTTP hacia el servidor de Forbes para obtener los recursos xml y luego presentar dicha información al usuario, puedes considerar un diseño Modelo Vista Controlador de Red.
Modelo Vista Controlador De Red En Android
El diagrama muestra como desde la actividad Home o principal realizamos una petición con Volley hacia la web, la cual enviará una respuesta que será almacenada en SQLite. Luego de ello se actualiza la vista.

Adicionalmente desde Home el controlador de eventos estará pendiente para mostrar el detalle de cada elemento en la actividad de Detalle.

Sería ideal usar restricciones del estilo RESTful para manejar las peticiones desde el modelo, pero hasta el momento no hemos hablado de los temas necesarios para ello.

Es importante resaltar que el modelo MVC se queda corto debido a que no usaremos un patrón de observación para la sincronización en tiempo real de datos.

El diagrama muestra que usaremos una base de datos local SQLite para simular una especie de Caching, la cual permitirá retener los datos consultados y tenerlos como base para actualizar el contenido cada vez que se inicie la aplicación.

Para completar por excelencia el MVC de Red junto a las prácticas REST, necesitamos usar un ContentProvider junto a un SyncAdapter. Pero estos serán temas que veremos en próximos artículos.

6. Codificación De La Aplicación

Bueno, ya sabemos que elementos debemos construir para darle forma a Feedky. Si todo ha salido bien, hasta el momento tu proyecto en Android Studio debe tener los siguientes materiales:

  • Clase MainActivity.java
  • Layout activity_main.xml
  • Clase DetailActivity.java
  • Layout activity_detail.xml
  • Layout item_layout.xml

Antes de comenzar es importante añadir el permiso de conexiones a internet y el de estado de red en el Android Manifest:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

Vamos a usar Volley para la gestión de peticiones HTTP así que incorpórala al proyecto de la forma que desees. En mi caso la añado como un módulo adicional.

Cuando ya estén listas las condiciones anteriores, entonces pasamos a codificar cada paso de funcionamiento.

Paso #1: Crear La Base De Datos SQLite

Antes de pensar en realizar una petición es necesario contar con nuestro almacenamiento local.
Es lógico que en cuanto a diseño conceptual de bases de datos, solo se necesita la entidad Entrada. Con esa tabla aseguraremos los datos del feed.

Base De Datos Lector Rss En Android

Así que debemos buscar que nuestra Contract Class o Script de base la base de datos implemente el siguiente comando:

CREATE TABLE entrada (
_ID INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT, 
descripcion TEXT, 
url TEXT,
thumb_url TEXT);

La tabla posee las columnas respectivas para representar el contenido de los elementos de la lista.

  • titulo: Es el título de la entrada.
  • descripción: Es el resumen de la entrada.
  • url: Enlace del artículo para visualizar su detalle.
  • thumb_url: Url de la miniatura (thumbnail).

Con estas condiciones tu script quedaría de la siguiente forma:

import android.provider.BaseColumns;

/**
 * Creado por Hermosa Programación
 *
 * Clase que representa un script restaurador del estado inicial de la base de datos
 */

public class ScriptDatabase {
    /*
    Etiqueta para Depuración
     */
    private static final String TAG = ScriptDatabase.class.getSimpleName();

    // Metainformación de la base de datos
    public static final String ENTRADA_TABLE_NAME = "entrada";
    public static final String STRING_TYPE = "TEXT";
    public static final String INT_TYPE = "INTEGER";

    // Campos de la tabla entrada
    public static class ColumnEntradas {
        public static final String ID = BaseColumns._ID;
        public static final String TITULO = "titulo";
        public static final String DESCRIPCION = "descripcion";
        public static final String URL = "url";
        public static final String URL_MINIATURA = "thumb_url";
    }

    // Comando CREATE para la tabla ENTRADA
    public static final String CREAR_ENTRADA =
            "CREATE TABLE " + ENTRADA_TABLE_NAME + "(" +
                    ColumnEntradas.ID + " " + INT_TYPE + " primary key autoincrement," +
                    ColumnEntradas.TITULO + " " + STRING_TYPE + " not null," +
                    ColumnEntradas.DESCRIPCION + " " + STRING_TYPE + "," +
                    ColumnEntradas.URL + " " + STRING_TYPE + "," +
                    ColumnEntradas.URL_MINIATURA + " " + STRING_TYPE +")";



}

Ahora extenderemos la clase SQLiteOpenHelper para crear nuestro administrador de bases de datos. Aquí incluiremos tres métodos para operaciones vitales: La inserción de filas, la modificación y la obtención de todos los elementos de la tabla entrada:

FeedDatabase.java

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

import com.herprogramacin.hermosaprogramacion.RssParse.Item;

import java.util.HashMap;
import java.util.List;

/**
 * Creado por Hermosa Programación.
 *
 * Clase que administra el acceso y operaciones hacia la base de datos
 */

public final class FeedDatabase extends SQLiteOpenHelper {

    // Mapeado rápido de indices
    private static final int COLUMN_ID = 0;
    private static final int COLUMN_TITULO = 1;
    private static final int COLUMN_DESC = 2;
    private static final int COLUMN_URL = 3;

    /*
    Instancia singleton
    */
    private static FeedDatabase singleton;

    /*
    Etiqueta de depuración
     */
    private static final String TAG = FeedDatabase.class.getSimpleName();


    /*
    Nombre de la base de datos
     */
    public static final String DATABASE_NAME = "Feed.db";

    /*
    Versión actual de la base de datos
     */
    public static final int DATABASE_VERSION = 1;


    private FeedDatabase(Context context) {
        super(context,
                DATABASE_NAME,
                null,
                DATABASE_VERSION);

    }

    /**
     * Retorna la instancia unica del singleton
     *
     * @param context contexto donde se ejecutarán las peticiones
     * @return Instancia
     */
    public static synchronized FeedDatabase getInstance(Context context) {
        if (singleton == null) {
            singleton = new FeedDatabase(context.getApplicationContext());
        }
        return singleton;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        // Crear la tabla 'entrada'
        db.execSQL(ScriptDatabase.CREAR_ENTRADA);


    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // Añade los cambios que se realizarán en el esquema
        db.execSQL("DROP TABLE IF EXISTS " + ScriptDatabase.ENTRADA_TABLE_NAME);
        onCreate(db);
    }

    /**
     * Obtiene todos los registros de la tabla entrada
     *
     * @return cursor con los registros
     */
    public Cursor obtenerEntradas() {
        // Seleccionamos todas las filas de la tabla 'entrada'
        return getWritableDatabase().rawQuery(
                "select * from " + ScriptDatabase.ENTRADA_TABLE_NAME, null);
    }

    /**
     * Inserta un registro en la tabla entrada
     *
     * @param titulo      titulo de la entrada
     * @param descripcion desripcion de la entrada
     * @param url         url del articulo
     * @param thumb_url   url de la miniatura
     */
    public void insertarEntrada(
            String titulo,
            String descripcion,
            String url,
            String thumb_url) {

        ContentValues values = new ContentValues();
        values.put(ScriptDatabase.ColumnEntradas.TITULO, titulo);
        values.put(ScriptDatabase.ColumnEntradas.DESCRIPCION, descripcion);
        values.put(ScriptDatabase.ColumnEntradas.URL, url);
        values.put(ScriptDatabase.ColumnEntradas.URL_MINIATURA, thumb_url);

        // Insertando el registro en la base de datos
        getWritableDatabase().insert(
                ScriptDatabase.ENTRADA_TABLE_NAME,
                null,
                values
        );
    }

    /**
     * Modifica los valores de las columnas de una entrada
     *
     * @param id          identificador de la entrada
     * @param titulo      titulo nuevo de la entrada
     * @param descripcion descripcion nueva para la entrada
     * @param url         url nueva para la entrada
     * @param thumb_url   url nueva para la miniatura de la entrada
     */
    public void actualizarEntrada(int id,
                                  String titulo,
                                  String descripcion,
                                  String url,
                                  String thumb_url) {

        ContentValues values = new ContentValues();
        values.put(ScriptDatabase.ColumnEntradas.TITULO, titulo);
        values.put(ScriptDatabase.ColumnEntradas.DESCRIPCION, descripcion);
        values.put(ScriptDatabase.ColumnEntradas.URL, url);
        values.put(ScriptDatabase.ColumnEntradas.URL_MINIATURA, thumb_url);

        // Modificar entrada
        getWritableDatabase().update(
                ScriptDatabase.ENTRADA_TABLE_NAME,
                values,
                ScriptDatabase.ColumnEntradas.ID + "=?",
                new String[]{String.valueOf(id)});

    }

}

Como ves insertarEntrada(), actualizarEntrada() y obtenerEntradas() representan las operaciones necesitadas.
También puedes usar un patrón singleton para generalizar el asistente de bases de datos y acceder a el desde una sola instancia, por eso es que ves el método getInstance() y el constructor privado.

Android Studio provee una plantilla para crear un singleton. Fíjate como lo hacemos con Volley que también implementa este estilo de diseño…

Paso #2: Crear Patrón Singleton Para Volley

Para crear un nuevo singleton que limite la propagación de Volley debes dar click derecho en tu paquete java y seleccionar “Java Class”.

Crear Nueva Clase En Android Studio
Ahora selecciona la opción “Singleton” y nombra la clase como VolleySingleton:

Nueva Clase De Tipo Singleton En Android Studio
Recuerda que necesitamos implementar una cola de peticiones y un image loader para la descarga de imágenes. Al final la clase quedaría de esta forma:

VolleySingleton.java

import android.content.Context;
import android.graphics.Bitmap;
import android.support.v4.util.LruCache;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.Volley;

/**
 * Creado por Hermosa Programación.
 *
 * Clase que representa un cliente HTTP Volley
 */

public final class VolleySingleton {

    // Atributos
    private static VolleySingleton singleton;
    private ImageLoader imageLoader;
    private RequestQueue requestQueue;
    private static Context context;


     private VolleySingleton(Context context) {
        VolleySingleton.context = context;
        requestQueue = getRequestQueue();

        imageLoader = new ImageLoader(requestQueue,
                new ImageLoader.ImageCache() {
                    private final LruCache<String, Bitmap>
                            cache = new LruCache<>(40);

                    @Override
                    public Bitmap getBitmap(String url) {
                        return cache.get(url);
                    }

                    @Override
                    public void putBitmap(String url, Bitmap bitmap) {
                        cache.put(url, bitmap);
                    }
                });
    }

    /**
     * Retorna la instancia unica del singleton
     * @param context contexto donde se ejecutarán las peticiones
     * @return Instancia
     */
    public static synchronized VolleySingleton getInstance(Context context) {
        if (singleton == null) {
            singleton = new VolleySingleton(context.getApplicationContext());
        }
        return singleton;
    }

    /**
     * Obtiene la instancia de la cola de peticiones
     * @return cola de peticiones
     */
    public RequestQueue getRequestQueue() {
        if (requestQueue == null) {
            requestQueue = Volley.newRequestQueue(context.getApplicationContext());
        }
        return requestQueue;
    }

    /**
     * Añade la petición a la cola
     * @param req petición
     * @param <T> Resultado final de tipo T
     */
    public <T> void addToRequestQueue(Request<T> req) {
        getRequestQueue().add(req);
    }

    public ImageLoader getImageLoader() {
        return imageLoader;
    }

}

Paso #3: Generar Un Parser XML Para El Feed RSS

A mi parecer este es el núcleo del problema que estamos asumiendo para crear nuestro lector Feedky. Los otros temas ya los hemos tratado en artículos anteriores, pero el parsing XML es nuevo.

¿Recuerdas cuando vimos parsing de formatos JSON?

A través de una clase auxiliar leíamos el flujo de los objetos JSON, donde identificábamos los atributos importantes y los convertíamos en objetos Java para utilizarnos en nuestras listas.

Es exactamente eso mismo lo que tienes que hacer con las etiquetas XML. La duda está en que clase o librería debemos usar para el parseo de los elementos del feed. En palabras simples lo que necesitamos es pasar de una jerarquía XML a objetos Java.

Parsing XML a Java en Android La documentación de Android Developers tiene un ejemplo práctico que parsea un feed Atom con una librería llamada XMLPULL, la cual contiene una clase principal llamada XmlPullParser que permite la lectura de los elementos XML de un flujo de datos.

Dentro de las librería de Java podemos encontrar otra clase llamada SAXParser para el parsing XML muy útil también por si deseas darle un vistazo.

Similar a JsonReader, XmlPullParser tiene métodos para obtener etiquetas, atributos, namespaces y contenidos CDATA. Pero a mí en particular no me gusta implementar largas clases en bajo nivel para extraer datos, es por eso que te contaré de una librería excelente de parsing que encontré…

LA LIBRERÍA SIMPLE PARA SERIALIZACIÓN XML

La librería Simple es una poderosa herramienta tanto para serializar elementos XML como para deserializarlos como objetos Java. Nos entrega un sistema de anotaciones que facilita tremendamente la descripción de los objetos que referenciarán las etiquetas XML.

Realmente deja por un alto nivel el parsing y podemos ahorrar mucho tiempo de desarrollo. Incluir Simple Framework XML en Android Studio Para incluirla en Android Studio debes descargar el paquete de distribución 2.7.1 de Simple.

Página De Descarga Simple Framework XML

Extraes el contenido del archivo .rar y luego te diriges a la carpeta “jar”. Una vez allí, copia y pega el archivo simple-xml-2.7.1.jar dentro de la carpeta “libs” de tu módulo principal:

Carpeta libs De Android Studio

Ahora presiona click derecho sobre el archivo y selecciona la opción “Add As Library…”:

Add As Library En Android Studio

Selecciona el módulo donde deseas refenciar su funcionamiento:

Choose App Module en Android Studio

Con ello tendremos a nuestra orden las características de Simple Framework XML a nuestra disposición. ¿Cómo parsear archivos XML con Simple? La documentación completa sobre el uso puedes verla en la sección “Tutorial” del sitio web oficial.

No obstante voy a resumirte las características de deserialización que necesitamos usar. La forma de establecer que etiquetas nos interesa obtener y en qué tipo de organización se determina a través de las anotaciones de referencia.

La idea es establecer con ellas que clases representan las etiquetas, cuales son hijas, que atributos tienen, si es necesario obtener varios elementos, etc. Algunas de las más frecuentes son:

  • @Root: Representa el equivalente Java de un objeto XML.
  • @Attribute: Referencia el atributo de un elemento XML en un objeto Java.
  • @Element: Se usa para representar un elemento hijo de una etiqueta XML.
  • @ElementList: Se refiere a una lista de elementos hijos del mismo tipo y características.

a. Etiquetas xml: Para indicar que una clase es el equivalente a una etiqueta xml basta con ubicar la anotación @Root en la parte superior de su encabezado. Por ejemplo…

@Root(name = "rss", strict = false)
public class Rss {
...
}

En el caso anterior se crea la clase Rss para representar a la etiqueta <rss> del formato.
Para la anotación @Root puedes especificar dos parámetros: name y strict.

Donde name es el nombre de la etiqueta XML y strict indica al framework si requerimos deserializar todos los elementos hijos y atributos de la etiqueta en nuestra clase.

Para name usaremos la cadena “rss”. Para strict usaremos false, ya que <rss> tiene atributos que no deseamos reflejar en nuestra clase.

b. Elementos hijos: Usa la anotación @Element si quieres declarar un elemento como hijo de otro. Por ejemplo la clase Rss debe contener un objeto Channel como hijo:

// Dentro de Rss
@Element
private Channel channel;

También podemos especificar una serie de atributos:

  • data: Determina si el elemento se encuentra dentro de un bloque CDATA o no.
  • name: El nombre del elemento hijo.
  • required: Especifica si el valor del elemento es obligatorio o no.
  • type: Es el tipo de dato del valor del elemento.

c. Listas de elementos: Si deseas indicar que un elemento contiene una lista usa la anotación @ElementList. Un buen ejemplo de esto sería donde la clase Channel contiene una lista de elementos Item como vimos en la jerarquía Rss.

Channel.java

import org.simpleframework.xml.ElementList;
import org.simpleframework.xml.Root;

import java.util.List;

/**
 * Creado por Hermosa Programación.
 *
 * Clase que representa la etiqueta <channel> del feed
 */

@Root(name = "channel", strict = false)
public class Channel {


    @ElementList(inline = true)
    private List<Item> items;

    public Channel() {
    }

    public Channel(List<Item> items) {
        this.items = items;
    }

    public List<Item> getItems() {
        return items;
    }
}

El parámetro inline le dice al framework que <channel> no contiene únicamente la lista de elementos <item>, si no que existen otros elementos distintos.
Si indicas true el framework ignorará los elementos distintos de la lista. Siendo false el valor por defecto.

d. Namespaces: Ya habíamos dicho que en ocasiones los formatos Rss tendrán namespacecs que representan módulos de extensión para la representación de datos detallados sobre algún elemento.

Es por ello que debemos emplear la anotación @Namespace para satisfacer este tipo de jerarquías.

Por ejemplo…

La etiqueta <media:content> implementa un namespace para la descripción de elementos multimedia de cada entrada del feed. Sabemos que esta tiene un atributo con el valor de la url de la miniatura, por ende necesitamos su lectura.

La implementación del namespace se declara en el nodo <rss>, así justo allí debemos usar una anotación @Namespace:

Rss.java

import org.simpleframework.xml.Element;
import org.simpleframework.xml.Namespace;
import org.simpleframework.xml.Root;

/**
 * Creado por Hermosa Programación
 *
 * Clase que representa al elemento <rss> del feed
 */

@Root(name = "rss", strict = false)
@Namespace(reference="http://search.yahoo.com/mrss/")
public class Rss {


    @Element
    private Channel channel;

    public Rss() {
    }

    public Rss(Channel channel) {
        this.channel = channel;
    }

    public Channel getChannel() {
        return channel;
    }
}

Debajo de @Root indicas el namespace. La referencia la relacionas con el parámetro reference. En este caso el valor es la URI del módulo Media.

Sin embargo ahora debes declarar el prefijo del elemento que represente la etiqueta <media:content> en la clase Item:

Item.java

import org.simpleframework.xml.Element;
import org.simpleframework.xml.Namespace;
import org.simpleframework.xml.Root;

/**
 * Creado por Hermosa Programación.
 *
 * Clase que representa la etiqueta <item> del feed
 */

@Root(name = "item", strict = false)
public class Item {

    @Element(name="title")
    private String title;

    @Element(name = "description")
    private String descripcion;

    @Element(name="link")
    private String link;

    @Element(name="content")
    @Namespace(reference="http://search.yahoo.com/mrss/", prefix="media")
    private Content content;



    public Item() {
    }

    public Item(String title, String descripcion, String link, Content content) {
        this.title = title;
        this.descripcion = descripcion;
        this.link = link;
        this.content = content;
    }

    public String getTitle() {
        return title;
    }

    public String getDescripcion() {
        return descripcion;
    }

    public String getLink() {
        return link;
    }

    public Content getContent() {
        return content;
    }
}

Como ves la clase Content representa la etiqueta con el namespace. Simplemente usamos la anotación @Namespace incluyendo el parámetro prefix con el valor del prefijo “media”.

e. Atributos: El archivo Rss casi no contiene atributos que nos interese en nuestras etiquetas, salvo el atributo url de la etiqueta <media:content>. Para extraerlo simplemente marca una variable con la anotación @Attribute.

Content.java

import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.Root;

/**
 * Creado por Hermosa Programación.
 *
 * Clase que representa la etiqueta <media:content> del feed
 */

@Root(name="content", strict = false)
public class Content {

    @Attribute(name="url")
    private String url;

    public Content() {
    }

    public Content(String url) {
        this.url = url;
    }

    public String getUrl() {
        return url;
    }
}

e. La clase Serializer: La librería Simple usa su clase principal Serializer para la representación de un elemento XML que puede ser serializados o deserializados.

Aunque no podemos instanciarla directamente, se usa una clase llamada Persister para crear una instancia que permita otorgar persistencia a los datos. Persister implementa una gran cantidad métodos de lectura y escritura de datos XML dependiendo de la fuente y tipo de datos.

Si descargaste el feed para tener un acceso local, puedes usar el método read de la clase Serializer de la siguiente forma:

Serializer serializer = new Persister();
File source = new File("ruta/carpeta/rss.xml");
Rss rss = serializer.read(Rss.class, source);

Este método recibe el tipo de elemento con que será deserializado el archivo xml, el cual tiene una referencia en el objeto source de tipo File.

No obstante también puedes cargarlo desde un flujo de datos InputStream. Pero eso lo veremos en el paso siguiente…

Paso #4: Crear Una Petición Personalizada XML Con Volley

Ahora el turno es para nuestra petición HTTP. Sabemos que podemos usar el cliente HttpURLConnection para dicho propósito, pero como bien sabes, Volley automatiza gran parte del trabajo.

Similar a la petición personalizada para formatos JSON que se creóo como ejemplo en el artículo de Volley, debemos derivar nuestra petición de la clase Request<T>.

XmlRequest.java

import android.util.Log;

import com.android.volley.AuthFailureError;
import com.android.volley.NetworkResponse;
import com.android.volley.ParseError;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.toolbox.HttpHeaderParser;

import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;
import java.io.UnsupportedEncodingException;
import java.util.Map;

/**
 * Creado por Hermosa Programación.
 *
 * Petición personalizada para el trato de flujos XML
 */

public class XmlRequest<T> extends Request<T> {

    private static final String TAG = XmlRequest.class.getSimpleName();

    // Atributos
    private final Class<T> clazz;
    private final Map<String, String> headers;
    private final Response.Listener<T> listener;
    private final Serializer serializer = new Persister();

    /**
     * Se predefine para el uso de peticiones GET
     */
    public XmlRequest(String url, Class<T> clazz, Map<String, String> headers,
                      Response.Listener<T> listener, Response.ErrorListener errorListener) {
        super(Method.GET, url, errorListener);
        this.clazz = clazz;
        this.headers = headers;
        this.listener = listener;
    }

    @Override
    public Map<String, String> getHeaders() throws AuthFailureError {
        return headers != null ? headers : super.getHeaders();
    }

    @Override
    protected void deliverResponse(T response) {
        listener.onResponse(response);
    }

    @Override
    protected Response<T> parseNetworkResponse(NetworkResponse response) {
        try {

            // Convirtiendo el flujo en cadena con formato UTF-8
            String xml = new String(response.data, "UTF-8");

            // Depurando...
            Log.d(TAG, xml);

            // Enviando la respuesta parseada
            return Response.success(
                   serializer.read(clazz, xml),
                    HttpHeaderParser.parseCacheHeaders(response));

        } catch (UnsupportedEncodingException e) {
            return Response.error(new ParseError(e));
        } catch (Exception e) {
            e.printStackTrace();
            return Response.error(new ParseError(e));
        }
    }
}

Al momento de enviar la respuesta con parseNetworkResponse() vemos que el flujo que viene de response es convertido a String y tomado con read(). Esto retornará directamente en un objeto java del tipo clazz, que en nuestro caso es Rss.

Paso #5: Enviar Petición Al Servidor De Forbes

El envío de la petición para obtener el formato XML se a través del método addRequestQueque() de nuestro singleton Volley.

¿Pero dónde debes invocarlo?

Bueno esta elección depende mucho de la arquitectura MVC de Red. Lo ideal es que el modelo haga las consultas hacia el servidor para generar un caching inmediato y no atrofiar nuestro hilo principal. Aquí un Content Provider nos vendría muy bien.

Sin embargo es posible hacerlo en la vista o el controlador siempre y cuando sea en segundo plano.
También es importante definir la forma en que se observará la actualización de los datos de la base de datos para que la lista se refresque.

Ahora, el caching de los datos simples es realizado sobre SQLite, pero… ¿cómo hacer el caching de las imágenes? Eso no tiene problema, Volley es el encargado de gestionar esto por ti.

Realizar petición XML desde la actividad principal
Añadiremos la nueva petición a la cola de peticiones de Volley en el método onCreate() de MainActivity. La idea es crear un método que procese la respuesta a la petición y almacene la información en la base de datos:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    // Obtener la lista
    listView = (ListView) findViewById(R.id.lista);

    VolleySingleton.getInstance(this).addToRequestQueue(
            new XmlRequest<>(
                    URL_FEED,
                    Rss.class,
                    null,
                    new Response.Listener<Rss>() {
                        @Override
                        public void onResponse(Rss response) {
                            // Caching
                            FeedDatabase.getInstance(MainActivity.this).
                                    sincronizarEntradas(response.getChannel().getItems());
                            // Carga inicial de datos...

                        }
                    },
                    new Response.ErrorListener() {
                        @Override
                        public void onErrorResponse(VolleyError error) {
                            Log.d(TAG, "Error Volley: " + error.getMessage());
                        }
                    }
            )
    );

}

Realizar caching de la información

La respuesta obtenida de la petición debe ser inmediatamente almacenada en nuestra base de datos. Para ello se creó el método sincronizarEntradas(), el cual procesa la lista de ítems arrojados.

/**
 * Procesa una lista de items para su almacenamiento local
 * y sincronización.
 *
 * @param entries lista de items
 */
public void sincronizarEntradas(List<Item> entries) {
    /*
    #1  Mapear temporalemente las entradas nuevas para realizar una
        comparación con las locales
    */
    HashMap<String, Item> entryMap = new HashMap<String, Item>();
    for (Item e : entries) {
        entryMap.put(e.getTitle(), e);
    }

    /*
    #2  Obtener las entradas locales
     */
    Log.i(TAG, "Consultar items actualmente almacenados");
    Cursor c = obtenerEntradas();
    assert c != null;
    Log.i(TAG, "Se encontraron " + c.getCount() + " entradas, computando...");

    /*
    #3  Comenzar a comparar las entradas
     */
    int id;
    String titulo;
    String descripcion;
    String url;

    while (c.moveToNext()) {

        id = c.getInt(COLUMN_ID);
        titulo = c.getString(COLUMN_TITULO);
        descripcion = c.getString(COLUMN_DESC);
        url = c.getString(COLUMN_URL);

        Item match = entryMap.get(titulo);
        if (match != null) {
            // Filtrar entradas existentes. Remover para prevenir futura inserción
            entryMap.remove(titulo);

            /*
            #3.1 Comprobar si la entrada necesita ser actualizada
            */
            if ((match.getTitle() != null && !match.getTitle().equals(titulo)) ||
                    (match.getDescripcion() != null && !match.getDescripcion().equals(descripcion)) ||
                    (match.getLink() != null && !match.getLink().equals(url))) {
                // Actualizar entradas
                actualizarEntrada(
                        id,
                        match.getTitle(),
                        match.getDescripcion(),
                        match.getLink(),
                        match.getContent().getUrl()
                );

            }
        }
    }
    c.close();

    /*
    #4 Añadir entradas nuevas
    */
    for (Item e : entryMap.values()) {
        Log.i(TAG, "Insertado: titulo=" + e.getTitle());
        insertarEntrada(
                e.getTitle(),
                e.getDescripcion(),
                e.getLink(),
                e.getContent().getUrl()
        );
    }
    Log.i(TAG, "Se actualizaron los registros");


}

Este método es el encargado de guardar en la base de datos todas las entradas que tiene el feed a partir de la lista que ingresa como parámetro.

Como ves en los comentarios se establecieron 4 pasos que marcan su recorrido. Lo primero fue mapear las entradas nuevas en una nuevo conjunto cuya clave es el título de la entrada. Se eligió el título debido a que representará su identificador.

Luego se obtuvieron las entradas locales existentes en la base de datos. Esto permitirá realizar un cotejamiento entre ambos grupos. Donde se irá filtrando las entradas duplicadas al realizar el recorrido.

Si la entrada existe pero tuvo algún cambio en su estructura, se actualiza su contenido a través del método actualizarEntrada().

Una vez terminada la comparación, se almacenan aquellas entradas que aún permanecen en el mapa, las cuales no existen todavía.

Realizar caching de las imágenes
Aunque volley provee un almacenamiento en caché basado en la clase DiskBaseCache, las respuestas están sometidas a las directivas que el servidor externo ha establecido.

Es decir, si el servidor ha declarado que sus recursos expiran en 30 minutos, no esperes que las imágenes permanezcan un tiempo mayor a esa cantidad.

O incluso si las cabeceras de control de cache indican que no debe almacenarse el flujo, entonces no tendrás en ningún momento la miniatura almacenada.

¿Cómo mantener en caché las imágenes?

Bueno, existen varias librerías que pueden ser de ayuda para almacenar nuestras miniaturas en el disco local. Una de ellas es Android Universal Image Loader, la cual te permite descargar las imágenes, darles persistencia en cache y visualizarlas de forma optimizada.

Ejemplo Aplicación Android Con Librería Universal Loader
La librería Picasso también es una excelente opción. Al igual que universal, te permite almacenar en cache las imágenes, además de tener una curva de aprendizaje muy corta.

Ejemplo Aplicación Android Con Librería Picasso
Ahora si no deseas irte tan lejos, puedes escribir tu propia definición de cache local con la ayuda de Jake Wharton y su implementación de caching.

No obstante, la solución que voy a implementar para este tutorial se basa en la modificación de la misma librería Volley.

¿Has visto el funcionamiento del método parseNetworkResponse() en las peticiones?

Bien, ese método cuando retornar la respuesta con el método success() usa como parámetro las cabeceras HTTP que el servidor ha enviado.

Para parsear las cabeceras que vienen en la respuesta existe la clase HttpHeaderParser, la cual compara las etiquetas de cada cabecera y extrae sus valores correspondientes.

Es justo allí donde se origina la duración de nuestras imágenes y su disposición de caching a través del método estático parseCacheHeaders().

Ahora… ¿qué tal si alteramos este método o creamos uno nuevo para que los valores de las cabeceras sean ignoradas?

En esta discusión sobre la alteración de las cabeceras HTTP que recibe Volley se explica una vía para manejar esta situación donde se ignoran los resultados de duración.

Simplemente debemos crear un nuevo método llamado parseIgnoreCacheHeaders() y llamarlo en la clase ImageRequest, que es la petición que usa ImageLoader.

Veamos:

// Dentro de HttpHeaderParse...
public static Cache.Entry parseIgnoreCacheHeaders(NetworkResponse response) {
    long now = System.currentTimeMillis();

    Map<String, String> headers = response.headers;
    long serverDate = 0;
    String serverEtag = null;
    String headerValue;

    headerValue = headers.get("Date");
    if (headerValue != null) {
        serverDate = HttpHeaderParser.parseDateAsEpoch(headerValue);
    }

    serverEtag = headers.get("ETag");

    final long cacheHitButRefreshed = 3 * 60 * 1000; // 3 minutos disponible ante las operaciones
    final long cacheExpired = 24 * 60 * 60 * 1000; // expira en 24 horas
    final long softExpire = now + cacheHitButRefreshed;
    final long ttl = now + cacheExpired;

    Cache.Entry entry = new Cache.Entry();
    entry.data = response.data;
    entry.etag = serverEtag;
    entry.softTtl = softExpire;
    entry.ttl = ttl;
    entry.serverDate = serverDate;
    entry.responseHeaders = headers;

    return entry;
}

Ahora ve al método doParse() de la clase ImageRequest (el cual es encargado de parsear el flujo a bitmap) y haz que el método success de la respuesta implemente nuestro nuevo método:

return Response.success(bitmap, HttpHeaderParser.parseIgnoreCacheHeaders(response));

La forma en que sabes si funciona o no es corriendo la aplicación para que se carguen las miniaturas. Luego de ello desconecta la conexión a internet, cierra la aplicación ábrela de nueva. Si todas las miniaturas aparecen, entonces fue un éxito.

Desconozco la efectividad funcional de este método. Aún debe ser probado con el tracking de Volley para ver los tiempos de respuesta y hitting de la cache. No obstante es funcional y fácil de implementar.

Paso #6: Crear Un CursorAdapter Personalizado Para La Lista

Debido a que la lista se puebla directamente desde el contenido de la base de datos, es necesario derivar nuestro adaptador de la clase CursorAdapter para recorrer los registros.

Usaremos un patrón de diseño View Holder para optimizar las llamadas de findViewById() en nuestro adaptador. Las imágenes las obtendremos a través de las peticiones del ImageLoader y así guardarlas automáticamente en caché:

FeedAdapter.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.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.NetworkImageView;
import com.herprogramacin.hermosaprogramacion.Modelo.ScriptDatabase;
import com.herprogramacin.hermosaprogramacion.R;
import com.herprogramacin.hermosaprogramacion.Web.VolleySingleton;

/**
 * Creado por Hermosa Programación
 *
 * Adaptador para inflar la lista de entradas
 */
public class FeedAdapter extends CursorAdapter {

    /*
    Etiqueta de Depuración
     */
    private static final String TAG = FeedAdapter.class.getSimpleName();

    /**
     * View holder para evitar multiples llamadas de findViewById()
     */
    static class ViewHolder {
        TextView titulo;
        TextView descripcion;
        NetworkImageView imagen;

        int tituloI;
        int descripcionI;
        int imagenI;
    }

    public FeedAdapter(Context context, Cursor c, int flags) {
        super(context, c, flags);

    }

    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());

        View view = inflater.inflate(R.layout.item_layout, null, false);

        ViewHolder vh = new ViewHolder();

        // Almacenar referencias
        vh.titulo = (TextView) view.findViewById(R.id.titulo);
        vh.descripcion = (TextView) view.findViewById(R.id.descripcion);
        vh.imagen = (NetworkImageView) view.findViewById(R.id.imagen);

        // Setear indices
        vh.tituloI = cursor.getColumnIndex(ScriptDatabase.ColumnEntradas.TITULO);
        vh.descripcionI = cursor.getColumnIndex(ScriptDatabase.ColumnEntradas.DESCRIPCION);
        vh.imagenI = cursor.getColumnIndex(ScriptDatabase.ColumnEntradas.URL_MINIATURA);

        view.setTag(vh);

        return view;
    }

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

        final ViewHolder vh = (ViewHolder) view.getTag();

        // Setear el texto al titulo
        vh.titulo.setText(cursor.getString(vh.tituloI));

        // Obtener acceso a la descripción y su longitud
        int ln = cursor.getString(vh.descripcionI).length();
        String descripcion = cursor.getString(vh.descripcionI);

        // Acortar descripción a 150 caracteres
        if (ln >= 150)
            vh.descripcion.setText(descripcion.substring(0, 150)+"...");
        else vh.descripcion.setText(descripcion);

        // Obtener URL de la imagen
        String thumbnailUrl = cursor.getString(vh.imagenI);

        // Obtener instancia del ImageLoader
        ImageLoader imageLoader = VolleySingleton.getInstance(context).getImageLoader();

        // Volcar datos en el image view
        vh.imagen.setImageUrl(thumbnailUrl, imageLoader);

    }
}

Si te fijas bien, el view holder almacena también el índice de las columnas del cursor para evitar su obtención múltiples veces.

También hemos añadido una restricción para el tamaño de la descripción de 150 caracteres. Y hemos usado un NetworkImageView para asignar las imágenes a través del image loader.

Paso #7: Poblar La Lista Asíncronamente

El siguiente paso es declarar todas las instancias globales dentro de nuestra actividad principal para proyectar los elementos de la interfaz. Con ello podremos crear una tarea asíncrona que consulte todos los registros de la base de datos en segundo plano sin alterar el main thread.

Aunque las tareas asíncronas son excelentes para los trabajos en segundo plano, estás no alcanzan a satisfacer el registro de un observer. Por el momento no hemos visto la clase CursorLoader para la gestión de operaciones con la base de datos, pero es la ideal para este tipo de procesos.

La tarea asíncrona debe cargar los datos en un cursor con el método obtenerEntradas() y luego presentar estos datos ante el adaptador que será asociado a la lista:

public class LoadData extends AsyncTask<Void, Void, Cursor> {

    @Override
    protected Cursor doInBackground(Void... params) {
        // Carga inicial de registros
        return FeedDatabase.getInstance(MainActivity.this).obtenerEntradas();

    }

    @Override
    protected void onPostExecute(Cursor cursor) {
        super.onPostExecute(cursor);

        // Crear el adaptador
        adapter = new FeedAdapter(
                MainActivity.this,
                cursor,
                SimpleCursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);

        // Relacionar la lista con el adaptador
        listView.setAdapter(adapter);
    }
}

Luego echas a andar la tarea asíncrona en onCreate(), cuando sincronizarEntradas() se haya llevado a cabo.

Antes debes asegurarte de que la conexión a internet está disponible. Recuerda que esto lo averiguas con el administrador de conexiones ConnectivityManager:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // Obtener la lista
    listView = (ListView)findViewById(R.id.lista);

    ConnectivityManager connMgr = (ConnectivityManager)
            getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
    if (networkInfo != null && networkInfo.isConnected()) {
        VolleySingleton.getInstance(this).addToRequestQueue(
                new XmlRequest<>(
                        URL_FEED,
                        Rss.class,
                        null,
                        new Response.Listener<Rss>() {
                            @Override
                            public void onResponse(Rss response) {
                                // Caching
                                FeedDatabase.getInstance(MainActivity.this).
                                        sincronizarEntradas(response.getChannel().getItems());
                                // Carga inicial de datos...
                                new LoadData().execute();
                            }
                        },
                        new Response.ErrorListener() {
                            @Override
                            public void onErrorResponse(VolleyError error) {
                                Log.d(TAG, "Error Volley: " + error.getMessage());
                            }
                        }
                )
        );
    } else {
        Log.i(TAG, "La conexión a internet no está disponible");
        adapter= new FeedAdapter(
                this,
                FeedDatabase.getInstance(this).obtenerEntradas(),
                SimpleCursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
        listView.setAdapter(adapter);
    }

}

Paso #8: Visualizar Las Entradas En La Actividad Detalle

Ahora solo queda usar intents explícitos para visualizar el contenido de la URL del ítem que el usuario presiona en la lista. Esto significa que al momento de asignar la escucha OnItemClickListener a la lista debemos usar el método startActivity(), donde añadiremos como valor extra la url de la entrada seleccionada. Veamos cómo hacerlo:

// Registrar escucha de la lista
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        Cursor c = (Cursor) adapter.getItem(position);

        // Obtene url de la entrada seleccionada
        String url = c.getString(c.getColumnIndex(ScriptDatabase.ColumnEntradas.URL));

        // Nuevo intent explícito
        Intent i = new Intent(MainActivity.this, DetailActivity.class);

        // Setear url
        i.putExtra("url-extra", url);

        // Iniciar actividad
        startActivity(i);
    }
});

Como bien sabes getItem() permite obtener la instancia de la fuente de datos que ha sido seleccionada por el usuario. Al hacer un casting a Cursor podemos conseguir la columna URL y así construir nuestro intent exitosamente.

Ahora simplemente recupera el valor de la url desde el lado de la actividad de detalle y carga el contenido de la url sobre el WebView:

DetailActivity.java

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import com.herprogramacin.hermosaprogramacion.R;

/**
 * Creado por Hermosa Programación
 *
 * Actividad que muestra el detalle de un articulo del feed
 */

public class DetailActivity extends AppCompatActivity{

    /*
    Etiqueta de depuración
     */
    private static final String TAG = DetailActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_detail);

        // Dehabilitar titulo de la actividad
        if(getSupportActionBar()!=null)
                getSupportActionBar().setDisplayShowTitleEnabled(false);


        // Recuperar url
        String urlExtra = getIntent().getStringExtra("url-extra");

        // Obtener WebView
        WebView webview = (WebView)findViewById(R.id.webview);

        // Habilitar Javascript en el renderizado
        webview.getSettings().setJavaScriptEnabled(true);

        // Transmitir localmente
        webview.setWebViewClient(new WebViewClient());

        // Cargar el contenido de la url
        webview.loadUrl(urlExtra);


    }

}

Finalmente ejecuta el proyecto Feedky y prueba su funcionamiento:
Aplicación Lectora De Feeds En Android

Conclusiones

Recuerda que existen dos estándares muy difundidos para la difusión de contenidos de una web llamados RSS y Atom. Dependiendo de la fuente de origen, así mismo se deben elegir las etiquetas correctas para el parsing.

Usa la librería Simple Framework para ahorrar tiempo de parsing XML. Aunque existen alternativas como XmlPullParser y SAXParser propias de Android, estas requieren una descripción de bajo nivel, mayor mantenimiento y reutilización compleja.

Con Volley puedes crear una petición personalizada para parsear y deserializar los flujos XML con una simplicidad asombrosa.

Aunque en este artículo no se implementó una sincronización con patrón observador, es necesario hacer uso de clases como SyncAdapter, ContentProvider y Service para completar el proceso (temas que serán explicados en futuros artículos).

Fuentes: Icono de la aplicación

  • cheperobert

    Muchas gracias, me has ayudado mucho a prender.

    Lo he implementado y tengo dos consultas
    1. Tengo la actividad principal, donde tengo tab’s y desde un tab llamo mi RSS, en un Fragment, en esta parte (cuando la conexion no esta disponible) no me funciona el contexto (es que no se como aplicarlo).

    } else {
    Log.i(ELOG, “La conexion a internet no esta disponible”);
    adapter= new FeedAdapter(
    this,
    miDB.getInstancia(—contexto—).obtenerEntradas(),
    SimpleCursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
    listView.setAdapter(adapter);

    2. Mi sitio RSS no tiene un namespace para las imagenes, y solo hay imagenes en la itiqueta

    <![CDATA[

    a traves de etiquteas "img", no se si con la librerias Simple XML, se pude hacer algo para obtener los datos de este Item y luego ver como obtengo la image. Por el momento he elimnado la parate de la imagen.

    Muchas gracias por tus Toriales

  • cheperobert

    Muchas gracias, me has ayudado mucho a prender.

    Lo he implementado y tengo dos consultas
    1. Tengo la actividad principal, donde tengo tab’s y desde un tab llamo mi RSS, en un Fragment, en esta parte (cuando la conexion no esta disponible) no me funciona el contexto (es que no se como aplicarlo).

    } else {
    Log.i(ELOG, “La conexion a internet no esta disponible”);
    adapter= new FeedAdapter(
    this,
    miDB.getInstancia(—contexto—).obtenerEntradas(),
    SimpleCursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
    listView.setAdapter(adapter);

    2. Mi sitio RSS no tiene un namespace para las imagenes, y solo hay imagenes en la itiqueta

    <![CDATA[

    a traves de etiquteas "img", no se si con la librerias Simple XML, se pude hacer algo para obtener los datos de este Item y luego ver como obtengo la image. Por el momento he elimnado la parate de la imagen.

    Muchas gracias por tus Toriales

  • Pingback: SIMPLE XML, empty value parsing description – Android Apps Development()

  • Fer Nan Do

    Hola James , me gustaría saber como puedo hacer un app que pueda estar alimentado por contenidos de una pagina de servicios de Facebook, tiene que ver con utilizar algun api de Facebook ? saludos

  • Gon Her

    Necesito una mano. Tengo un textView en mi layout. esto es de prueba para que me habra caminos. a este TextView necesito setear una columna de tal tabla.
    Por ejemplo en mi tx.setText necesito cargar el titulo del primer registro basandome en el _ID 1
    Por favor ayuda. Un saludo

  • Facundo

    Hola, muy buen post, quisiera preguntarte una cosa tengo dos rss que quiero ver como puedo agregar otro canal rss asi veo los dos en el mismo lugar?

    • Hola Facundo, gracias por comentar.

      Púes depende de la vista que desees tener, ya sea si usarás una lista, pestañas, un nav drawer, etc, solo debes enviar una petición para cada elemento y recibir los datos como hicimos con el ejemplo de este artículo.

  • Muy buenas!
    Éste es el tutorial más complejo y completo que he encontrado en la web, enhorabuena.
    A pesar de mi desconocimiento del entorno Android, he sido capaz de llevarlo a cabo e implementarlo en la aplicación que estoy desarrollando para el Proyecto de Fin de Curso de mi grado.
    El problema es que tengo asociado el feed a un botón en mi APP y cada vez que accedo al mismo sólo he conseguido que aparezcan los datos de la primera vez que ejecuté tu código, o dicho de otra manera, mi RSS no se actualiza.
    ¿Alguna idea?
    Gracias de antemano.

  • NeedSe Ns

    Alguien que me ayude con este feed porfavor http://feeds.feedburner.com/elandroidelibre/qJkz?format=xml

    • ¿Cuál es el problema con el compañero?, fíjate que si ves el código tiene exactamente la misma estructura solo que se añade un namespace feedburner, pero de resto esta todo

  • NeedSe Ns

    Como hago para ver los articulos offline?

  • NeedSe Ns

    Alguien que me ayude con este feed porfavor, no me funciona http://feeds.feedburner.com/elandroidelibre/BnjZ

    error Error Volley: org.simpleframework.xml.core.PersistenceException: Element ‘content’ is already used with @org.simpleframework.xml.Element(data=true, name=content, required=true, type=void) on field ‘content’ private com.herprogramacin.hermosaprogramacion.RssParse.Content com.herprogramacin.hermosaprogramacion.RssParse.Item.content at line 55

  • NeedSe Ns

    Porfavor ayudenme a hacer que me muestre las entradas del feed en orden descendente, estuve tranado pero no puedo. ¿Alguien que ayude porfavor?

  • NeedSe Ns

    Porfavor ayudenme a hacer que me muestre las entradas del feed en orden descendente, estuve tranado pero no puedo. ¿Alguien que ayude porfavor?

    • Jaime Tellez

      hola amigo, pudiste resolverlo? tengo el mismo problema, me muestra lo más reciente al final del listView! y no encuentro como invertir el orden. gracias por la ayuda!

      • NeedSe Ns

        Lo resolví, tienes que ir al feeddata creo que sellama y buscar donde dice obtener entradas y antes de null agregar + ” order by “+ScripDataBase…Id+” Desc”.
        En los puntos suspensivos va la extension hacia el Id , no recuerdo cual es , lo agregas tú, y es importante los espacios antes y despues del order by tal como lo puse igual para el desc. Espero te sirva

        • Jaime Tellez

          muchas gracias amigo, lo intentare!

    • Gon Her

      Yo tambien tenia ese problema. Resuelto:

      return getWritableDatabase().rawQuery(

      “select * from ” + Script.ENTRADA_TABLE_NAME + ” ORDER BY _ID DESC”, null);

      }

      ASC/DESC

  • Antonio Miguélez

    Hola, me gustaría saber cómo hacer para meter el lector RSS dentro de un Fragment. Simplemente creando una clase que extiende de Fragment e inflandola con el layout creado aquí para la Main no va (aparece, pero no carga el feed) y si intento poner el código de la Main en el Fragment, me da error. ¿Cómo debo hacerlo para que funcione?

  • Facu

    Hola, muy buen post, quisiera preguntarte una cosa tengo dos rss que quiero ver como puedo agregar otro canal rss asi veo los dos en el mismo lugar?

  • Jaime Tellez

    hola amigo, me gustaria saber si este tipo de rss funcina: https://ufpso.edu.co/rss/rss-novedades.php , o si hay que hacer algun tipo de modificacion, ya que al ponerlo donde va la url del feed no me sale nada. gracias

    • Si claro que funciona, solo debes elegir las etiquetas que deseas y usar los namespaces que se indican allí.

      Hay campos que parecen estar, por lo que debes quitarlos del ejemplo.

  • andres cando

    amigo porfavor ayuda me habré el proyecto y todo me habre la app y se queda en blanco porque??

  • Marcio David

    Hola amigo tiene que cambiar algo más o simplemente el enlace rss ??

    gracias !!

  • ds

    No me funciona por más que intento. Según el logcat, ya se está recuperando la información de las etiquetas xml, pero al llegar a la etiqueta content, se corta todo el contenido de ésta. Después aparece este error
    11-03 10:35:55.495 W/System.err(4801): org.simpleframework.xml.core.ValueRequiredException: Unable to satisfy @org.simpleframework.xml.Element(data=false, name=content, required=true, type=void) on field ‘content’ private com.darksilk.lectornoticias.RssParse.Content com.darksilk.lectornoticias.RssParse.Item.content for class com.darksilk.lectornoticias.RssParse.Item at line 12

    Y después una serie errores que hace referencia a algo mal en volley…

    Tampoco estoy seguro si el rss que pretendo usar es válido pues el formato luce terrible… http://www.utj.edu.mx/index.php/noticias?format=feed&type=rss

  • Gianmarco Bendezu Cheglio

    Me sale este error Error:(81, 95) error: incompatible types: List cannot be converted to List
    En mi caso mi activity principal es otro.

  • Manuel SG

    baje el proyecto, quise cambiarle la url por la de mi feed y no se muestra nada, me marca 11 errores.. :(

    • Eso te sale en la compilación de Android Studio o en el logcat?

      • Manuel SG

        android studio

        • Parece que está molestando el encodificado que tienes por defecto en tu Android Studio. Creo que yo uso UTF-8. Intenta cambiar el encodificado a este valor y prueba.

  • Juan

    Como puedo poner el lector RSS en un fragment?? se puede?

    • Claro Juan. Solo crea una nueva clase que extienda de Fragment e integra parte del layout de la actividad en el layout de este. Luego inflalo en onCreateView() y así.

      • Juan

        Gracias! Logre hacer que funcione dentro de un fragment, mi otra duda ahora es como cambio el servidor del feed, en este caso es forbes, pero cuando cambio la pagina del URL_FEED = “web..”, el feed de la pagina web nueva que ponga no se muestra

        • Tal vez sucede porque ese feed no tiene algunos campos que estamos leyendo del de forbes. Revisa los atributos de las etiquetas y modifica las clases para ajustar a lo que necesitas

          • Juan

            Hola! pude adaptar mi feed al lector de la app, mi problema ahora es que las noticias nuevas se cargan al final del listview, hay alguna manera de poder invertir la lista? y que las noticias nuevas aparezcan primeras ??

          • Has intentando usando el método Collections.sort() para organizar elementos?

            Eso ya es programación compañero, tal vez podrías idearte un método para ordenar los elementos en forma descendente.

        • Jhair Ibarra

          podrias explicar un poco mas esto pues me interesa usarlo en un fragment pero no soy nuevo en este tema de android

        • Jhair Ibarra

          podrias explicar un poco mas esto pues me interesa usarlo en un fragment pero no soy nuevo en este tema de android

      • Jhair Ibarra

        podrias explicar un poco mas esto pues me interesa usarlo en un fragment pero no soy nuevo en este tema

      • Jhair Ibarra

        podrias explicar un poco mas esto pues me interesa usarlo en un fragment pero no soy nuevo en este tema

    • Jaime Tellez

      hola amigo, me gustaria saber si este tipo de rss funcina: https://ufpso.edu.co/rss/rss-novedades.php , o si hay que hacer algun tipo de modificacion, ya que al ponerlo donde va la url del feed no me sale nada. gracias

  • Ricardo Rodrigo González

    Muchas gracias por el Tutorial, muy completo. Me pondré manos a la obra para añadir este lector a mi aplicación.

    Saludos

  • Fabian Cisneros Bridon

    Hola!!!

    Ante todo gracias por el tutoríal que esta muy bueno.

    Como sidiux3 estoy tratando de agarrar otro feed pero tengo un problemita.

    He visto el ficher XML de forbes y el de mi sitio. Los items tienen prácticamente la misma estructura, o sea, titulo, url, description y contenido. Lo único que el contenido de mi XML esta en un bloque CDATA. Si entendí bien en tu tutorial, yo debo agregar lo siguiente al element content:

    @Element(name=”content”, data = true)
    @Namespace(reference=”http://search.yahoo.com/mrss/”, prefix=”media”)
    private Content content;

    Mi XML no tiene imágenes, y creo haber comentado las lineas de código que se refieren a eso.

    Pero bueno, no funciona, no se lee nada.

    He olvidado otra cosa?? En fin, creo que estoy un poco perdido…

    Te agradeceria una ayuda.

    • Hola Fabian.

      ¿Estás usando el namespace “media”?, si no es así, quita esa línea.

      • Fabian Cisneros Bridon

        Hola,

        Ya la habia quitado luego de haberte escrito y nada de nada.

        • Que error ves en el log cat?

          • Fabian Cisneros Bridon

            James,

            Estoy un poco cogido con el tiempo y he encontrado otra forma de hacerlo, cierto, menos bonita que la tuya, pero mas acorde con la premura de lo que quiero hacer.
            No desmayo tu opción porque me encanta lo que has hecho. Te mantengo al tanto, ok??
            Gracias!!!

          • Ok Fabian.

            Fíjate que precisamente estoy haciendo un tutorial sobre parsing xml con pull. Voy a incluir un ejemplo con CDATA, espero te pueda servir para luego.

            Saludos amigo.

          • Jose Quintana

            Hola James, gracias por tus tutoriales nos ayudan mucho a todos.

            Siguiendo con lo que comentó Fabian, estoy tratando de adaptar el feeder a WordPress cuyos elementos descripcion y content estan en CDATA, mi código quedó asi:

            @Root(name = “item”, strict = false)
            public class Item {

            @Element(name=”title”)
            private String title;

            @Element(name = “description”, data=true)
            private String descripcion;

            @Element(name=”link”)
            private String link;

            //content:encoded es la forma en la que aparece en el rss de wordpress

            @Element(name=”content:encoded”, data=true)
            @Namespace(reference=”http://search.yahoo.com/mrss/”, prefix=”media”)

            private Content content;

            Y este es el error que obtengo en el logcat:
            Error Volley: org.simpleframework.xml.core.ValueRequiredException: Unable to satisfy @org.simpleframework.xml.Element(data=true, name=content:encoded, required=true, type=void) on field ‘content’ private com.example.ricardo.takeit.RssParse.Content com.example.ricardo.takeit.RssParse.Item.content for class com.example.ricardo.takeit.RssParse.Item at line 29

            Se te ocurre algo? gracias desde ya.

  • sidiux3

    yo tengo la duda de como hacer para que agarre otro feed que no sea el de forbes soy nuevo en esto y no se como hacerlo jajaja =)

  • Randy Douglas

    Buen dia!
    Una pregunta, soy nuevo en desarrollo en Android y no entendi bien esta parte, el codigo que esta antes de esta parte del FeedDatabase.java, el script para crear la DB, en donde meto ese archivo, es el Feed.db?
    Saludos!

  • Buenas tardes
    Lo primero agradeceros este tutorial, es el más completo que he encontrado hasta ahora sobre como desarrollar un lector RSS.
    Quería consultaros ¿Como se puede hacer para que la lista muestre los item ordenados por fecha? es decir la entrada más actual arriba y a continuación las siguientes entradas de más reciente a más antigua.
    En este momento las entradas se muestran en la lista de una manera aleatoria, sin orden concreto.
    Un saludo y gracias de antemano

    • James Revelo

      Hola amigo. Puedes usar el comando ORDER BY en el método obtenerEntradas(). Algo como:

      public Cursor obtenerEntradas() {
      // Seleccionamos todas las filas de la tabla ‘entrada’
      return getWritableDatabase().rawQuery(
      “select * from ” + ScriptDatabase.ENTRADA_TABLE_NAME+”ORDER BY “+ScriptDatabase.ColumnEntradas.PUBFECH, null);
      }

      Donde PUBFECH es la fecha de la publicación que se obtiene a través de la etiqueta del archivo Rss. Obviamente debes hacer las extensiones necesarias para procesar este campo.

      Saludos!

      • Gracias por tu respuesta

        Me lo había planteado ya usar el pubDate pero el parseo y el formatter me echaron para atrás.
        Al final le metido un item más al RSS llamado (estoy con el RSS de un website wordpress, que cada entrada, tiene un post id más elevado que la anterior entrada) y así he hecho el ORDER BY de ScriptDatabase.ColumnEntradas.POSTID y para que las muestre de más nueva a más antigua, le puesto el DESC.

        Vuelvo a darte las gracias por el tutorial, me ha servido de mucho, un saludo.

        • James Revelo

          Que bien que hayas solucionado tu inconveniente. Saludos :D

  • Jose Javier

    Hola, gracias por este tutorial, necesito ayuda con esto>

    new XmlRequest(
    URL_FEED, ///esta parte me marca error, dice que el simbolo URL_FEED no puede se resuelto.
    Rss.class,
    null,

    • James Revelo

      Hola.

      ¿Estas usando la librería desde github?

  • facontact

    Excelente tutorial! Gracias a esto hice algo parecido con el RSS de un diario, pero sucede que cada vez que inicio la aplicación las nuevas noticias se agregan mas abajo, necesito una ayuda enorme! Cómo hago para que las noticias del día de ayer (por ejemplo) no se vean, y se vean solo las nuevas noticias??? Te agradecería enormemente.

  • Rene

    excelentes tutoriales:
    quisera saber como se puede generar un rss propio, y dentro de la etiqueta item tener atributos personalizados. ¿es posible eso?. y ademas, ¿el rss residiria en un servidor de mi propiedad o existen paginas donde alojarlo?

  • Diego Cardenas

    Hola, esta muy bueno tu tutorial, pero quisiera saber como harias tu FeedAdapter si no trabajarias con una lista, sino con RecyclerView.Adapter.
    no encuentro la forma :/ se agradece la ayuda! :D

  • FreddyASG

    Saludos.

    Amigo podrías explicarme para que es el uso de esta linea de código:

    private static final String TAG = DetailActivity.class.getSimpleName();

    Gracias…

    • James Revelo

      Hola Freddy. Ese código es para obtener el nombre de la clase que estás referenciando. En ese caso retornaría el string “DetailActivity”, así cuando uso el método Log.d(), puedo debuggear en el logcat resultados que voy obteniendo.

  • Waykyky

    Excelente tutorial, tan solo un problema, me perdí un poco ya que estoy usando master detail xD
    alguna sugerencia?

    • James Revelo

      Hola Waykyky.

      Sugerencia en cuanto a que aspecto? ¿la creación del maestro detalle?

      Saludos!

  • Nino

    Perfectamente explicado :)

    Se podría hacer sin Sqlite?

    • James Revelo

      Me alegra que te haya gustado. :D

      Claro se puede realizar sin sqlite. Solo cambias el adaptador e insertas directamente la lista de elementos en él.

      Saludos!

      • NINO

        Cuando hablas de la lista te refieres a la clase Item? estoy intentando reproducirlo en un Master Detail y no consigo ver de donde sacar la lista.

        • James Revelo

          Hola NINO. Bueno lo que digo es que el adaptador debe ser modificado para que reciba objetos java en lugar de un cursor.