Android Aplicación completa IV: Realizando la request asíncrona desde el main activity

Porqué una llamada asincrona

En esta parte integraremos juntos los servicios del  HTTP retriever con el XML parser para realizar una API search request desde la main activity de nuestra aplicación. La request será ejecutada asíncronamente en un hilo en background para evitar bloquear el main UI thread.En desarrollo de aplicaciones para móviles, un aspecto muy importante del comportamiento de la aplicación es una ejecución suave. La respuesta de la aplicación a la entrada del usuario debe ser rápida y la experiencia de usuario debe ser suave y fácil. La responsividad es muy significante específicamente en la plataforma Android y por eso Google ha publicado algunas guias de diseño para ello. Esto es la razón por la que la búsqueda de operaciones la iremos haciendo en background ejecutándose en threads distintos al del main UI thread.

Independientemente del hecho que la velocidad de la conexión a internet en móviles ha mejorado mucho en los últimos tiempos, permanece el hecho que descargar datos desde internet es aún una operación costosa en tiempo. Asi, no queremos pausar el main thread mientras el HTTP client espera para que los datos terminen de ser descargados. También observa que pausar el main thread UI por más de 5 segundos causará que salga un diálogo  “Application Not Responding” (ANR)  y al usuario se le da la oportunidad de matar la aplicación. Por tanto no queremos esto.

Para este propósito, vamos a tirar del API de Android y vamos a utilizar una clase built-in llamada AsyncTask. Esta clase nos permite usar fácilmente el UI thread. Según la documentación oficial: “AsyncTask habilita un uso apropiado y fácil del UI thread. Esta clase te permite realizar operaciones en background y publicar resultados en el UI thread sin tener que manipular  thread y/o handlers”.

Servicio GenericSeeker

Antes de empezar el código asíncrono, primero introducimos algunas clases de servicio que serán responsables para realizar las peticiones HTTP, parsear las respuestas XML, crear los objetos de modelo correspondientes y retornar estos al Activity que los invoca. Estas clases extenderán la clase base abstracta GenericSeeker que tiene el siguiente código:

package com.javacodegeeks.android.apps.moviesearchapp.services;
import java.net.URLEncoder;
import java.util.ArrayList;
public abstract class GenericSeeker<E> {

	protected static final String BASE_URL = "http://api.themoviedb.org/2.1/";
	protected static final String LANGUAGE_PATH = "en/";
	protected static final String XML_FORMAT = "xml/";
	protected static final String API_KEY = "<YOUR_API_KEY_HERE>";
	protected static final String SLASH = "/";
	protected HttpRetriever httpRetriever = new HttpRetriever();
	protected XmlParser xmlParser = new XmlParser();
	public abstract ArrayList<E> find(String query);
	public abstract ArrayList<E> find(String query, int maxResults);
	public abstract String retrieveSearchMethodPath();

	protected String constructSearchUrl(String query) {
		StringBuffer sb = new StringBuffer();
		sb.append(BASE_URL);
		sb.append(retrieveSearchMethodPath());
		sb.append(LANGUAGE_PATH);
		sb.append(XML_FORMAT);
		sb.append(API_KEY);
		sb.append(SLASH);
		sb.append(URLEncoder.encode(query));
		return sb.toString();
	}

	public ArrayList<E> retrieveFirstResults(ArrayList<E> list, int maxResults) {
		ArrayList<E> newList = new ArrayList<E>();
		int count = Math.min(list.size(), maxResults);
		for (int i=0; i<count; i++) {
		newList.add(list.get(i));
		}
		return newList;
	}
}

