¿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:
[sociallocker id=»7121″]
1. ¿Qué es un Feed?
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 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:
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:
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.
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 <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.
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».
Ahora selecciona la opción «Singleton» y nombra la clase como VolleySingleton:
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 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.
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.
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:
Ahora presiona click derecho sobre el archivo y selecciona la opción «Add As Library…»:
Selecciona el módulo donde deseas refenciar su funcionamiento:
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.
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.
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 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); } }
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:
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