El ejemplo se verá de la siguiente manera:

Disposición inicial

Empecemos descargando el código que debe ser importado hacia un nuevo proyecto es muy similar al del capítulo anterior, pero revisemos las diferencias:

  • El XMLParser ahora obtiene no solo el link y título de cada artículo, también su autor, fecha, descripción y una imagen.
  • Para ser más ordenados, la información se guardará utilizando una clase de Java que represente a cada artículo llamada Element (src/com/android/mdw/demo/Element.java)
  • La Activity principal (Main) hereda de ListActivity y no de Activity facilitando y permite caminos cortos para cosas específicas a una Activity que muestra un listado de cosas. Por lo mismo, en el layout es necesario un ListView con el identificador @android:id/list

Además, tiene las especificaciones de la guía anterior.

  • Es necesario darle permiso a la aplicación para que pueda accesar a Internet,  tener un LinearLayout con orientación vertical en el archivo de diseño principal por trabajar con una LisActivity también necesitamos el ListView con id @android:id/list y tener una forma de reconocer XML.

Diseño

Trabajamos con Views del capítulo anterior como: LinearLayout, ListView, TextView, Button y agregamos nuevas Views.

  • ImageView: muestra una imagen que puede cargarse de diferentes fuentes para el ejemplo vamos a obtenerla de una dirección de Internet (etiqueta img de HTML dentro del artículo) revisar (2d Graphics).
  • Menu y MenuItem: en este capítulo aprenderás cómo hacer un menú de opciones en XML, revisar (Menus).

Para el layout principal, repetimos el procedimiento del capítulo anterior de nuevo recordando que el identificador (android:id) del ListView debe ser @android:id/list.

Además, necesitaremos 3 Views adicionales:

  • Para el menú menu.xml
  • Para la vista previa dentro de la aplicación showelement.xml
  • Para cada fila del listado row.xml

Para agregar estos Views, hacemos click derecho sobre la carpeta /res/layout luego New > (nuevo) y other… (otro):

En el diálogo que aparece seleccionamos archivo XML de Android y hacemos click en Next > (siguiente):

Escribimos el nombre del archivo (es importante incluir la extensión XML), en el ejemplo el nombre es menu.xml y hacemos click en el botón Finish (finalizar):

Este procedimiento es necesario cada vez que necesitemos un Layout nuevo, para esta aplicación serán necesarios 3: menu.xml, row.xml y showelement.xml describiremos a detalle cada uno de ellos.

View para el menú: menu.xml

Utiliza un elemento menú y dos elementos ítem para cada opción, le pondremos a cada ítem texto y el icono que trae por defecto la aplicación, el archivo XML debe quedar así:

<!--?xml version="1.0" encoding="utf-8"?-->

El menú debería verse de la siguiente forma:

View para preview: showelement.xml

En esta vista previa mostraremos el título del artículo, el autor y la descripción, necesitamos varios TextView dentro de un LinearLayout con orientación vertical, el archivo XML debe quedar así:

<!--?xml version="1.0" encoding="utf-8"?-->

La vista previa debería verse de la siguiente forma:

View para fila: row.xml

En este capítulo en cada una de las filas de la lista no mostraremos dos líneas como en el anterior, ahora vamos a tener una imagen del elemento y su título agrupados por un LinearLayout con orientación horizontal, el archivo XML debe quedar de la siguiente forma:

<!--?xml version="1.0" encoding="utf-8"?-->

Clases de apoyo

En esta versión del lector de feeds tenemos varias clases que apoyan para facilitar el desarrollo:

  • Element: guarda los datos de cada artículo en el listado.
  • MyApp: clase de aplicación para representar datos volátiles dentro de la aplicación.
  • MyAdapter: adaptador personalizado que permite mostrar la imagen en cada fila del listado.

Para la vista previa del artículo necesitamos otra Activity que es otra clase que llamaremos ShowElement.java para agregar una clase es necesario hacer click derecho sobre com.android.mdw.demo (bajo src) luego New (nuevo) y por último Class (clase).

Después colocamos el nombre “MyApp” para el ejemplo y la superclase (clase de la que hereda) android.app.Application para el ejemplo y hacemos click en finalizar.

El proceso es necesario para MyApp, MyAdapter y ShowElement la clase Element va incluida, ahora veamos el contenido de cada una de estas clases:

Element

Haremos un objeto Element, para aumentar el orden porque ahora guardaremos más información de cada elemento del listado obtenido del feed. Esta clase está incluida en los archivos de prueba y no es muy complicada, maneja métodos para colocar y obtener los atributos de cada elemento como: Título, Enlace, Autor, Descripción, Fecha de publicación, Imagen.