La clase GenericSeeker denota que es hábil para encontrar resultados de una clase en particular y las clases que la extiendan tendrán que proporcionar implementaciones concretas para las clases apropiadas.Utilizaremos los objetos HttpRetriever y XmlParser creados en la parte 3 de esta serie. Recordemos que el API TMDb usa URLs similares tanto para la búsqueda de movies como para la de personas:

  • Movie.search (http://api.themoviedb.org/2.1/methods/Movie.search) Para la búsqueda por una película
  • Person.search (http://api.themoviedb.org/2.1/methods/Person.search) Para la búsqueda por un actor, actriz o miembro de producción

Asi , estamos utilizando una URL base común y las extending clases tiene que proporcionar un path adicional implementando el método “retrieveSearchMethodPath”. Hay dos métodos más que deben ser implementados, find(String) y find(String,int), ambos retornan un ArrayList de objetos con las clase apropiada. La segunda puede ser utilizada para estrechar el número total de resultados. Esto puede ser útil porque el API típicamente retorna resultados que no son muy relevantes para la búsqueda que se ha hechoy y pueden ser descartados para tener un mejor rendimiento. Finalmente, no olvidemos reemplazar el valor de la variable API_KEY con un valid key desde el TMDb site.

Servicios MovieSeeker y PersonSeeker

Lo siguiente es presentar el código para las dos clases hijas: MovieSeekerPersonSeeker. Las clases son muy similares, así que mostraremos sólo una de ellas por razones de brevedad:

package com.javacodegeeks.android.apps.moviesearchapp.services;

import java.util.ArrayList;
import android.util.Log;
import com.javacodegeeks.android.apps.moviesearchapp.model.Movie;

public class MovieSeeker extends GenericSeeker<Movie> {
	private static final String MOVIE_SEARCH_PATH = "Movie.search/";
	public ArrayList<Movie> find(String query) {
		ArrayList<Movie> moviesList = retrieveMoviesList(query);
		return moviesList;
	}

	public ArrayList<Movie> find(String query, int maxResults) {
		ArrayList<Movie> moviesList = retrieveMoviesList(query);
		return retrieveFirstResults(moviesList, maxResults);
	}

	private ArrayList<Movie> retrieveMoviesList(String query) {
		String url = constructSearchUrl(query);
		String response = httpRetriever.retrieve(url);
		Log.d(getClass().getSimpleName(), response);
		return xmlParser.parseMoviesResponse(response);
	}

	@Override
	public String retrieveSearchMethodPath() {
		return MOVIE_SEARCH_PATH;
	}
}

El método private “retrieveMoviesList” es el importante de la clase. Primero construye la URL para hacer la llamada al TMDb API y entonces ejecuta la petición HTTP utilizando una instancia de la case HttpRetriever. Si la request tiene éxito, la respuesta XML es pasada al servicio XMLParser que es el responsable de mapear la respuesta a un objeto de modelo Movie. Los métodos find(String) y find(String, int) son de hecho envolturas para este método privado

Utilizar los servicios desde el main Activity

Ahora estamos preparados para usar los servicios de búsqueda desde nuestra main Activity. Primero creamos una instancia de esos servicios como sigue:

...
private GenericSeeker<Movie> movieSeeker = new MovieSeeker();
private  GenericSeeker<Person> personSeeker = new PersonSeeker();
...

Como mencionamos al comienzo de este artículo, la invocación de los métodos find lo haremos en un thread aparte del UI thread. Es ahora momento de crear nuestra implementación de AsyncTask, que para la búsqueda de pelis es lo siguiente:

...
private class PerformMovieSearchTask extends AsyncTask<String, Void, List<Movie>> {
	@Override
	protected List<Movie> doInBackground(String... params) {
		String query = params[0];
		return movieSeeker.find(query);
	}

	@Override
	protected void onPostExecute(final List<Movie> result) {
		runOnUiThread(new Runnable() {
			@Override
			public void run() {
				if (progressDialog!=null) {
					progressDialog.dismiss();
					progressDialog = null;
					}
					if (result!=null) {
						for (Movie movie : result) {
						longToast(movie.name + " - " + movie.rating);
					}
				}
			}
		});
	}
}

...

Primero declaramos que nuestra implementación extiende de la clase Asynctask. Con este encabezado estamos diciendo que el tipo de los parámetros enviados a la tarea en ejecución son del tipo String, que no se mostrarán unidades de progreso durante lo que tarde la ejecución en background (lo denotamos con Void) y que los resultados de este ejecución en segundo plano es del tipo List que contiene objetos de tipo Movie.

En el método doInbackground, realizamos la recuperación de datos por HTTP y entonces en el método onPostExecute  presentamos los resultados en la forma de notificaciones Toast. Notese que hay que crear otra clase tarea llamada “PerformPersonSearchTask” para realizar el otro tipo de búsqueda.

Notese que un widget ProgressDialog es usado para permitir al usuario conocer ir sabiendo que la recuperación de datos está en curso y que debe ser paciente. EL progress dialog está declarado cancelable en su método de Factory asi que el usuario puede cancelar la tarea de la request. El código es el siguiente:

private void performSearch(String query) {
	progressDialog = ProgressDialog.show(MovieSearchAppActivity.this, "Please wait...", "Retrieving data...", true, true);
	if (moviesSearchRadioButton.isChecked()) {
		PerformMovieSearchTask task = new PerformMovieSearchTask();
		task.execute(query);
		progressDialog.setOnCancelListener(new CancelTaskOnCancelListener(task));
	}  else if (peopleSearchRadioButton.isChecked()) {
		PerformPersonSearchTask task = new PerformPersonSearchTask();
		task.execute(query);
		progressDialog.setOnCancelListener(new CancelTaskOnCancelListener(task));
	}
}

...

También proporcionamos una implementación del OnCancelListener del progress dialog con el único propósito de cancelar la tarea relevante. El código es el siguiente:

...
private class CancelTaskOnCancelListener implements OnCancelListener {
	private AsyncTask<?, ?, ?> task;

	public CancelTaskOnCancelListener(AsyncTask<?, ?, ?> task) {
		this.task = task;
	}

	@Override
	public void onCancel(DialogInterface dialog) {
		if (task!=null) {
			task.cancel(true);
		}
	}
}

...

Veamos los resultados de nuestro código. Ejecutemos la aplicación , lancemos una búsqueda. El dialogo de progreso debe aparecer notificando de que se está haciendo una operación de request como en la siguiente imagen:

El usuario puede cancelar en cualquier momento la tarea dándole al botón “Back”. Si la operación se hace bien, entonces se invoca al método callback y se van mostrando un toast para cada uno de los resultados retornados, algo como esto: