Tutorial De Parsing Xml En Android Con XmlPullParser

En este artículo veremos cómo implementar parsing XML en Android con la librería XML Pull Parsing.

Para ello construiremos un ejemplo de parsing con un archivo XML alojado en el servidor de Hermosa Programación. Analizaremos como leer etiquetas, atributos y prefijos.

Además incluiremos todos los elementos dentro de una lista creada con un Recycler View y veremos su detalle en otra actividad.

Descargar Proyecto Android Studio De “Guía De Hoteles En Roma”

Si te estás preguntando que resultado habrá luego de seguir este tutorial, entonces este video ilustrativo habla por si solo:

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

Archivo XML Con Datos De Hoteles

En esta ocasión usaremos datos sobre los mejores hoteles en Roma. La idea es crear una aplicación Android que proyecte los elementos de cada hotel en una lista.

Puedes obtener el formato XML desde el siguiente enlace:

http://ejemplos.hermosaprogramacion.com/hoteles-roma/hoteles.xml

Si visualizas si contenido verás la siguiente jerarquía XML:

hoteles.xml

<?xml version="1.0" encoding="utf-8"?>
<hoteles xmlns:datosWeb="http://ejemplos.hermosprogramacion.com/hoteles-roma">
    <hotel>
        <idHotel>1</idHotel>
        <nombre>Nerva Boutique Hotel</nombre>
        <precio>145</precio>
        <valoracion calificacion="5.0" noOpiniones="420" />
        <urlImagen>
            http://ejemplos.hermosaprogramacion.com/hoteles-roma/nerva-boutique-hotel.jpg
        </urlImagen>
        <datosWeb:descripcion>
            <![CDATA[ El mejor hotel de Roma ]]>
        </datosWeb:descripcion>
    </hotel>
    <hotel>
        <idHotel>2</idHotel>
        <nombre>Vatican View</nombre>
        <precio>155</precio>
        <valoracion calificacion="4.5" noOpiniones="230" />
        <urlImagen>
            http://ejemplos.hermosaprogramacion.com/hoteles-roma/vatican-view.jpg
        </urlImagen>
        <datosWeb:descripcion>
            <![CDATA[ Disfruta de la mejores noches en Roma ]]>
        </datosWeb:descripcion>
    </hotel>
    <hotel>
        <idHotel>3</idHotel>
        <nombre>Barcelo Aran Park</nombre>
        <precio>210</precio>
        <valoracion calificacion="4.5" noOpiniones="345" />
        <urlImagen>
            http://ejemplos.hermosaprogramacion.com/hoteles-roma/barcelo-aran-park.jpg
        </urlImagen>
        <datosWeb:descripcion>
            <![CDATA[ Desayuna con la mejor pizza ]]>
        </datosWeb:descripcion>
    </hotel>
    ...
</hoteles>

¿Qué interpretas de la estructura XML?

Por intuición verás un conjunto de datos describiendo varias entidades tipo hotel, con un formato homogéneo. Donde el propósito de cada etiqueta es el siguiente:

  • <hoteles>: Nodo raíz que encabeza que representa al conjunto de datos.
  • <hotel>: hace referencia a una entidad de tipo hotel en particular.
  • <idHotel>: Identificador único de cada hotel. Será de utilidad si deseas usar una base de datos local sqlite para aplicar caching.
  • <nombre>: Nombre registrado del hotel como compañía.
  • <valoracion>: Representa la popularidad del hotel, la cual es descrita en los atributos "calificacion" y "noOpiniones". El primero determina de 1 a 5 la calificación promedio obtenida por la cantidad de opiniones (segundo) generadas por los usuarios que acceden al servicio de búsqueda.
  • <urlImagen>: Contiene el link de la imagen que representa al hotel para su promoción.
  • <datosWeb:descripcion>: Esta etiqueta representa la descripción de promoción que existe para el hotel. Le asigne el namespace "datosWeb" para cubrir el tema de prefijos ahora que empecemos a parsear. Igualmente incluí la marca CDATA para mostrar la obtención de este tipo de valor.

Crear Nuevo Proyecto En Android Studio

Paso 1. Crea un nuevo proyecto en Android Studio accediendo a File > New > New Project…  y nómbralo “Guía De Hoteles En Roma”. Añade una actividad del tipo Blank Activity en el asistente de creación y asígnale el nombre de ActividadPrincipal.java.

Paso 2. Este proyecto tiene los siguientes componentes especiales para el diseño de la interfaz:

Lo que significa que debes modificar las dependencias tú archivo build.gradle de la siguiente forma:

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

Código Para Parsear Archivo XML

El parsing XML requiere que separes las etiquetas que necesitas de aquellas que no.

Recuerda que parsear un formato XML significa convertir el flujo de datos con ese formato a una representación interna de nuestra aplicación. En este caso objetos Java que cobren significado dentro de la lógica de implementación.

Paso 3. Crea una clase que represente cada hotel del archivo XML llamada Hotel.java. Debido a que necesitamos todos los campos de la etiqueta <hotel>, entonces incluimos una representación de cada etiqueta en la clase.

Hotel.java

import java.util.ArrayList;
import java.util.List;

/**
 * Clase que representa cada hotel del archivo XML
 */
public class Hotel {
    private String idHotel;
    private String nombre;
    private String precio;
    private float valoracion;
    private String urlImagen;
    private int noOpiniones;
    private String descripcion;

    // Proveedor estático de datos para el adaptador
    public static List<Hotel> HOTELES = new ArrayList<>();

