ListView personalizado en Android

... que acaba siendo un lector RSS

La idea de este proyecto era simplemente probar un poco la personalización de los elementos de un ListView. Pero como eso no es bastante y uno se lía y luego se viene arriba pues lo que he hecho es el típico lector de RSS que carga el contenido del ListView con las últimas noticias de una web. Y la petición y el parseo se hace a la manera de Android, con un AsyncTask. Esto es un poco como cuando haces una ensalada, que dices igual le añado esto, y esto otro, y ese trozo de queso y esa media manzana. Al final la lechuga queda enterrada en complementos. Pues bien, el BaseAdapter que extendemos para personalizar el ListView es la lechuga de esta ensalada de Android.

Android RSS logo
MainActivity

La activity contiene un botón y un listView donde vamos a cargar las noticias del RSS. Al pulsar el botón se pone en marcha el AsyncTask y una vez este ha terminado, desde le propio AsyncTask se carga la lista, no hace falta mandar mensajes. Por cierto, en el manifest no olvides activar el permiso para INTERNET.

package info.pello.android.listadapter;

import java.util.ArrayList;

import android.os.Bundle;
import android.app.Activity;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.widget.ListView;

/**
 * The main activity that holds de ListView
 * @author Pello Xabier Altadill Izura
 * @greetz quality RSS contents
 */
public class MainActivity extends Activity {

	private RssListItemAdapter rssListItem;
	private RssReaderAsyncTask rssReaderAsyncTask;
	private ListView listNews;
	private static final String RSS_URL = "http://www.pello.info/index.php/rss2";

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		listNews = (ListView) findViewById(R.id.listNews);
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// Inflate the menu; this adds items to the action bar if it is present.
		getMenuInflater().inflate(R.menu.main, menu);
		return true;
	}

	/**
	 * method called when refresh button is pressed
	 * It Starts AsyncTask
	 * @param v
	 */
	public void refreshNews(View v) {
		Log.d("PELLODEBUG","MainActivity> refresh news...");
		rssReaderAsyncTask = new RssReaderAsyncTask(this);
		rssReaderAsyncTask.execute(RSS_URL);

	}
	
	/**
	 * refresh the List with parsed data from AsyncTask
	 * @param rssItems
	 */
	public void refreshList (ArrayList rssItems) {
		Log.d("PELLODEBUG","MainActivity> Async task finished...");
		rssListItem = new RssListItemAdapter(this, rssItems);
		listNews.setAdapter(rssListItem);
	}
	
}

RssListItemAdapter

Esta sería una clase que extiende un Adapter, en este caso el BaseAdapter. Nos permite crear una lista con aspecto personalizado. El método donde se cuece todo es getView, que es precisamente donde se "pinta" cada elemento de la lista.

package info.pello.android.listadapter;

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

import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

/**
 * extends a base adapter to create a customized ListAdapter
 * @author Pello Xabier Altadill Izura
 * @greetz 4vientos students
 */
class RssListItemAdapter extends BaseAdapter {
	
	private Activity activity;
	private ArrayList rssItems;
	

	/**
	 * Constructor
	 * 
	 * @param context
	 * @param layoutId
	 * @param rssItems
	 */
	public RssListItemAdapter(Activity activity,ArrayList rssItems) {
		super();
		this.activity = activity;
		this.rssItems = rssItems;
	}
	
	
	/**
	 * return number of items
	 * @return int
	 */
	public int getCount() {
		// TODO Auto-generated method stub
		return rssItems.size();
	}

	/**
	 * returns one object in a given position
	 * @param position
	 * @return Object
	 */
	public Object getItem(int position) {
		return rssItems.get(position);
	}

	/**
	 * returns id for the item(position)
	 */
	@Override
	public long getItemId(int position) {
		// TODO Auto-generated method stub
		return 0;
	}

	/**
	 * for each list item it call this method to render it in the ListView
	 * @param position
	 * @param converView
	 * @param parent
	 * @result View
	 */
	public View getView(int position, View convertview, ViewGroup parent) {
		View view = convertview;
		if(convertview == null){
			LayoutInflater inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
			view = inflater.inflate(R.layout.item,null);
		}
		
		RssItem item = rssItems.get(position);
		Log.d("PELLODEBUG", item.toString());
		
		TextView textViewTitle = (TextView) view.findViewById(R.id.textViewTitle);
		textViewTitle.setText(item.getTitle());
		
		TextView textViewText = (TextView) view.findViewById(R.id.textViewText);
		textViewText.setText(item.getText());
	
		
		return view;
		
		
	}

}


RssItem

Este no es más que un POJO para representar un item de un fichero RSS. Tiene los campos más interesantes para nuestro propósito, como son el título, la descripción, la url, la fecha, etc...

package info.pello.android.listadapter;

/**
 * represents an item from a Rss source
 * @author Pello Xabier Altadill Izura
 * @greetz to all the fresh news 
 */
public class RssItem {
	private String title;
	private String text;
	private String url;
	private String imageUrl;
	private String rssDate;
	
	/**
	 * default constructor
	 */
	public RssItem () {
		
	}
	
	/**
	 * constructor with minimal params
	 * @param title
	 * @param text
	 * @param url
	 */
	public RssItem (String title, String text, String url) {
		this.title = title;
		this.text = text;
		this.url = url;
		
	}
	/**
	 * @param title
	 * @param text
	 * @param url
	 * @param imageUrl
	 * @param rssDate
	 */
	public RssItem(String title, String text, String url, String imageUrl,
			String rssDate) {
		this.title = title;
		this.text = text;
		this.url = url;
		this.imageUrl = imageUrl;
		this.rssDate = rssDate;
	}
	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return "RssItem [" + (title != null ? "title=" + title + ", " : "")
				+ (text != null ? "text=" + text + ", " : "")
				+ (url != null ? "url=" + url + ", " : "")
				+ (imageUrl != null ? "imageUrl=" + imageUrl + ", " : "")
				+ (rssDate != null ? "rssDate=" + rssDate : "") + "]";
	}

	/**
	 * @return the title
	 */
	public String getTitle() {
		return title;
	}
	/**
	 * @param title the title to set
	 */
	public void setTitle(String title) {
		this.title = title;
	}

	// GETTERS/SETTERS
	// ...
	
}

RssReaderAsyncTask

Last but not least, el nombre lo dice todo: esta clase es un AsynTask que levantaremos para solicitar un fichero RSS, parsearlo y generar un ArrayList que le pasaremos a nuestra ListView personalizada con RssListItemAdapter. Necesitamos un AsyncTask porque la tarea va a necesitar un tiempo no muy concreto que se puede acercar al peligroso límite de los 6-7 segundos: debe descargar un fichero xml de la web y parsearlo, y todo ello en Java. Vamos que te da tiempo a levantarte y a echarte un cubito de hielo en el café. Algo que nos facilita las cosas es que podemos pasar una referencia de la Activity que le llama así que desde el AsyncTask podemos meter mano en el Activity sin problemas, cosa que con si la hicieras con un Thread Android diría que caca, Exception y apurtu du. En un principio había separado en dos la tarea de descargar el fichero y parsearlo, pero es una tontería porque el propio parser te puede descargar el fichero con un simple GET. Dejo el método porque en un futuro puede venir bien si necesitamos un descargador más evolucionado (proxy, parámetros post, usuario/password, ...).

package info.pello.android.listadapter;


import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;


import android.os.AsyncTask;
import android.util.Log;
import android.widget.Toast;


/**
 * Extends AsyncTask to get RSS contents and parse the to populate
 * a ListAdapter
 * @author Pello Xabier Altadill Izura
 * @greetz any
 */
public class RssReaderAsyncTask extends AsyncTask {
	
	// We well keep a reference to our caller activity
	// so we can attach/detach in case of activity is destroyed in a rotation
	private MainActivity mainActivity;
	private ArrayList rssItems;

	/**
	 * Default constructor
	 * @param mainActivity
	 */
	public RssReaderAsyncTask (MainActivity mainActivity) {
		attach(mainActivity);
	}
	
	/**
	 * sets mainActivity reference
	 * @param mainActivity
	 */
	public void attach(MainActivity mainActivity) {
		this.mainActivity = mainActivity;
	}

	/**
	 * when task is finished this reference is not needed any longer
	 */
	public void detach () {
		this.mainActivity = null;
	}
	
