Pestañas, Fragments y Carga Asíncrona de Imágenes Remotas

appbar-sherlock-demo-2-2-1Desde la aparición de Android 3.0, el diseño de las apps ha cambiado mucho. Antes ya existían las pestañas, pero no quedaban ni remotamente igual de bien (visualmente) de lo que quedan las que se integran en la actionbar, esa barra superior que incluyen las apps diseñadas a partir de 3.0. Sin embargo, no es necesario excluir a todos los dispositivos que no tengan esa versión de Android. Los que tengan 2.1 en adelante pueden aprovecharse de estas características si programas usando una librería externa: La Action Bar Sherlock.

Idea inicial

Vamos a hacer una demo, una app que no tendrá datos dinámicos, pero que sirve para ver qué aspecto tendría una app de verdad. Partiremos del supuesto de una app que sirva para explorar recetas de cocina – por ejemplo – y nos centraremos en el detalle de la misma. Dicho detalle debe mostrar a) la foto de la receta ya preparada, que se descargará del blog de cocina adecuado; b) datos de la receta (puntuación de los usuarios, coste económico aproximado, autor y datos del blog en el que se aloja; y c) una lista con los comentarios de los usuarios.

Como hemos dicho, se tratará de una demo, por lo que todos los datos irán hardcodeados  – esto es, definidos en el código.

Toda esa información (foto, datos y comentarios) se verán en tres ‘pantallas’ diferentes cuya navegación irá a través de las Tabs o Pestañas.

La teoría tras la idea

Usando la support library de android y la ActionBar Sherlock, podremos desarrollar esta demo para cualquier dispositivo con Android 2.1 o superior. Esto nos permitirá tener una barra superior (se puede configurar para que sea inferior, pero vamos a quedarnos con lo mas sencillo) en la que aparecerán por la izquierda las pestañas y por la derecha las opciones del menú contextual configuradas para que aparezcan ahí – de haberlas. Esa es la ActionBar.

La lógica de cada pestaña (la que muestra la imagen, la que muestra los datos y la que carga y muestra la lista) irá en unas clases específicas llamadas Fragments. Los Fragments permiten encapsular lógica que interactúa con la Interfaz de Usuario y emplearla en una o varias Actividades tantas veces se desee. En los tablets, al disponer de mas espacio de pantalla para mostrar contenido, es muy habitual que se tome una única Actividad y muestre contenido que en un móvil posiblemente habría necesitado dos o mas Actividades. Por ejemplo: en el móvil, elijes un contacto de una lista, y te lleva a otra pantalla (una segunda Actividad) dónde ves el detalle del contacto. En el tablet la lista ocupa una parte lateral de la pantalla y el detalle aparece en el otro lado, todo en la misma Actividad. Lo que vamos a hacer en este tutorial es, en una única actividad, crear unas tabs – una especie de botones en la Action Bar – y según pulsemos aparecerá el contenido de uno u otro Fragment.

Aprovecho, además, para introducir la carga asíncrona de imágenes. En general, cualquier proceso del que no sepamos cuánto va a tardarse en completar (ej: acceso a base de datos, comunicación con servidores, descarga de imágenes, etc) dicho proceso debe ir en un hilo de procesado diferente del principal. Hay veces que se bloquea la Interfaz, como es el caso del login, y es necesario. Pero en otras no tanto, como en una lista de contactos que cargue el avatar del mismo de un servidor: tú quieres poder llamar al contacto Fulano y si tienes que esperar a que cargue la imagen de su avatar…

Nosotros manejamos el hilo principal en el código de las Actividades, por ejemplo. Si en nuestro onCreate o  en onResume descargamos la imagen de una url y la asignamos al ImageView de turno, hasta que no termine de descargarse no seguirá ejecutándose el código, quedará ahí “congelada”. Y ahí es cuando salen los mensajes del sistema de “La app X está tardando demasiado ¿quiere detenerla o esperar?”. Que salga eso es una forma segura de lograr que desinstalen nuestra app y nos la pongan por los suelos.

Hay en Android formas de tratar con los procesos asíncronos (en un hilo diferente del de la Interfaz) como es la AsyncTask. Imlpementando un patroń Observer sencillo con interfaces de tipo Listener, los procesos asíncronos se resuelven en un coser y cantar. Pero para las imágenes remotas existe una librería muy interesante que nos permite solucionar esto en una línea de código. Os estoy hablando de Prime.