    public Hotel(String idHotel,
                 String nombre,
                 String precio,
                 float valoracion,
                 String urlImagen,
                 int noOpiniones,
                 String descripcion) {
        this.idHotel = idHotel;
        this.nombre = nombre;
        this.precio = precio;
        this.valoracion = valoracion;
        this.urlImagen = urlImagen;
        this.noOpiniones = noOpiniones;
        this.descripcion = descripcion;
    }

    public int getNoOpiniones() {
        return noOpiniones;
    }

    public String getNombre() {
        return nombre;
    }

    public String getIdHotel() {
        return idHotel;
    }

    public String getPrecio() {
        return precio;
    }

    public String getUrlImagen() {
        return urlImagen;
    }

    public float getValoracion() {
        return valoracion;
    }

    public String getDescripcion() {
        return descripcion;
    }
}

Las imágenes que usaremos están alojadas en el servidor, pero si deseas descargarlas para realizar pruebas locales, usa este botón:

DESCARGAR IMÁGENES

Paso 4. Ahora es turno para crear el Parser XML de la aplicación Android. Esto requiere que usemos la clase XmlPullParser para gestionar el recorrido del documento.

Pero… ¿Como funciona XmlPullParser?

Pasos a seguir:

1. Crear una clase que controle el flujo de parsing.

2. Implementar un método del parsing general que retorne en los datos completos.

3. Subdividir en métodos cada uno de los procesamientos de las etiquetas para reducir complejidad.

La lectura es sencilla. Este parser permite al programador recorrer todo el documento XML pasando de etiqueta en etiqueta de forma controlada. Esto permite recorrer toda la jerarquía y decidir en qué momento obtener los valores que necesitamos.

Si te fijas necesitamos dos ciclos para leer el archivo.

Recorrer Etiquetas Xml Con bucle

El primero para recorrer todas las etiquetas <hotel> y el segundo para recorrer los elementos anidados de cada etiqueta.

Parsing Xml De Atributos Con Bucle

La clase XmlPullParser basa su comportamiento de lectura en varios tipos de eventos. Estos nos servirán para usar condiciones que al ser verdaderas, obtengan los datos que requerimos. Los siguientes son los eventos más importantes:

Tipo de evento Descripción
START_DOCUMENT Este tipo de evento se presenta cuando se inicializa el parser.
START_TAG Representa el inicio de una nueva etiqueta en el DOM. Esto permite que leas el nombre de la etiqueta con getName() o los atributos de la etiqueta con getAttributeValue().
TEXT Indica que se encontró el valor de la etiqueta y este puede ser leído en formato texto. Para ello usa el método getText().
END_TAG Se da cuando el parser acaba de leer una etiqueta de cierre dentro de los datos XML. Si deseas obtener el nombre de dicha etiqueta, entonces usa getName().
END_DOCUMENT Indica que el parser acaba de encontrar el final del flujo XML.

Algunos puntos importantes:

  • Crear nueva instancia del Parser— Usa el método estático Xml.newPullParser() para crear una instancia de XmlPullParser.
    XmlPullParser parser = Xml.newPullParser();
  • Indicar el flujo que alimenta al Parser— Invoca el método setInput(). Este recibe el flujo de datos tipo InputStream . Como segundo parámetro puedes indicar el encoding del formato, como por ejemplo formato utf-8.
    parser.setInput(inputStream, null);
  • Habilita características especiales si es necesario— Cambia el estado de una característica especial del parser XML con el método setFeature(). Una de ellas es la capacidad para leer namespaces con la constante XmlPullParser.FEATURE_PROCESS_NAMESPACES. Por defecto no está habilitada, así que habilítala si lo necesitas.
    parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
  • Obtener el siguiente elemento u evento de parsing— Debido a que recorremos manualmente el archivo XML, emplea el método next() para ir al próximo elemento u evento. También puedes usar nextTag() para este propósito, la diferencia está en que este método solo retorna un evento si actualmente el parser está en START_TAG o END_TAG.
    if (parser.next() == XmlPullParser.TEXT) {
        result = parser.getText();
        parser.nextTag();
    }
    return result;
  • Retorna en el tipo de evento actual en el que se encuentra el parser— Averigua este dato con el método getEventType().
    if (parser.getEventType() != XmlPullParser.START_TAG) {
        throw new IllegalStateException();
    }
  • Comprobar eventos y nombres— Usar el método require() para saber si el evento actual es del tipo especificado y si el namespace y el nombre de la etiqueta, coinciden con las características requeridas. Si no es así, entonces se lanza una excepción.
    parser.require(XmlPullParser.START_TAG, "prefijo_namespace", ETIQUETA_NOMBRE);

Con estos datos claros, veamos como parsear el archivo XML de hoteles en Roma:

ParserXml.java

import android.util.Xml;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/**
 * Parser XML de hoteles
 */
public class ParserXml {

    // Namespace general. null si no existe
    private static final String ns = null;

    // Constantes del archivo Xml
    private static final String ETIQUETA_HOTELES = "hoteles";

    private static final String ETIQUETA_HOTEL = "hotel";

    private static final String ETIQUETA_ID_HOTEL = "idHotel";
    private static final String ETIQUETA_NOMBRE = "nombre";
    private static final String ETIQUETA_PRECIO = "precio";
    private static final String ETIQUETA_VALORACION = "valoracion";
    private static final String ETIQUETA_URL_IMAGEN = "urlImagen";
    private static final String ETIQUETA_DESCRIPCION = "descripcion";