	/**
	 * This is called before doInBackground and is a perfect place
	 * to prepare the progress Bar.
	 */
	@Override
	protected void onPreExecute () {
		Toast.makeText(this.mainActivity, "Starting Async Task", Toast.LENGTH_SHORT).show();
	}
	
	/**
	 * This is where the task begins and runs
	 * String... url declares variable arguments list,
	 * and we can get values using indexes: url[0], url[1],...
	 * Whenever we consider that we make som progress we can
	 * notify through publishProgress()
	 */
	@Override
	protected Void doInBackground(String... url) {
		Log.d("PELLODEBUG","AT> URL passed to AsyncTask: " + url[0]);
		String sampleRSS ="";
		try {
			rssItems = this.parseXML(url[0]);
		} catch (Exception e) {
			Log.d("PELLODEBUG","AT> Exception processing RSS: " + e.getMessage());
		}
		// With this call we notify to progressUpdate
		Log.d("PELLODEBUG","AT> doInBackbround publishes progress");

		// TODO Auto-generated method stub
		return null;
	}
	

	/**
	 * This method is called when we call this.publishProgress
	 * and can be used to update contents,progress bars,... in the Activity
	 */
	@Override
	protected void onProgressUpdate(String... item) {
		Log.d("PELLODEBUG","AT> onProgressUpdate> on progress... ");

	}
	
	/**
	 * called when task is finished.
	 */
	@Override
	protected void onPostExecute(Void unused) {
		Toast.makeText(this.mainActivity, "Finished.", Toast.LENGTH_SHORT).show();
		Log.d("PELLODEBUG","AT> onPostExecute was called: ");
		this.mainActivity.refreshList(rssItems);
		//this.mainActivity.getProgressBar1().setVisibility(ProgressBar.INVISIBLE);

	}

	/**
     * parseXML
	 * parses XML content from a given URL
     * @return ArrayList with parsed data
     */
    private ArrayList parseXML (String rssContent) {
		ArrayList rssItemsArray = new ArrayList();

		// If we do it using a String
		//Reader xml = new StringReader("..");
		//Document doc = builder.parse(new InputSource(xml));
		
    	DocumentBuilder builder;
    	DocumentBuilderFactory builderFactory;
		try {
			builderFactory = DocumentBuilderFactory.newInstance();
			
			// I tried this features to relax the parser but with no effect
			// See: http://xerces.apache.org/xerces2-j/features.html
			builderFactory.setNamespaceAware(true);
			builderFactory.setValidating(false);
			//builderFactory.setFeature("http://apache.org/xml/features/continue-after-fatal-error", true);
			
			builder = builderFactory.newDocumentBuilder();

    	Document doc=builder.parse(rssContent);
    	NodeList items =doc.getElementsByTagName("item");
    	
    	for (int i=0;i response = new BasicResponseHandler();
			
			String responseContents = httpClient.execute(request,response);

			if (responseContents != null && responseContents.length() > 0) {
				result += responseContents;
			} else {
				result += "Error\n"+responseContents;
			}

		} catch (ClientProtocolException e) {
			result += "Unexpected ClientProtocolException" + e.getMessage();
		} catch (IOException e) {
			e.printStackTrace();
			result += "Unexpected IOException" + e.getMessage();
		}
		Log.d("PELLODEBUG","RssReader> Result: "+ result);
		return result;
    }

}

Pantallazo del programa
Problemas en el parseo

Aunque en el pantallazo sale el RSS de esta página, previamente estaba probando con otro RSS, pero el parser (que es el oficial) me soltaba una excepción porque al parecer no le gustaba el contenido de la etiqueta dc:creator, que contenía un salto de línea una etiqueta de imagen... sorry mate. Igual el que estaba mal era mi parser, de todas formas he tratado de alterar la configuración del parser con las features y NO ha funcionado. Quizá una solución sea meter nuestro propio Handler. O lo más directo, usar un parser tipo JSOUP que es transigente con los errores. Al menos para el parser oficial no he visto una solución clara.

by Pello Altadill 08/22/2013 21:19:24 - 1492 hits

Enviar emails desde php... y que funcione

Ya que acabo de hablar de mandar correos utilizando Spring voy a retomar el tema del envío de correos desde php. Recordaba que en su día había posteado algo al respecto, y debo pedir disculpas si en su día alguien confió de ese post y no le funcionó. Lo que puse ahí mejoraba ligeramente lo que se solía explicar habitualmente pero en muchos escenarios y en años posteriores eso es raro que funcione. De todas formas ese p...

by Pello Altadill 08/22/2013 11:47:19 - 938 hits

More »

Enviando emails con Spring

Para convertirme en un auténtico frontender me he agenciado unas super gafas de pasta de un grosor lo suficientemente desmesurado como para hacerme respetar en la tribu del asincronismo y de los programas de cuatro líneas. Nótese el tintado rojizo de las lentes que me proporcionan una experiencia mucho más responsiva con el entorno que me rodea. Por si no lo sabías, los cristales negros están deprecados. ...

by Pello Altadill 08/22/2013 00:03:03 - 1024 hits

More »

A tortas con los BroadcastReceiver de Android

Un BroadcastReceiver es un componente de Android que una vez registrado reacciona cuando el sistema envía los Intent para los que estaba preparado. Un caso muy típico es el del Receiver que se registra para que reaccione cada vez que se recibe una llamada de teléfono o un SMS. Ese BroadcastReceiver capta el Intent y en un método onReceive hace lo que tenga que hacer. Se supone que es muy fácil y todo muy bonito. Para Android tienes infinidad de ejemplos colgados por la red. Lo que nunca pued...

by Pello Altadill 08/21/2013 14:22:14 - 1465 hits

More »

Ejemplo de REST con Node.js + express y MongoDB

Alrededor de Node.js existen infinidad de proyectos en plena ebullición, y prueba de ello es que solamente para desarrollar webs existen varios frameworks, algunos basados en el clásico MVC y otros más orientados a servir recursos REST. Ese es el caso de Express, uno de los frameworks más populares disponibles para Node.js. En este post veremos cómo crear un servidor que ofrezca recursos REST a un frontend. Si no te gusta javascript, amijo, jejej, no sé: deja la web y vuelve al Cobol, porque esto v...

by Pello Altadill 08/20/2013 23:15:49 - 1017 hits

More »

Ejemplo de Web Workers, los hilos javascript

Desde que los navegadores tenían soporte para Javascript, su código siempre se ha ejecutado en un único hilo. Los scripts que se desarrollan para el navegador están orientados a eventos y estos se van ejecutando en una especie de cola. Es decir, conforme se van generando los eventos estos van a una cola de tareas y Javascript los va procesando de uno en uno en un bucle. Web Workers, hilos en tu navegador HTML5 trae como novedad los Web Workers, que son procesos de javascript separado...

by Pello Altadill 08/20/2013 12:51:28 - 2016 hits

More »

Services en Android

En Android no solamente hay aplicaciones de ventanas o activities. Hay muchos otros tipos de elementos como por ejemplo los Services. Los servicios nos permiten dejar un programa residente en la memoria y disponible para cualquiera que lo necesite. Los servicios no tienen interfaz gráfico ni nada, son programas pensados para ser utilizados desde las Activities o incluso desde otros servicios. Un service puede crearse para dar servicio -privado- a una sola aplicación o puede hacerse público (a través ...

by Pello Altadill 08/19/2013 22:29:37 - 1617 hits

More »

Creando módulos en Node.js

Lo del servidor web con pocas líneas o refactorizado para que tenga forma de clase va quedando más o menos apañado. Pero en cualquier proyecto que vaya creciendo vamos a necesitar bastantes más cosas y claro, ir metiendo todo el programa en un único fichero, aunque algún programador bajeril pueda decir que es más simple, pues está muy feo. Como en cualquier otro lenguaje y sea el tipo de programación que se...

by Pello Altadill 08/18/2013 22:45:09 - 634 hits

More »

AsyncTask en Android

Los AsyncTask están recomendados para tareas concretas y finitas, como por ejemplo descargar el contenido de una URL. Si lo que necesitas es una especie de Hilo que esté ejecutándose contínuamente tendrías que usar los Thread convencionales (no se detienen ni al pulsar home ni al pulsar Back) o directamente crear un Service de Android. Para crear un AsyncTask debes extender la clase AsyncTask indicándole tres tipos de clase en la declaración, como por ejemplo AsyncTask<String, String, V...

by Pello Altadill 08/18/2013 11:06:08 - 1423 hits

More »