Requisitos

  • Que nuestro proyecto apunte desde el 2.2 (o 2.1 si se desea, pero casi no quedan dispositivos que aún corran esa versión tan antigua) y tengan por objetivo la mas reciente 4.X.
  • Tendremos que tener la última versión de la android support library – descargable a través del SDK Manager de Eclipse.
  • La última versión de la librería Action Bar Sherlock. Se descarga de su web. Una vez metida en el Build Path, hay que cambiar el Theme de la aplicación a uno de los propios que vienen con la librería (yo he usado, por ejemplo, la @style/Theme.Sherlock.Light.DarkActionBar) o uno que nos hagamos nosotros pero que herede de uno de los de Sherlock. Si no se hace así directamente no arrancará la app.
  • La última versión de la librería Prime para poder usar la descarga asíncrona de imágenes. Una opción es descargarse el proyecto del repositorio Prime y marcarlo como librería. Luego, en Propiedades de nuestro proyecto, en la pestaña Android, abajo del todo (en el mismo sitio en que se marca el proyecto como “is Library”, añadimos los proyectos librería cuyo .jar no tengamos.
  • La última versión de Crouton. Al igual que con Prime, si no encontramos el jar, descargamos el proyecto en su última versión del repositorio, lo marcamos como librería y lo añadimos a nuestra app. Crouton nos permite sacar notificaciones en nuestra app con un poco mas de “gracia” que el clásico Toast.

Al lío

Con todo ya preparado, tenemos que hacer lo siguiente:

  • Una Actividad principal, que heredará de SherlockFragmentActivity. Si estuviéramos programando a partir de la 3.0 (ignorando, por tanto, todas las versiones anteriores) pasaríamos de todo lo relacionado con Sherlock y heredaría de FragmentActivity. En ambos casos se trata de la actividad que gestiona los Fragments.
  • Un layout para esta Actividad principal. Puede estar vacío, ya que el contenido de los Fragments entrará de forma dinámica.
  • Tres layouts para los fragments. El de los comentarios sólo tiene un ListView ocupándolo todo. El de los datos tendrá una serie de TextViews – cuyo contenido, ya dijimos, vendrá harcodeado en el propio Fragment – y un Rating Bar. El de la foto usará un elemento View propio de la librería Prime: la RemoteImageView.
  • Una clase que herede de SherlockFragment (o de Fragment, si pasamos de las versiones antiguas) para cada fragment: uno dónde se carga y muestra la foto, otro dónde se cargan y muestran los datos y el tercero con la lista de comentarios.
  • En el Manifest, permiso de acceder a Internet: android.permission.INTERNET

En los Fragments programaremos como si cada uno fuera una Actividad normal salvo por dos excepciones: 1) No hay que registrar un Fragment en el Manifest; 2) el ‘inflado’ del layout varía un poco, tal como se ve a continuación el ejemplo del layout de la foto:

package com.alberovalley.tabstutorial.fragments;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.actionbarsherlock.app.SherlockFragment;
import com.alberovalley.tabstutorial.R;
import com.alberovalley.tabstutorial.TabsFragmentActivity;
import com.handlerexploit.prime.widgets.RemoteImageView;

public class Tab1Fragment extends SherlockFragment {
	private RemoteImageView foto;
	private static final String remoteImgUrl
	= "http://grimoriodelchef.files.wordpress.com/2013/01/wpid-img_20130104_151946.jpg";

	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState) {
		if (container == null) {
			// Tenemos diferentes layouts, y en uno de ellos el frame
			// que contiene este fragment no existe. El fragment
			// se puede aún crear desde su estado guardado, pero no hay
			// razón alguna para intentar crear su jerarquía de vistas ya que
			// no se mostrará. Notar que esto no es necesario - podríamos simplemente
			// ejecutar el código de debajo, dónde crearíamos y devolveríamos
			// la jerarquía de vistas; simplemente nunca se usaría
			return null;
		}
		View root = inflater.inflate(R.layout.tab_frag1_layout, container, false);
		foto = (RemoteImageView) root.findViewById(R.id.tab1_img);
		return root;
	}

	@Override
	public void onResume() {
		super.onResume();
		// cargamos la imagen remota en asíncrono, sin bloquear el hilo de la UI
		Log.d("TabTutorial", "Cargando imagen remota " + remoteImgUrl);
		foto.setImageURL(remoteImgUrl);
	}

}