    private static final String PREFIJO = "datosWeb";
    private static final String ATRIBUTO_CALIFICACION = "calificacion";
    private static final String ATRIBUTO_OPINIONES = "noOpiniones";


    /**
     * Parsea un flujo XML a una lista de objetos {@link Hotel}
     *
     * @param in flujo
     * @return Lista de hoteles
     * @throws XmlPullParserException
     * @throws IOException
     */
    public List<Hotel> parsear(InputStream in) throws XmlPullParserException, IOException {
        try {
            XmlPullParser parser = Xml.newPullParser();
            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
            parser.setInput(in, null);
            parser.nextTag();
            return leerHoteles(parser);
        } finally {
            in.close();
        }
    }

    /**
     * Convierte una serie de etiquetas <hotel> en una lista
     *
     * @param parser
     * @return lista de hoteles
     * @throws XmlPullParserException
     * @throws IOException
     */
    private List<Hotel> leerHoteles(XmlPullParser parser)
            throws XmlPullParserException, IOException {
        List<Hotel> listaHoteles = new ArrayList<Hotel>();

        parser.require(XmlPullParser.START_TAG, ns, ETIQUETA_HOTELES);
        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() != XmlPullParser.START_TAG) {
                continue;
            }
            String nombreEtiqueta = parser.getName();
            // Buscar etiqueta <hotel>
            if (nombreEtiqueta.equals(ETIQUETA_HOTEL)) {
                listaHoteles.add(leerHotel(parser));
            } else {
                saltarEtiqueta(parser);
            }
        }
        return listaHoteles;
    }

    /**
     * Convierte una etiqueta <hotel> en un objero Hotel
     *
     * @param parser parser XML
     * @return nuevo objeto Hotel
     * @throws XmlPullParserException
     * @throws IOException
     */
    private Hotel leerHotel(XmlPullParser parser) throws XmlPullParserException, IOException {
        parser.require(XmlPullParser.START_TAG, ns, ETIQUETA_HOTEL);
        int idHotel = 0;
        String nombre = null;
        String precio = null;
        float calificacion = 0;
        String urlImagen = null;
        int noOpiniones = 0;
        String descripcion = null;
        HashMap<String, String> valoracion = new HashMap<>();

        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() != XmlPullParser.START_TAG) {
                continue;
            }
            String name = parser.getName();

            switch (name) {
                case ETIQUETA_ID_HOTEL:
                    idHotel = leerIdHotel(parser);
                    break;
                case ETIQUETA_NOMBRE:
                    nombre = leerNombre(parser);
                    break;
                case ETIQUETA_PRECIO:
                    precio = leerPrecio(parser);
                    break;
                case ETIQUETA_VALORACION:
                    valoracion = leerValoracion(parser);
                    calificacion = Float.parseFloat(valoracion.get(ATRIBUTO_CALIFICACION));
                    noOpiniones = Integer.parseInt(valoracion.get(ATRIBUTO_OPINIONES));
                    break;
                case ETIQUETA_URL_IMAGEN:
                    urlImagen = leerUrlImagen(parser);
                    break;
                case ETIQUETA_DESCRIPCION:
                    descripcion = leerDescripcion(parser);
                    break;
                default:
                    saltarEtiqueta(parser);
                    break;
            }
        }
        return new Hotel(idHotel,
                nombre,
                precio,
                calificacion,
                urlImagen,
                noOpiniones,
                descripcion);
    }

    // Procesa la etiqueta <idHotel> de los hoteles
    private int leerIdHotel(XmlPullParser parser) throws IOException, XmlPullParserException {
        parser.require(XmlPullParser.START_TAG, ns, ETIQUETA_ID_HOTEL);
        int idHotel = Integer.parseInt(obtenerTexto(parser));
        parser.require(XmlPullParser.END_TAG, ns, ETIQUETA_ID_HOTEL);
        return idHotel;
    }

    // Procesa las etiqueta <nombre> de los hoteles
    private String leerNombre(XmlPullParser parser) throws IOException, XmlPullParserException {
        parser.require(XmlPullParser.START_TAG, ns, ETIQUETA_NOMBRE);
        String nombre = obtenerTexto(parser);
        parser.require(XmlPullParser.END_TAG, ns, ETIQUETA_NOMBRE);
        return nombre;
    }

    // Procesa la etiqueta <precio> de los hoteles
    private String leerPrecio(XmlPullParser parser) throws IOException, XmlPullParserException {
        parser.require(XmlPullParser.START_TAG, ns, ETIQUETA_PRECIO);
        String precio = obtenerTexto(parser);
        parser.require(XmlPullParser.END_TAG, ns, ETIQUETA_PRECIO);
        return precio;
    }

    // Procesa la etiqueta <valoracion> de los hoteles
    private HashMap<String, String> leerValoracion(XmlPullParser parser)
            throws IOException, XmlPullParserException {
        parser.require(XmlPullParser.START_TAG, ns, ETIQUETA_VALORACION);
        String calificacion = parser.getAttributeValue(null, ATRIBUTO_CALIFICACION);
        String noOpiniones = parser.getAttributeValue(null, ATRIBUTO_OPINIONES);
        parser.nextTag();
        parser.require(XmlPullParser.END_TAG, ns, ETIQUETA_VALORACION);

        HashMap<String, String> atributos = new HashMap<>();
        atributos.put(ATRIBUTO_CALIFICACION, calificacion);
        atributos.put(ATRIBUTO_OPINIONES, noOpiniones);

        return atributos;
    }

    // Procesa las etiqueta <urlImagen> de los hoteles
    private String leerUrlImagen(XmlPullParser parser) throws IOException, XmlPullParserException {
        String urlImagen;
        parser.require(XmlPullParser.START_TAG, ns, ETIQUETA_URL_IMAGEN);
        urlImagen = obtenerTexto(parser);
        parser.require(XmlPullParser.END_TAG, ns, ETIQUETA_URL_IMAGEN);
        return urlImagen;
    }

    // Procesa las etiqueta <descripcion> de los hoteles
    private String leerDescripcion(XmlPullParser parser) throws IOException, XmlPullParserException {
        String descripcion = "";
        parser.require(XmlPullParser.START_TAG, ns, ETIQUETA_DESCRIPCION);
        String prefijo = parser.getPrefix();
        if (prefijo.equals(PREFIJO))
            descripcion = obtenerTexto(parser);
        parser.require(XmlPullParser.END_TAG, ns, ETIQUETA_DESCRIPCION);
        return descripcion;
    }

    // Obtiene el texto de los atributos
    private String obtenerTexto(XmlPullParser parser) throws IOException, XmlPullParserException {
        String resultado = "";
        if (parser.next() == XmlPullParser.TEXT) {
            resultado = parser.getText();
            parser.nextTag();
        }
        return resultado;
    }

    // Salta aquellos objeteos que no interesen en la jerarquía XML.
    private void saltarEtiqueta(XmlPullParser parser) throws XmlPullParserException, IOException {
        if (parser.getEventType() != XmlPullParser.START_TAG) {
            throw new IllegalStateException();
        }
        int depth = 1;
        while (depth != 0) {
            switch (parser.next()) {
                case XmlPullParser.END_TAG:
                    depth--;
                    break;
                case XmlPullParser.START_TAG:
                    depth++;
                    break;
            }
        }
    }

}