SetImage es el método diferente y se debe a la forma en que obtenemos las imágenes. El parser XML buscará dentro del contenido del artículo la primera ocurrencia de la etiqueta img de HTML y en algunos casos la única es la foto del autor del artículo. En ese caso, tomamos prestada la imagen del avatar de Twitter de @maestros el URL es:

http://a1.twimg.com/profile_images/82885809/mdw_hr_reasonably_small.png

La URL se recibe como parámetro en este método, si al tratar de obtener la imagen algo fallara entonces también se establece la imagen del avatar de twitter. Para obtener la imagen en base a un URL se utiliza la ayuda de la función loadFromUrl que devuelve un Bitmap.

Este método abre una conexión hacia el URL especificado, luego decodifica el flujo (stream) de bytes recibido y en base a ellos construye un objeto Bitmap. El código de la clase Element (ya incluido en el código base de la guía) es el siguiente:

package com.android.mdw.demo;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

public class Element {
	static SimpleDateFormat FORMATTER = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z");
	private String title;
	private String author;
	private String link;
	private Bitmap image;
	private String description;
	private Date date;

	public String getTitle() {
		return this.title;
	}

	public String getLink() {
		return this.link;
	}

	public String getDescription() {
		return this.description;
	}

	public String getDate() {
		return FORMATTER.format(this.date);
	}

	public Bitmap getImage(){
		return this.image;
	}

	public String getAuthor(){
		return this.author;
	}

	public void setTitle(String title) {
		this.title = title.trim();
	}

	public void setLink(String link) {
		this.link = link;
	}

	public void setDescription(String description) {
		this.description = description.trim();
	}

	public void setDate(String date) {
		try {
			this.date = FORMATTER.parse(date);
		} catch (java.text.ParseException e) {
			e.printStackTrace();
		}
	}

	public void setImage(String image){
		if (image.contains("autor")) {
			image = "http://a1.twimg.com/profile_images/82885809/mdw_hr_reasonably_small.png";
		}
		try {
			this.image = loadFromUrl(new URL(image));
		} catch (Exception e) {
			try {
				this.image = loadFromUrl(new URL("http://a1.twimg.com/profile_images/82885809/mdw_hr_reasonably_small.png"));
			} catch (MalformedURLException e1) {}
		}
	}

	public void setAuthor(String author){
		this.author = author;
	}

	public String toString(){
		return this.title;
	}

	private Bitmap loadFromUrl(URL link) {
		Bitmap bitmap = null;
		InputStream in = null;
		try {
			in = link.openConnection().getInputStream();
		    bitmap = BitmapFactory.decodeStream(in, null, null);
		    in.close();
		} catch (IOException e) {}
		return bitmap;
	}
}

MyApp

En el capítulo anterior el listado de artículo los representamos como una lista estática dentro de la clase de la activity principal, mencioné que esa no era la forma correcta por que debían utilizarse clases de aplicación y para eso utilizaremos MyApp.

No es posible utilizar una variable local o una variable de instancia porque la Activity es constantemente destruida y creada de nuevo. Por ejemplo, al rotar el teléfono. A pesar de estar representado dentro de una clase esta información no deja de ser volátil, para almacenamiento permanente es necesario una base de datos de SQLite.

Para decirle a la aplicación que es de tipo MyApp es necesario editar el manifest AndroidManifest.xml y en los atributos de la etiqueta aplicación agregar android:name="MyApp"

Inicialmente decía:

<application android:icon="@drawable/icon" android:label="@string/app_name">

Al modificarlo debe decir:

<application android:icon="@drawable/icon" android:label="@string/app_name" android:name="MyApp">

Dentro de la clase MyApp vamos a guardar dos cosas:

  • El listado de los artículos
  • La opción seleccionada por el usuario para visualizar el artículo (ya sea en una vista previa dentro de la aplicación o en el navegador)

Además de estas dos variables de instancia, vamos a incluir métodos para guardar y devolver estas variables (getters y setters). Para representar la opción elegida por el usuario utilizaremos enteros dentro de la Activity principal, estos están incluidos en el código base y fueron definidos así:

final static int APP_VIEW = 1;
final static int BROWSER_VIEW = 2;

Al iniciar la aplicación, colocaremos el valor de APP_VIEW en el campo que guarda la preferencia del usuario dentro de la clase de aplicación, el código de la clase MyApp queda de la siguiente forma:

package com.android.mdw.demo;

import java.util.LinkedList;
import android.app.Application;

public class MyApp extends Application {
	  private LinkedList data = null;
	  private int selectedOption = Main.APP_VIEW;

	  public LinkedList getData(){
		  return this.data;
	  }
	  public void setData(LinkedList d){
		  this.data = d;
	  }

	  public int getSelectedOption(){
		  return this.selectedOption;
	  }

	  public void setSelectedOption(int selectedOption) {
		  this.selectedOption = selectedOption;
	  }
}

MyAdapater

La clase MyAdapter será una personalización de un ArrayAdapter, un adaptador que recibe un arreglo (o listado) de elementos. La clase MyAdapter hereda de ArrayAdapter<Element>, dentro de la clase manejaremos dos variables de instancia, un LayoutInflater esta clase se utilizar para instanciar el diseño a partir de un archivo de XML hacia un View y además un listado (LinkedList) de objetos (en este caso, artículos, representados por la clase Element).

Dentro del constructor, llamamos al padre (super) y asignamos a los campos (variables de instancia). Además, dado que queremos personalizar la representación (rendering) de cada fila, necesitamos sobrecargar revisar (Override) el método getView.

Dentro de este método, cuando sea necesario instanciamos el archivo de su diseño correspondiente row.xml y le asignamos valor a sus Views (imagen y texto) a partir de la información guardada en el listado de artículos.

El código de la clase MyAdapter es el siguiente:

package com.android.mdw.demo;

import java.util.LinkedList;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

public class MyAdapter extends ArrayAdapter {
	LayoutInflater inf;
	LinkedList objects;
	public MyAdapter(Context context, int resource,
int textViewResourceId,
			LinkedList objects) {
		super(context, resource, textViewResourceId, objects);
		this.inf = LayoutInflater.from(context);
		this.objects = objects;
	}

	public View getView(int position, View convertView,
   ViewGroup parent){
		View row = convertView;
		Element currentElement = (Element)objects.get(position);

		if (row == null) {
			row = inf.inflate(R.layout.row, null);
		}

		ImageView iv = (ImageView) row.findViewById(R.id.imgElement);
		iv.setImageBitmap(currentElement.getImage());
		iv.setScaleType(ImageView.ScaleType.FIT_XY);

		TextView tv = (TextView) row.findViewById(R.id.txtElement);
		tv.setText(currentElement.getTitle());

		return row;
	}

}

ShowElement

Por último tenemos la Activity que nos mostrará la vista previa del artículo seleccionado, cada vez que se agrega un Activity es necesario especificarlo en el manifest AndroidManifest.xml, bajo la etiqueta de aplicación (<application>) vamos a colocar:

<activity android:name=".ShowElement"></activity>"

Para pasar la información entre las Activities utilizaremos la información extra puede llevar un intent previo a levantar una Activity. Disponemos de un diccionario que en nuestro caso utilizaremos para mandar un entero representando la posición del elemento que queremos visualizar.

Para empezar obtenemos elintent que levanto esta activity que debería traer la posición del elemento a visualizar, si en caso fuera nulo (el intent que levanto esta aplicación no manda nada) entonces se le asigna el valor por defecto (esperamos que la posición sea mayor o igual que 0, entonces para distinguir un error podemos asignar -1).

Intent it = getIntent();
int position = it.getIntExtra(Main.POSITION_KEY, -1);

Con la posición recibida, buscamos a través de la clase de aplicación MyApp y llamando al método getData obtenemos el artículo que nos interesa:

MyApp appState = ((MyApp)getApplication());
Element e = appState.getData().get(position);

Luego colocamos esos valores en los campos correspondientes definidos en su diseño showelement.xml si en caso no se recibiera una posición válida, se regresa a la Activity principal enviando la posición -1 para indicar que hubo un error:

Intent backToMainActivity = new Intent(this, Main.class);
backToMainActivity.putExtra(Main.POSITION_KEY, -1);
startActivity(backToMainActivity);

Al finalizar, el código de esta clase es el siguiente:

package com.android.mdw.demo;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.widget.TextView;

public class ShowElement extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.showelement);
		Intent it = getIntent();
		int position = it.getIntExtra(Main.POSITION_KEY, -1);
		if (position != -1) {
			MyApp appState = ((MyApp)getApplication());
			Element e = appState.getData().get(position);

			TextView txtTitle = (TextView)findViewById(R.id.txtTitle);
			txtTitle.setText(e.getTitle());

			TextView txtAuthor = (TextView)findViewById(R.id.txtAuthor);
			txtAuthor.setText("por " + e.getAuthor());

			TextView txtDesc = (TextView)findViewById(R.id.txtDesc);
			txtDesc.setText(e.getDescription());

		} else {
			Intent backToMainActivity = new Intent(this, Main.class);
			backToMainActivity.putExtra(Main.POSITION_KEY, -1);
			startActivity(backToMainActivity);
		}
    }
}

Esas son las clases adicionales luego el trabajo es sobre la Activity principal: la clase Main.java

Mostrar datos siguiendo el diseño

La función auxiliar setDatade la guía anterior se ve drásticamente reducida ya que ahora utilizamos nuestro adaptador y por tener una activity que hereda de ListActivity la asignación al ListView lo hacemos con setListAdapter.

private void setData(){
    	this.setListAdapter(new MyAdapter(this, R.layout.row, 0, appState.getData()));
}

Esta herencia de nuestra Activity, también nos permite colocar la función onListItemClick para especificar la operación a realizar cuando el usuario presiona click sobre un elemento. Validamos la opción seleccionada por el usuario guardada en la clase de aplicación y dependiendo del caso levantamos un intent diferente.

protected void onListItemClick(ListView l, View v, int position, long id) {
	super.onListItemClick(l, v, position, id);
	Intent nextActivity = null;

	if (appState.getSelectedOption() == APP_VIEW) {
		nextActivity = new Intent(this, ShowElement.class);
		nextActivity.putExtra(POSITION_KEY, position);
	} else {
		LinkedList data = appState.getData();
		nextActivity = new Intent(Intent.ACTION_VIEW,
				Uri.parse(data.get(position).getLink()));

	}

	this.startActivity(nextActivity);
}

Permanece la variable de instancia para el diálogo de progreso y el manejador es ligeramente distinto porque ya no recibe los datos a través del mensaje.

private final Handler progressHandler = new Handler() {
	public void handleMessage(Message msg) {
		setData();
		progressDialog.dismiss();
    }
};

Carga de datos

La carga de datos no cambia mucho, seguimos teniendo el diálogo de progreso pero ahora ya no necesitamos mandar los datos reconocidos por el parser a través del manejador si no puedo guardarlos en la clase de aplicación directamente y mando un mensaje vacío.

private void loadData() {
	progressDialog = ProgressDialog.show(
			Main.this,
			"",
			"Por favor espere mientras se cargan los datos...",
			true);
	new Thread(new Runnable(){
		@Override
		public void run() {
			XMLParser parser = new XMLParser(feedUrl);
			appState.setData(parser.parse());
			progressHandler.sendEmptyMessage(0);
		}}).start();
}

Agregando el menú de opciones

Necesitamos indicarle qué hacer cuando el usuario presione la tecla de menú en el teléfono, como en este caso construimos el menú en un XML solo es necesario crear una instancia.

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.layout.menu, menu);
    return true;
}

Además, requerimos sobrecargar otra función que se dispara cuando el usuario elige alguna de las opciones del menú. Aquí guardaremos en la clase de aplicación lo que sea que el usuario haya elegido.

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
    	case R.id.mmElementApp:
    		appState.setSelectedOption(APP_VIEW);
    		break;
    	case R.id.mmElementBrw:
    		appState.setSelectedOption(BROWSER_VIEW);
    		break;
    }

    return true;
}

Llamando todo desde la función onCreate

Dentro del cuerpo de la function oCreate inicializamos la variable para nuestra clase de aplicación:

appState = ((MyApp)getApplication());

Validamos si el intent lo levantó alguna otra Activity y si viene un -1 en el mensaje mostramos un error:

Intent it = getIntent();
int fromShowElement = it.getIntExtra(POSITION_KEY, 0);
if (fromShowElement == -1) {
	Toast.makeText(this, "Error, imposible visualizar el elemento", Toast.LENGTH_LONG);
}

La acción sobre el click del botón no cambia mucho, la verificación la hemos cambiado y en lugar de ver si el adaptar ya tiene datos revisamos si la clase de aplicación devuelve algo diferente de null:

LinkedList data = appState.getData();
if (data != null) { … }

Descarga:

Puedes descargar el código de la aplicación completa y funcional en: UI en Android y aumentar la funcionalidad de un lector de feeds.

Conclusión

Foro Android

  • ListActivity: aprendimos la utilización de este tipo de actividad que nos da algunas ayudas cuando nuestra actividad muestra una lista de datos.
  • ImageView, Menu y MenuItem: en la parte de diseño, manejamos ImageView para mostrar imágenes, en el caso de nuestra aplicación, la imagen se obtiene de una dirección de internet y los bytes recibidos se asignan para mostrarla. También agregamos un menú con opciones MenuItems para elegir y colocar una configuración muy simple de nuestra aplicación.
  • Guardar datos a través de clases de aplicación: como un medio de guardar el estado de la aplicación vimos la forma de utilizar una clase creada por nosotros y también los mecanismos para guardar y recuperar los datos guardados.
  • Sobrecargar clases para personalización (MyAdapter): con el fin de personalizar la forma en que se muestran los datos, utilizamos como base una clase ya existente y modificamos lo necesario para que el rendering fuera a nuestro gusto.
  • Incluir Activities a nuestra aplicación: la aplicación puede contener más de una activity, vimos lo necesario para agregar y configurar en el manifest nuevas activities.
  • Enviar datos a través de intents: utilizamos intents no solo para levantar una nueva Activity si no también para enviarle datos de una Activity a otra.
  • Recibir datos a través de intents: para lograr la comunicación entre mis activities no solo fue necesario enviar los datos si no en la activity llamada también obtenerlos para su utilización.