Como se observa, en lugar de hacer la asignación de la View RemoteImageView directamente, primero hacemos una asignación de una View genérica (root) con el layout del fragment y la usamos para encontrar las diferentes Views (en este caso, la RemoteImageView) , devolviendo por último la View genérica para que los métodos internos de la clase FragmentActivity hagan lo que deben.

Cabe observar, además, lo fácil que es la carga asíncrona de la imagen con la RemoteImageView de Prime: una única línea de código basta.

Hemos de proceder de modo similar con las otras dos clases Fragment:

La de la lista

package com.alberovalley.tabstutorial.fragments;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.ListView;

import com.actionbarsherlock.app.SherlockFragment;
import com.alberovalley.tabstutorial.R;

public class Tab3Fragment extends SherlockFragment {
	private ListView listaComentarios;
	private ArrayAdapter<String> listaAdapter;
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState) {
		if (container == null) {
			// Tenemos diferentes layouts, y en uno de ellos el frame
			// que contiene este fragment no existe. El fragment
			// se puede aún crear desde su estado guardado, pero no hay
			// razón alguna para intentar crear su jerarquía de vistas ya que
			// no se mostrará. Notar que esto no es necesario - podríamos simplemente
			// ejecutar el código de debajo, dónde crearíamos y devolveríamos
			// la jerarquía de vistas; simplemente nunca se usaría
			return null;
		}
		View root = inflater.inflate(R.layout.tab_frag3_layout, container, false);
		listaComentarios = (ListView) root.findViewById(R.id.tab3_lista);
		listaAdapter = new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1);
		listaComentarios.setAdapter(listaAdapter);
		return root;
	}
	@Override
	public void onResume() {
		// TODO Auto-generated method stub
		super.onResume();
		listaAdapter.add("Una receta fácil y deliciosa (Fulano)");
		listaAdapter.add("Gracias! Me encantan las trufas... y todo lo de chocolate xDDD (Mengan)");
		listaAdapter.add("Estoy deseando probarlas (Zut)");
		listaAdapter.add("Qué pintaza! Quierooooooooooo (Pereng)");

	}
}

La de los datos y el RatingBar

package com.alberovalley.tabstutorial.fragments;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.RatingBar;
import android.widget.RatingBar.OnRatingBarChangeListener;

import com.actionbarsherlock.app.SherlockFragment;
import com.alberovalley.tabstutorial.R;

import de.keyboardsurfer.android.widget.crouton.Crouton;
import de.keyboardsurfer.android.widget.crouton.Style;

public class Tab2Fragment extends SherlockFragment implements OnRatingBarChangeListener {
	private RatingBar puntuar;
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState) {
		if (container == null) {
			// Tenemos diferentes layouts, y en uno de ellos el frame
			// que contiene este fragment no existe. El fragment
			// se puede aún crear desde su estado guardado, pero no hay
			// razón alguna para intentar crear su jerarquía de vistas ya que
			// no se mostrará. Notar que esto no es necesario - podríamos simplemente
			// ejecutar el código de debajo, dónde crearíamos y devolveríamos
			// la jerarquía de vistas; simplemente nunca se usaría
			return null;
		}
		View root = inflater.inflate(R.layout.tab_frag2_layout, container, false);
		puntuar = (RatingBar) root.findViewById(R.id.tab2_puntuar);
		puntuar.setOnRatingBarChangeListener(this);
		return root;
	}
	@Override
	public void onRatingChanged(RatingBar ratingBar, float rating,
			boolean fromUser) {
		Crouton.makeText(getActivity(), "Puntuación elegida: [" + rating +"]", Style.INFO).show();

	}
}

En este último, resaltamos el uso de Crouton para crear una notificación emergente cuando se hace uso del Rating Bar. Fácil, sencillo y limpio.

