¿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.
Si sigues leyendo podrás obtener el siguiente resultado:
Ahora, si deseas descargar el proyecto de Android Studio completo para seguir paso a paso este tutorial, entonces sigue la siguiente instrucción:
Descargar Código
Desbloquea el código con una de las siguientes acciones sociales
Tweet
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 version, 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 articulo 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 De La Aplicación Android
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:
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 al parte inferior pondremos la miniatura del articulo junto al titulo de este. La línea divisoría 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 RESTfull 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.xmlAntes 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.
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 delos elementos de la lista.
titulo: Es el título de la entrada.
descripcion: Es el resumen de la entrada.
url: Enlace del articulo 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