El método parsear() es aquel que se encarga de orquestar todo el proceso de parsing para retornar la lista de hoteles.

El primer bucle while del cual habíamos hablado se encuentra en el método leerHoteles(), el cual obtiene todos los datos.

Sin embargo este método se sirve de leerHotel(), donde encontraremos el segundo bloque anidado del que habíamos hablando. Este se encarga de procesar cada etiqueta dependiendo de su nombre.

El método obtenerTexto() es usado para conseguir el valor de todas las etiquetas. Dentro de este se comprueba el evento TEXT para obtener el valor con getText().

Finalmente viene una serie de métodos encargados de procesar la toma de datos de cada etiqueta. También tenemos el método saltarEtiqueta() que se encarga de ignorar aquellas etiquetas que no deseamos, por si llegase a ocurrir.

Si te fijas, la única etiqueta que tiene un componente CDATA es <descripcion>. Pero no es necesario usar métodos adicionales para la obtención del valor. Procesamos su contenido normalmente. Adicionalmente obtuvimos el prefijo de esta etiqueta con el método getPrefix() y así comparar la validez de esta.

Crear Lista Con RecyclerView Y Cards

Paso 5. Diseña el layout de los ítems que se mostrarán en la lista. Para ello ve a la carpeta res/layout y agrega un nuevo documento llamado item_hotel.xml con la siguiente definición 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="4dp"
    card_view:cardUseCompatPadding="true">

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

        <ImageView
            android:id="@+id/miniatura_hotel"
            android:layout_width="match_parent"
            android:layout_height="@dimen/altura_miniatura_hotel"
            android:layout_alignParentTop="true"
            android:background="@color/background_material_light" />

        <RatingBar
            android:id="@+id/valoracion"
            style="?android:attr/ratingBarStyleSmall"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/nombre_hotel"
            android:layout_marginBottom="8dp"
            android:layout_marginLeft="8dp"
            android:visibility="visible" />

        <TextView
            android:id="@+id/nombre_hotel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/miniatura_hotel"
            android:padding="8dp"
            android:text="Nombre del hotel"
            android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
            android:textSize="14sp" />

        <TextView
            android:id="@+id/precio_actual"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignBottom="@+id/miniatura_hotel"
            android:layout_alignParentEnd="true"
            android:padding="8dp"
            android:text="Precio"
            android:textAppearance="@style/TextAppearance.AppCompat.Headline"
            android:textColor="@android:color/white" />

        <ImageView
            android:id="@+id/boton_masOpciones"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_alignParentEnd="true"
            android:layout_alignParentRight="true"
            android:padding="@dimen/padding_boton_icono"
            android:src="@drawable/ic_dots_vertical" />

        <ImageView
            android:id="@+id/boton_favoritos"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_toLeftOf="@+id/boton_masOpciones"
            android:padding="@dimen/padding_boton_icono"
            android:src="@drawable/ic_heart" />

    </RelativeLayout>

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

La idea es mostrar el nombre del hotel, el precio por noche, su valoración, la imagen que está asociada y dos iconos que representan las acciones de las cards.

Layout De Hotel En Android Estilo TripAdvisor

Paso 6. Lo siguiente es crear el adaptador para el RecyclerView. El objetivo de este componente es inflar cada uno de los datos que vienen de la lista de hoteles.

Así que crea una nueva clase llamada AdaptadorDeHoteles.java e incluye el siguiente código.

import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RatingBar;
import android.widget.TextView;

import com.bumptech.glide.Glide;

/**
 * {@link android.support.v7.widget.RecyclerView.Adapter} para poblar un recycler view
 * con información de hoteles.
 */
public class AdaptadorDeHoteles extends RecyclerView.Adapter<AdaptadorDeHoteles.ViewHolder> {

    /**
     * Interfaz de comunicación
     */
    public interface OnItemClickListener {
        void onItemClick(ViewHolder item, int position);
    }

    private OnItemClickListener listener;

    public void setOnItemClickListener(OnItemClickListener listener) {
        this.listener = listener;
    }

    public OnItemClickListener getOnItemClickListener() {
        return listener;
    }

    public static class ViewHolder extends RecyclerView.ViewHolder
            implements View.OnClickListener {
        // Campos respectivos de un item
        public TextView nombre;
        public TextView precio;
        public RatingBar valoracion;
        public ImageView imagen;

        private AdaptadorDeHoteles padre = null;

        public ViewHolder(View v, AdaptadorDeHoteles padre) {
            super(v);

            v.setOnClickListener(this);
            this.padre = padre;

            nombre = (TextView) v.findViewById(R.id.nombre_hotel);
            precio = (TextView) v.findViewById(R.id.precio_actual);
            valoracion = (RatingBar) v.findViewById(R.id.calificacion);
            imagen = (ImageView) v.findViewById(R.id.miniatura_hotel);

        }

        @Override
        public void onClick(View v) {
            final OnItemClickListener listener = padre.getOnItemClickListener();
            if (listener != null) {
                listener.onItemClick(this, getAdapterPosition());
            }
        }
    }

    @Override
    public long getItemId(int position) {
        return Hotel.HOTELES.get(position).getIdHotel();
    }

    public AdaptadorDeHoteles() {
    }

    @Override
    public int getItemCount() {
        return Hotel.HOTELES.size();
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        View v = LayoutInflater.from(viewGroup.getContext())
                .inflate(R.layout.item_hotel, viewGroup, false);
        return new ViewHolder(v, this);
    }

    @Override
    public void onBindViewHolder(ViewHolder viewHolder, int i) {
        Hotel item = Hotel.HOTELES.get(i);

        viewHolder.nombre.setText(item.getNombre());
        viewHolder.precio.setText("$" + item.getPrecio());
        viewHolder.valoracion.setRating(item.getCalificacion());
        Glide.with(viewHolder.itemView.getContext())
                .load(item.getUrlImagen())
                .centerCrop()
                .into(viewHolder.imagen);

    }


}

Es fundamental que la carga de las imágenes se realice con la librería Glide para que no tengas problemas en el hilo principal.

También puedes sobrescribir el método getItemId() para retornar el identificador de cada elemento. Esto será de gran ayuda a la hora de procesar los eventos del recycler view.

Parsing XML Desde Url Con La Clase HttpUrlConnection

Paso 7. Debido a que el formato XML se encuentra en la url que especificamos al inicio, es necesario usar el cliente HttpUrlConnection para traer su contenido a la aplicación Android.

Recuerda que las operaciones de red necesitan ser aisladas en un hilo de trabajo a parte. Lo que significa que primero debemos declarar una AsyncTask dentro de ActividadPrincipal para realizar la consulta.

Veamos:

private class TareaDescargaXml extends AsyncTask<String, Void, List<Hotel>> {

    @Override
    protected List<Hotel> doInBackground(String... urls) {
        try {
            return parsearXmlDeUrl(urls[0]);
        } catch (IOException e) {
            return null; // null si hay error de red
        } catch (XmlPullParserException e) {
            return null; // null si hay error de parsing XML
        }
    }

    @Override
    protected void onPostExecute(List<Hotel> result) {
        // Actualizar contenido del proveedor de datos
        Hotel.HOTELES = result;
        // Actualizar la vista del adaptador
        adaptador.notifyDataSetChanged();
    }
}

Dentro de doInBackground() retornarmos en el resultado que nos entrega el método parsearXmlDeUrl(). Este recibe la url que viene como parámetro en la tarea asíncrona, luego descarga el contenido alojado en la url y realiza el parsing XML.

Es importante el manejo de errores al realizar el parsing y ejecutar la operación de red.

Finalmente en onPostExecute() se recibe la lista de hoteles con el fin de actualizar el proveedor de datos Hotel.HOTELES. Esto permite que llames al método notifyDataSetChanged() del recycler para actualizar la lista.

Paso 8. Procesa la operación Http dentro del método parsearXmlDeUrl().

// Dentro de ActividadPrincipal.java
private List<Hotel> parsearXmlDeUrl(String urlString)
        throws XmlPullParserException, IOException {
    InputStream stream = null;
    ParserXml parserXml = new ParserXml();
    List<Hotel> entries = null;

    try {
        stream = descargarContenido(urlString);
        entries = parserXml.parsear(stream);

    } finally {
        if (stream != null) {
            stream.close();
        }
    }

    return entries;
}

private InputStream descargarContenido(String urlString) throws IOException {
    URL url = new URL(urlString);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setReadTimeout(10000);
    conn.setConnectTimeout(15000);
    conn.setRequestMethod("GET");
    conn.setDoInput(true);
    // Iniciar la petición
    conn.connect();
    return conn.getInputStream();
}

Lo primero es descargar el flujo del archivo hoteles.xml desde la url (declarada como constante) a través del método descargarContenido().

Luego con el InputStream que resulta de esta acción, podemos inicializar el parser de hoteles e invocar al método parsear() para retornar en una colección tipo List<Hotel>.

Con ello ya es posible echar a andar la tarea asíncrona dentro del método onCreate().

Cambiar La Actividad Principal

Paso 9. Ve a res/layout y abre el archivo actividad_principal.xml. Ahora incluye dentro del diseño una toolbar para representar la action bar de la actividad principal, junto a un RecyclerView para la lista.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ActividadPrincipal">

    <!-- Toolbar -->
    <android.support.v7.widget.Toolbar xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
        app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />

    <android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/reciclador"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/toolbar"
        android:padding="3dp"
        android:scrollbars="vertical" />


</RelativeLayout>

Paso 10. Modifica la clase ActividadPrincipal para que obtenga la instancia del recycler view. Luego relaciona el adaptador y un LinearLayoutManager.

import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;

import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;

/**
 * Actividad que muestra el parsing XML en una lista
 */
public class ActividadPrincipal extends AppCompatActivity{
    private RecyclerView reciclador;
    private LinearLayoutManager linearManager;
    private AdaptadorDeHoteles adaptador;

    private final static String URL =
            "http://ejemplos.hermosaprogramacion.com/hoteles-roma/hoteles.xml";

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

        usarToolbar();

        reciclador = (RecyclerView) findViewById(R.id.reciclador);
        reciclador.setHasFixedSize(true);
        linearManager = new LinearLayoutManager(this);
        reciclador.setLayoutManager(linearManager);

        adaptador = new AdaptadorDeHoteles();
        adaptador.setHasStableIds(true);
        adaptador.setOnItemClickListener(this);

        reciclador.setAdapter(adaptador);

        new TareaDescargaXml().execute(URL);

    }

    private void usarToolbar() {
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    }

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

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

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

        return super.onOptionsItemSelected(item);
    }

    private class TareaDescargaXml extends AsyncTask<String, Void, List<Hotel>> {

        @Override
        protected List<Hotel> doInBackground(String... urls) {
            try {
                return parsearXmlDeUrl(urls[0]);
            } catch (IOException e) {
                return null; // null si hay error de red
            } catch (XmlPullParserException e) {
                return null; // null si hay error de parsing XML
            }
        }

        @Override
        protected void onPostExecute(List<Hotel> result) {
            // Actualizar contenido del proveedor de datos
            Hotel.HOTELES = result;
            // Actualizar la vista del adaptador
            adaptador.notifyDataSetChanged();
        }
    }

    private List<Hotel> parsearXmlDeUrl(String urlString)
            throws XmlPullParserException, IOException {
        InputStream stream = null;
        ParserXml parserXml = new ParserXml();
        List<Hotel> entries = null;

        try {
            stream = descargarContenido(urlString);
            entries = parserXml.parsear(stream);

        } finally {
            if (stream != null) {
                stream.close();
            }
        }

        return entries;
    }

    private InputStream descargarContenido(String urlString) throws IOException {
        URL url = new URL(urlString);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setReadTimeout(10000);
        conn.setConnectTimeout(15000);
        conn.setRequestMethod("GET");
        conn.setDoInput(true);
        // Iniciar la petición
        conn.connect();
        return conn.getInputStream();
    }
}

Para el recycler view usa el método setHasStableIds() con el valor de true. Esta acción habilita la obtención de identificadores para los ítems a través del método getItemId() del adaptador.

Ejecuta el proyecto y verás el siguiente resultado.

Aplicación Android Para Buscar Hoteles En Roma

Crear Actividad De Detalle

Paso 11. Mostraremos la descripción de cada hotel en una actividad nueva que contenga su detalle. Para ello crea una nueva actividad en Android Studio y asígnale el nombre de “ActividadDetalle.java”.

Paso 12. Ahora ve a res/layout y abre su archivo de diseño. La idea es añadir tres bloques de contenido para mostrar la información de cada hotel romano.

El primer bloque será un ImageView que despliegue la imagen del hotel. En segundo lugar se encuentra un LinearLayout central que mostrará el nombre y precio.

El último espacio será ocupado con otro LinearLayout para la calificación, el número de opiniones y la descripción.

Actividad Con Datos De Un Hotel

Lo que dejaría la siguiente definición XML del layout:

actividad_detalle.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.herprogramacion.guadehotelesenroma.ActividadDetalle">

    <!-- Toolbar -->
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
        app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />

    <ImageView
        android:id="@+id/hotelImageView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="50"
        android:scaleType="centerCrop" />


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?colorPrimary"
        android:elevation="2dp"
        android:orientation="vertical"
        android:paddingBottom="@dimen/margen_estandar"
        android:paddingStart="@dimen/especificacion2"
        android:paddingTop="@dimen/margen_estandar">

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

        <TextView
            android:id="@+id/precioTextView"
            style="@style/TextAppearance.AppCompat.Subhead.Inverse"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </LinearLayout>


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginLeft="@dimen/especificacion2"
        android:layout_weight="50"
        android:elevation="2dp"
        android:orientation="vertical"
        android:paddingBottom="@dimen/margen_estandar"
        android:paddingTop="@dimen/margen_estandar">

        <RatingBar
            android:id="@+id/calificacion"
            style="?android:attr/ratingBarStyleSmall"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="@dimen/margen_estandar"
            android:layout_marginTop="@dimen/margen_estandar"
            android:visibility="visible" />

        <TextView
            android:id="@+id/noOpinionesTextView"
            style="@style/TextAppearance.AppCompat.Body1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingBottom="@dimen/margen_estandar"
            android:textIsSelectable="true" />

        <TextView
            android:id="@+id/descripcionTextView"
            style="@style/TextAppearance.AppCompat.Body1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingBottom="@dimen/margen_estandar"
            android:textIsSelectable="true" />


    </LinearLayout>

</LinearLayout>

Paso 13. Abre la clase ActividadDetalle y modifica su comportamiento con el fin de actualizar cada view del layout. Usa el método getIntent() para obtener el intent que vendrá desde ActividadPrincipal. Luego extrae el identificador del hotel que fue enviado y publica sus datos.

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ImageView;
import android.widget.RatingBar;
import android.widget.TextView;

import com.bumptech.glide.Glide;

public class ActividadDetalle extends AppCompatActivity {

    public static final String EXTRA_ID = "com.herprogramacion.guadehotelesenroma.extra.ID";

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

        usarToolbar();

        // Extraer ID 
        Intent intent = getIntent();
        int idHotel = intent.getIntExtra(EXTRA_ID, 0);
        Hotel hotelActual = Hotel.getItem(idHotel);

        // Obtener instancias de cada view
        TextView nombre = (TextView) findViewById(R.id.nombreTextView);
        TextView precio = (TextView) findViewById(R.id.precioTextView);
        RatingBar calificacion = (RatingBar) findViewById(R.id.calificacion);
        TextView noOpiniones = (TextView) findViewById(R.id.noOpinionesTextView);
        TextView descripcion = (TextView) findViewById(R.id.descripcionTextView);
        ImageView imagen = (ImageView) findViewById(R.id.hotelImageView);

        // Añadir valores del hotel
        assert hotelActual != null;
        nombre.setText(hotelActual.getNombre());
        precio.setText("$" + hotelActual.getPrecio());
        calificacion.setRating(hotelActual.getCalificacion());
        noOpiniones.setText(hotelActual.getNoOpiniones() + " Opiniones");
        descripcion.setText(hotelActual.getDescripcion());
        Glide.with(this)
                .load(hotelActual.getUrlImagen())
                .into(imagen);

    }

    private void usarToolbar() {
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        if (getSupportActionBar() != null)
            getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    }

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

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }
}

Procesar Eventos Del RecyclerView Con OnItemClickListener

Paso 14. Haz que la actividad principal herede de la interfaz de comunicación AdaptadorHoteles.OnItemClickListener e implementa el controlador onItemClick(). Esto permitirá iniciar la actividad de detalle según el ítem presionado.

public class ActividadPrincipal extends AppCompatActivity
        implements AdaptadorDeHoteles.OnItemClickListener {    

    ...

    @Override
    public void onItemClick(AdaptadorDeHoteles.ViewHolder item, int position) {
        Intent intent = new Intent(this, ActividadDetalle.class);
        int itemId = (int) item.getItemId();
        intent.putExtra(ActividadDetalle.EXTRA_ID, itemId);
        startActivity(intent);
    }

    ...
}

El identificador que se envía como extra a través del intent, es obtenido con el view holder que el método onItemClick() trae como primer parámetro.

Usa el método getItemId() a fin de obtener el identificador que el adaptador asignó al ítem.

Finalmente ejecuta la app y prueba su funcionalidad.

Conclusión

Este tutorial te ha mostrado un ejemplo de parsing XML en Android con XmlPullParser.

Vimos cómo usar esta librería para recorrer secuencialmente un archivo XML y controlar las acciones necesarias, sobre las etiquetas, atributos y valores elegidos.

Adicionalmente puedes aumentar la funcionalidad de tu app, si usas almacenamiento local con un Content Provider y además aprendes a sincronizar los datos con un servidor.

  • Alejandro Valencia

    Hola como puedo hacer que funciones el raiting bar y los botones.

  • Alejandro Valencia

    Hola tengo un problema y es que no me cargan las imagenes que podria ser.

    • Hola Alejandro, debo arreglar mis enlaces. Cuando cambié de hosting algunas cosas dejaron de funcionar

  • julian

    buenas tardes, estoy tratando de ejecutar el proyecto, tal cual como lo descargue y me sale este error::::::

    java.lang.NullPointerException

    at com.herprogramacion.guadehotelesenroma.AdaptadorDeHoteles.getItemCount(AdaptadorDeHoteles.java:76)

    at android.support.v7.widget.RecyclerView.onMeasure(RecyclerView.java:2357)

    at android.view.View.measure(View.java:15172)

    alguien me puede ayudar gracias

    • Diego

      resolveu este erro, porq aqui estou tendo o mesmo problema

  • Juan M. Paz

    Muy bueno! pero como se puede hacer para hacerlo local ya que las imagenes las tiene en un servidor pero para hacerlo local como lo menciones ¿como puede ser?

    • Hola Juan, supongo que debes proveer la URL de tu servidor local adjuntando el nombre del archivo.

      O crea un servicio web que te provea el contenido a través de una URL personalizada. Luego aplica el parsing que se ve arriba dependiendo del contenido

  • Christian Sari

    Estimado James, a ver si me puedes ayudar con una indicación, seguí tus tutoriales, tengo un problema, implementé tablayouts, con xmlpullparser, con cuatro tabs, mi idea era hacer diferentes adaptadores, diferentes fragments, sucede que son diferentes estructuras de xml’s, pero la primera vez que le hice con el primer tab y el primer xml funcionó bien, pero al querer hacer con el siguiente tab y el nuevo xml, dio error y no se ejecuta la aplicacion. ME ayudas por favor..

  • Christian Sari

    Buenos dias, excelente tuturial, pero tengo un problema con el gradle, talvez que versión de usas? 22? yo intente hacerlo con el 23?, no reconoce las dependencias, appcompat-v7,recyclerview-v7,cardview-v7, muestra un error en el layout’s, algo que este haciendo mal?

    • Hola Christian. Creo que son más antiguas, ¿que tal si pruebas dando clean en Android Studio?

      • Christian Sari

        Gracias James, logre solucionar eso, bueno te comento que quiero implementar este ejemplo pero no con hoteles sino con noticias, pero tengo problemas con la clase Adapter, la clase main no se implementa con la clase adapter, ahi debe ser algo mas de forma, voy a intentar nuevamente para ver el error. Gracias por tu respuesta. Una excelente página.

        • Samuel Carpinteyro

          Como lo lograste? porque tengo el mismo error que tu de la versión

          • Christian Sari

            Hola Samuel, el problema estaba en la version, busque las versiones nuevas, estas son las que ocupo actualmente.

            compile ‘com.android.support:appcompat-v7:23.1.1’
            compile ‘com.android.support:design:23.1.1’
            compile ‘com.android.support:recyclerview-v7:23.1.1’
            compile ‘com.github.bumptech.glide:glide:3.6.1’
            compile ‘com.android.support:support-v4:23.1.1’
            compile ‘com.android.support:cardview-v7:23.1.1’

            Espero te ayude, cualquier otra cosa me avisas.

  • Gabriel Villarreal

    Hola James, hay alguna forma de agregar una nueva etiqueta, celular, pagina web, etc..por ejemplo, en cada hotel que tenga en el archivo xml? o tengo que crear la etiqueta manualmente una por una en cada hotel? gracias

  • Eduardo

    Muy bueno este tutorial.
    Hay algunas cosas que no se ven aquí y solo en el proyecto.
    Si pudieras volver a subirlo sería genial, ya que cuando trato de descargarlo me sale que el archivo ya no existe.

    Saludos.

    • Hola Eduardo, acabo de renovar el link. Intenta descargarlo. Lo que pasa es que cambié el nombre de la carpeta en dropbox y todos los links caducaron

  • Johana Cedeño Estupiñan

    En la clase hotel hay un codigo que no veo aqui pero que esta en los archivos de descarga

    public static Hotel getItem(int id) {

    for (Hotel item : HOTELES) {

    if (item.getIdHotel() == id) {

    return item;

    }

    }

    return null;

    }

    Que hace eso?

    • Obtiene un item de la lista HOTELES dependiendo del id que se ingrese como parámetro

  • zeba

    Hola, de que manera puedo trabajar esto con PHP y mysql GRACIAS, SALUDOS!

  • Micaela

    Hola muy buenos tutoriales , te felicito y gracias , estoy buscando tutoriales sobre gcm con php mysql , me preguntaba si tenias pensado armar uno ya que gcm es algo que se utiliza bastante y no encuentro mucho,, desde ya muchas gracias por todo :)

    • Hola Micaela. Si claro, espero que se de pronto, ya son varias personas que lo piden.

  • Gabriel Villarreal

    Felicitaciones por el tutorial!, es parecido a lo que andaba buscando, solo que trayendo los datos desde una base de datos sql, donde tengo los datos de los hoteles y la url de la imagen tambien alli. Lo que me quedo en duda es sobre cual metodo es mejor? una base de datos o tener los datos en un archivo xml?

    • Hola Gabriel.

      A mi modo de ver, ambos se complementan. Lo digo porque normalmente los archivos xml vendrán desde un servicio web o recurso web, para mantener la información actualizada.

      Cuando lleguen los datos xml a la app, es indispensable guardarlos en la base de datos con el fin de crear una cache y evitar descargar nuevamente el flujo.

      Esto permitirá que sincronices los datos nuevos que vienen en xml con los datos que ya existen en tu base de datos.

      • Gabriel Villarreal

        pero usar json para conectarse a un servicio web y traer datos de una base de datos de la nube no se estila? se usan archivos xml para traer datos y almacenarlos en una base de datos local como cache e ir actualizando solo el xml. Es asi? Uds que saben mas, que metodo me aconsejan usar ya que estoy empezando a programar en android. Gracias James por tu tiempo!

        • Json y Xml permiten comunicar datos entre aplicaciones. Ambos pueden ayudarte a crear un servicio web. Todo depende de tu estilo de diseño del servicio (REST, SOAP, RPC, etc) y de la estructura de la información.

          Sin importar que formato elijas, es necesario guardar los datos ya parseados en una base de datos local para crear una cache. Esto evita consultar la web cada vez que vayas a revisar los datos.

          • Gabriel Villarreal

            Gracias por la respuesta!, espero el tutorial de luis Miguel para crear un servicio en xml o como usar xmlpullparser.

          • El archivo xml está en mi servidor apache. Cuando yo obtengo su contenido con el cliente httpurlconnection, simplemente convierto esos datos y los almaceno en la base de datos. El archivo XML como tal no lo guardo en el dispositivo.

          • Gabriel Villarreal

            Y si quieres guardar otras fotos por ejemplo, pones los links en la etiqueta UrlImagen o creas una nueva etiqueta por cada foto?.

          • Los links son almacenados como atributo para tener una referencia directa del origen de cada imagen.

            Sin embargo, es buena opción almacenar las miniaturas de las imágenes en una cache de disco. Para ello te serviría la librería Glide.

  • Luis Miguel

    Muy bien James! es un articulo muy interesante y una alternativa a los que se nos dificulta con el parsing de Json, te felicito! tambien seria bueno que nos ayudaras a crear un servicio en xml. como el del tutorial! http://ejemplos.hermosaprogramacion.com/hoteles-roma/hoteles.xml Saludos! :)