Cuando hay que usar el contexto o la actividad en algún método (ej: la creación del Adapter de la lista) en un Fragment, podemos obtener la Actividad en la que está ‘incrustado’ el Fragment con getActivity(), que devuelve una FragmentActivity. Sherlock dispone de su propio método getSherlockActivity() que sólo devuelve una SherlockFragmentActivity, pero para lo que estamos haciendo nos da igual tanto una que otra.

El código de la Actividad, como se puede ver a continuación, es bastante sencillo y claro:

package com.alberovalley.tabstutorial;

import android.os.Bundle;
import android.support.v4.app.FragmentTransaction;
import android.util.Log;

import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.app.ActionBar.Tab;
import com.actionbarsherlock.app.ActionBar.TabListener;
import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.alberovalley.tabstutorial.fragments.Tab1Fragment;
import com.alberovalley.tabstutorial.fragments.Tab2Fragment;
import com.alberovalley.tabstutorial.fragments.Tab3Fragment;

public class TabsFragmentActivity extends SherlockFragmentActivity implements TabListener {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		// Obtenemos la barra de esta actividad
		ActionBar bar = getSupportActionBar(); // sin Sherlock sería getActionBar()

		// Creamos tantas tabs como vayamos a necesitar
		ActionBar.Tab tab1 = bar.newTab();
		ActionBar.Tab tab2 = bar.newTab();
		ActionBar.Tab tab3 = bar.newTab();
		// asignamos una etiqueta (setText) y una identificación (setTag) a cada una.
		tab1.setText("Foto");
		tab1.setTag("Foto");

		tab2.setText("Datos");
		tab2.setTag("Datos");

		tab3.setText("Comentarios");
		tab3.setTag("Comentarios");

		// Se puede asignar un icono a cada pestaña con tab1.setIcon(Drawable icon)

		// añadimos el Listener para cuando activemos las tabs
		tab1.setTabListener(this);
		tab2.setTabListener(this);
		tab3.setTabListener(this);

		// con todo listo, asignamos las tabs a la barra
		bar.addTab(tab1);
		bar.addTab(tab2);
		bar.addTab(tab3);

		// activar el modo de navegación por pestañas,
		// mejor al final, para que no de problemas al rotar
		bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
	}

	@Override
	public void onTabSelected(Tab tab, FragmentTransaction ft) {
		String tag = (String) tab.getTag();
		Log.d("TabTutorial", "Elegido tab con tag: " + tag);
		// Creamos un Fragment nuevo (con new Fragment) del tipo adecuado según la elección
		// y reemplazamos lo que hubiera antes usando
		// el método replace del FragmentTransition que recibe este método
		if (tag.equalsIgnoreCase("Foto")){
			Tab1Fragment frag = new Tab1Fragment();
			ft.replace(android.R.id.content, frag);
		}else if (tag.equalsIgnoreCase("Datos")){
			Tab2Fragment frag = new Tab2Fragment();
			ft.replace(android.R.id.content, frag);
		}else if (tag.equalsIgnoreCase("Comentarios")){
			Tab3Fragment frag = new Tab3Fragment();
			ft.replace(android.R.id.content, frag);
		}else{
			Log.w("TabTutorial", "tag de tab no registrada: " + tag);
		}
	}

	@Override
	public void onTabUnselected(Tab tab, FragmentTransaction ft) {
		// si se necesita hacer algo especial cuando un tab
		// deja de estar seleccionado
	}

	@Override
	public void onTabReselected(Tab tab, FragmentTransaction ft) {
		// si se necesita hacer algo especial cuando un tab
		// vuelve a ser seleccionado
	}

}

Nada mas, con estas cuatro clases y los tres layouts de los fragments ya está todo listo para ejecutar y probar.
Cómo es habitual, dejo enlace al repo dónde está todo el código de este tutorial aquí.

Anuncios

2 comentarios to “Pestañas, Fragments y Carga Asíncrona de Imágenes Remotas”

  1. hola, primero que nada muchas gracias por el aporte.
    Quise ver el link que has puesto, y me lleva a otro tutorial, no a donde esta el codigo

  2. @cristian ¡Hola! Vaya, se me volvieron locos algunos enlaces y no me había dado cuenta de todos. Ya debería apuntar al repositorio de github. ¡Saludos!

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: