“Desarrollo de Aplicaciones Móviles en Android”
Ejercicio Avanzado B: Web Services y Ejecución en Segundo plano
Aunque ya hemos visto muchas cosas a lo largo del curso, aún queda
mucho por descubrir. Uno de los grandes usos que se les da a los smartphones, es el de usuarios de servicios web, o Web Services. Esto se debe a que consiguen dar una nueva vuelta de tuerca a
los servicios que ya existen (Google Search, Google Maps, Facebook, Twiiter, Flickr, Picasa, Quora, etc.) En este ejercicio, veremos una breve introducción sobre cómo acceder a ellos desde la plataforma Android.
A. Descripción
El gran auge de la denominada “Web 2.0” está fundada sobre la existencia de los Web Services. En esencia, un Web Service es un método de comunicación entre dos dispositivos electrónicos sobre una red. Para nosotros, desarrolladores de smartphones, significa que hay una serie de máquinas conectadas a Internet a las cuales podemos realizar peticiones,
peticiones que volverán a nosotros en forma de respuestas.
Todo esto se basa en la existencia de una serie de protocolos y estándares (HTTP, WSDL, SOAP,
REST, RPC, XML, JSON, etc.) además de una gran variedad de tecnologías (Apache, .NET) ¿Pero, qué es lo que importa aquí? Primero, debemos entender que hay varias formas de comunicarse con un servidor, dependiendo del protocolo que utilice éste para comunicarse
con el exterior, y lo segundo es que las comunicaciones con servicios web están basadas en APIs. Cada proveedor de servicio web tiene su propia API, y debido a que nosotros, los desarrolladores, somos usuarios de esas APIs, debemos cumplir con ellas, y estar atentos a
posibles cambios por parte de los autores, porque si no, lo pagarán nuestros usuarios.
Nosotros, en este ejercicio, llamaremos a la API de codificación geográfica de Google. ¿Nuestro
objetivo? Pasarle una posición GPS (Latitud, Longitud) y que Google nos diga cómo se llama la
calle que se encuentra en esas coordenadas.
A.1. REST
Al igual que SOA, REST es una arquitectura sobre la que pueden basarse los Web
Services. En esencia, REST implica que el servidor no posee estado, es decir, que cada petición
que hacemos al servidor es independiente de todas las anteriores y de todas las posteriores;
no hay tokens ni cookies de por medio. Esto es común en servicios que no requieren inicio de sesión, como nuestro caso, pero por ejemplo, si quisiéramos comunicarnos con Facebook para actualizar nuestro perfil, el funcionamiento sería diferente (tendríamos que mantener el inicio
de sesión como un token en este caso)
REST, además, resulta ser muy simple, pues posee un número fijo de operaciones definidas
(GET, POST, PUT y DELETE), con un propósito bien definido por el protocolo. GET se utiliza para obtener algún dato, POST y PUT para subir, modificar o añadir algo al servidor, mientras que DELETE permite eliminar un dato del servidor. En la práctica sin embargo, GET y POST son
ambos utilizados para obtener datos. La razón es muy simple; si alguna vez os habéis fijado en
las direcciones de vuestro navegador, habréis visto direcciones en las que al final hay parejas (clave, valor) separadas por un ampersand (‘&’); estas parejas son los parámetros que se le pasan al servidor, y si por ejemplo necesitamos enviar datos privados (por ejemplo: nuestra dirección), no querríamos que éstos navegaran abiertamente por la red. Por esta razón se utiliza el POST como alternativa al GET, ya que el POST permite enviar los parámetros como parte de su cabecera (dentro del paquete), lo cual permite esconder los datos un poco, e incluso deja abierta la posibilidad de encriptar los datos y enviarlos.
En la práctica, las operaciones GET, POST, PUT y DELETE son muy comunes, y no tienen por qué ceñirse a únicamente a los servicios de tipo REST; pueden ser utilizados por cualquier Web Service para que la API sea más fácil de entender, y para mantener un cierto estándar.
B. Implementación
Para comunicarnos con la API de Google necesitaremos realizar una petición de tipo GET. En concreto, el servicio que vamos a utilizar es el de reverse---geocoding (codificación geográfica inversa), el cual nos permite obtener una dirección a partir de unas coordenadas
GPS. Para ello, nos bastará con una única Activity, un layout, y varios String.
Empecemos.
Nota: Para más información acerca de la API que estamos utilizando, visitar http://code.google.com/intl/es---ES/apis/maps/documentation/geocoding/
C. Pasos a seguir
1. Comenzamos creando un nuevo proyecto Android para la versión 2.2 (NOTA: Este ejercicio sigue siendo perfectamente válido para versiones anteriores), con el
nombre que queramos, el paquete que queramos y asignándole un nombre a la
Activity por defecto.
2. Antes de escribir el layout, necesitaremos definir una serie de etiquetas
(String):
<string name="latitude">Latitud: </string>
<string name="defaultLatitude">28.482566</string>
<string name="longitude">Longitud: </string>
<string name="defaultLongitude">-16.323004</string>
<string name="result">Resultado: </string>
<string name="getAddress">Obtener Dirección</string>
3. Para el layout, utilizaremos un RelativeLayout como padre, que comenzará con dos parejas de combinaciones TextView/EditText (dos pares de TextView seguidos de EditText). Los llamaremos latitudeTV, latitudeET, longitudeTV y longitudeET respectivamente. Todos
ocuparán la mínima altura posible pero todo el ancho posible. Además, daremos a cada uno un valor a su atributo text en el orden en el que se encuentran (latitude y longitude para los TextView, defaultLatitude y defaultLongitude para los EditText). El primer TextView
(latitudeTV) ha de ir anclado a la parte superior de su padre
(android:layout_alignParentTop="true"), y el resto se irá colocando
en orden justo debajo (utilizar android:layout_below)
A continuación colocaremos el Botón para accionar la petición; se llamará requestButton, irá en la parte inferior del layout (android:layout_alignParentBottom="true"), y ocupará el mismo espacio que las View anteriores. Su texto será la etiqueta getAddress.
Para terminar con el layout, colocaremos otra pareja TextView/EditText
más: el TextView debe ir bajo el EditText longitudeET, mientras que el EditText irá bajo el TextView que acabamos de añadir y sobre el requestButton. El TextView ocupará el mismo espacio que los demás (altura mínima, pero todo el ancho posible), mientras que el EditText ocupará todo el espacio disponible, tanto en ancho como en alto. Además, nos interesa
prepararlo para que soporte largas entradas de texto (ver atributos gravity, inputType y scrollbars) Sus nombres serán resultTV y resultET respectivamente. El texto de resultTV debe ser el de la etiqueta result. Por último, este EditText no lo utilizaremos para introducir datos, sino para
mostrarlos, luego sería conveniente que el usuario no pudiese modificarlo
(android:enabled="false")
4. Una vez hayamos comprobado que el layout carga correctamente, empezaremos a escribir código en nuestra Activity. Nuestro objetivo es utilizar los dos primeros EditText para introducir las coordenadas GPS, el botón para realizar la petición, y el último EditText para mostrar la salida. Por tanto, necesitamos variables que nos permitan referenciar estas View; necesitaremos una referencia a latitudeET, un longitudeET, un requestButton y un resultET, todos son sus tipos correspondientes (llamar las variables de la misma forma). Utilizaremos nuestro método initConfig() para cargar el layout y referenciar
las View. A continuación, añadiremos un onClickListener al
requestButton y haremos que el anterior llame al método makeRequest()
sin argumentos. En este método implementaremos la petición.
5. Escribimos la cabecera del método privado makeRequest() sin argumentos y sin ningún valor a devolver. Lo primero que debemos hacer en él es desactivar los EditText y el Button para que el usuario no pueda realizar modificaciones ni llamar a este método más de una vez al mismo tiempo. Al final del método, escribimos el código contrario, es decir, el que vuelve a activar los EditText y el
requestButton.
6. El código importante viene aquí. En medio del método makeRequest() ( tras desactivar los EditText y el requestButton ) debemos añadir un bloque try/catch genérico (que capture cualquier Exception) Dentro del bloque try/catch, introduciremos el siguiente código:
// obtenemos una conexión
DefaultHttpClient httpClient = new DefaultHttpClient(); String url =
"," + longitude + "&sensor=false";
// creamos una petición de tipo GET (parámetros en la URL) HttpGet request = new HttpGet();
request.setURI(new URI(url));
// ejecutamos la petición
HttpResponse response = httpClient.execute(request);
// convertimos la respuesta en string
String responseString = EntityUtils.toString(response.getEntity());
// escribimos el resultado resultET.setText(responseString);
// cerramos la conexión httpClient.getConnectionManager().shutdown();
7. Con este código en posición la aplicación debería funcionar correctamente. ¿O no?
¿Nos hemos olvidado de algo? Sí. Queremos realizar una petición en Internet,
luego nos hace falta el permiso (uses-permission) INTERNET en el manifest
de nuestra aplicación.
8. Como podemos ver, la aplicación retorna un resultado muy largo, dentro del cual
se encuentra la calle de la Escuela Técnica Superior de Ingeniería Informática de
la Universidad de La Laguna, pero, ¿qué es todo este texto que recibimos como respuesta? Y lo que es más importante aún, ¿por qué se detiene la aplicación mientras realiza la petición?
B (2) Ejecución en segundo plano
Detengámonos un segundo a mirar lo que acabamos de hacer. Android posee varias
librerías incluidas, y una de ellas (HttpClient), es la que hemos utilizado para obtener
acceso a Internet. Puede verse que primero necesitamos inicializar un objeto de tipo HttpClient y que luego, al final de la petición lo apagamos ( método shutdown() ). En este ejercicio hemos querido simplificar el código lo máximo posible para centrarnos en lo importante: entender cómo implementar comunicaciones con Web Services, pero en realidad,
el hecho de inicializar y de cerrar clientes HTTP es una operación costosa, y en aplicaciones que requieran de acceso a Internet a lo largo de muchas Activity lo recomendado es compartir
un único HttpClient compartido, y que éste se cierre únicamente cuando el usuario ya no
esté utilizando la aplicación.
Lo siguiente a tener en cuenta es la respuesta. Como ya hemos mencionado, nuestro Web Service en cuestión posee su propio protocolo, uno al que ya nos hemos adaptado (en parte) al realizar la petición (nótese que los parámetros están en la URL), sin embargo, la sorpresa viene al ver que la respuesta no nos da los datos de forma directa, ni mucho menos.
La respuesta que tenemos es en realidad un objeto JSON (JavaScript Object Notation), al que muchos nos gusta llamar “el sucesor del XML”. Lo común cuando se interactúa con Web Services es recibir la respuesta o bien en formato XML, o bien en formato JSON. XML tiene la ventaja de ser muy conocido y de que es increíblemente flexible, además de que está soportado por casi todos los lenguajes. Sin embargo, tiene una gran desventaja con respecto al
JSON, y es que requiere de la construcción de un parser, un analizador o escáner, específico para cada tipo de documento XML. Esto significa que para un mismo Web Service, podríamos tener que escribir dos o más escáneres, uno por cada petición distinta que tengamos que hacer (en la práctica, los Web Service intentan minimizar que esto ocurra) Es aquí donde gana
el más moderno JSON: funciona por parejas (clave, valor), se adapta a cualquier estructura,
cada vez es más soportado, y además, no requiere construir un parser específico, solamente
adaptarnos a la respuesta recibida.
Lo más importante, sin embargo, es el hecho de que nuestra aplicación se detiene cada vez que realiza la petición. Con detenerse queremos decir que, si nosotros interactuamos con la aplicación mientras está realizando la llamada al Web Service, ésta no es capaz de hacernos caso. ¿Por qué? La respuesta es muy simple: porque está ocupada.
Hasta ahora, todas las acciones que hemos realizado en Android han sido acciones
rápidas, sin apenas retardo ni complejidad; esto ha permitido que pasemos por alto un hecho
muy importante: hemos estado utilizando el hilo (thread) encargado de procesar los eventos
de la interfaz gráfica para todas nuestras acciones.
Es decir, la secuencia de instrucciones que mantiene viva nuestra aplicación, respondiendo cada vez que nosotros presionamos la pantalla, pulsamos un botón, o simplemente deslizamos un dedo para ver cómo se ilumina una parte de la pantalla, es la misma que ejecuta todas y cada una de las líneas de código que hemos programado hasta
ahora (eventos, creación de múltiples Activity, la rotación, etc.) han detenido brevemente
la capacidad de la aplicación de responder ante nuestras peticiones, pero debido a que le
hemos pedido relativamente poco al hilo de la interfaz gráfica, no nos hemos dado cuenta.
Todo ha cambiado con la petición al Web Service, donde dependiendo del dispositivo y
de la red no nos damos cuenta, pero el hecho es que si una interacción por parte del usuario tarda más de medio segundo, el usuario medio se da cuenta, y a los cinco segundos, Android permite al usuario cerrar la aplicación.
Esto se soluciona moviendo las partes de código lentas a otro hilo, uno secundario, que pueda realizar estas acciones en paralelo. Mientras, el hilo de la interfaz mantendrá la aplicación “viva”, y cuando el hilo secundario termine, podrá notificar al primer hilo para que
actualice la interfaz. Hagámoslo.
D. Pasos a seguir
1. Debemos crear un nuevo paquete, esta vez para guardar las clases de tipo Thread. Una vez creado, pediremos a Eclipse que añada una nueva clase, RequestThread, que heredará de la clase java.lang.Thread.
2. La clase RequestThread estará vacía. Los datos que necesitamos son la latitud, la longitud y un Handler. Un Handler es un objeto de Java capaz de recibir mensajes (eventos) de forma asíncrona, es decir, sin detener el hilo de ejecución
desde el que fue llamado. Además, funciona muy bien cuando se le llama desde un
hilo distinto. Comenzamos añadiendo estas líneas de código a la cabecera de la
clase RequestThread:
private Handler myHandler;private String latitude;private String longitude;3. A continuación, añadimos un constructor a la clase RequestThread para poder inicializar las variables anteriores. Como siempre, lo primero que hacemos es llamar al constructor del padre (constructor de la clase Thread de Java):
public RequestThread(String latitude, String longitude, Handler myHandler) {
super();
this.latitude = latitude; this.longitude = longitude; this.myHandler = myHandler;
}4. Lo siguiente es añadir el método más importante, el método run() (público, sin retorno y con uso del @Override porque estamos sustituyendo un método de la clase padre). Éste es el método del hilo que se ejecuta en segundo plano, ya que
por lo demás, cuando vayamos a crear un objeto de tipo RequestThread desde la RequestActivity, funcionará sobre el mismo hilo, es decir, sobre el de la interfaz gráfica. Copiar el siguiente código al método run():
try {
// obtenemos una conexión
DefaultHttpClient httpClient = new DefaultHttpClient();
"," + longitude + "&sensor=false";
// creamos una petición de tipo GET (parámetros en la URL) HttpGet request = new HttpGet();
request.setURI(new URI(url));
// ejecutamos la petición
HttpResponse response = httpClient.execute(request);
// convertimos la respuesta en string
String responseString = EntityUtils.toString(response.getEntity());
// escribimos el resultado
Message msg = new Message();
// de qué trata el mensaje msg.what = 0;
// añadir datos
Bundle data = new Bundle();
data.putString(RequestActivity.__RESPONSE_KEY__, responseString);
// colocamos los datos
msg.setData(data);
// enviamos el mensaje myHandler.sendMessage(msg);
// cerramos la conexión httpClient.getConnectionManager().shutdown();
}
catch (Exception e) {
e.printStackTrace();
// mensaje vacío (sin datos)
myHandler.sendEmptyMessage(1);
}
Revisemos el código con calma. Lo primero es ver que se trata del mismo bloque try/catch que nos detenía la ejecución de la aplicación en el método makeRequest() de la RequestActivity. Los cambios vienen a partir de que obtenemos la respuesta como String, pero antes miremos la nueva línea del bloque catch. En ella, enviamos un mensaje al Handler, un mensaje vacío (sin
datos), en el que el único parámetro que hay asigna un valor al campo what del
Message. Este campo what se utiliza para identificar qué se está enviando con el
mensaje; cuando se quieren enviar distintos mensajes sin enviar datos pesados, se envían mensajes vacíos con distintos valores para el campo what. El receptor, que pondremos en la clase RequestActivity, preguntará al mensaje cuál es el valor de su campo what, y así sabrá qué significa la recepción de ese mensaje. Si miramos el final del bloque try, veremos que creamos una nueva variable de tipo
Message, que asignamos el valor del campo what a cero (en el catch le asignamos 1), que creamos un bundle, y que en el bundle añadimos el String que hemos recibido (como clave utilizamos un campo que añadiremos ahora a la clase RequestActivity). Por último, colocamos el bundle en el
mensaje, y lo enviamos, sin olvidarnos de cerrar la conexión.
5. Como acabamos de mencionar, debemos añadir una variable pública, estática,
final (final) de tipo String con nombre RESPONSE_KEY
a la cabecera
de la clase RequestActivity. Además, debemos ir al método makeRequest(), y eliminar todas las líneas que siguen a aquellas en las que desactivamos los EditText y el requestButton. A continuación, creamos un RequestThread a cuyo constructor le pasamos el texto de los EditText (latitudeET, longitudeET) y una variable (myHandler) que crearemos en
el último paso. Para terminar, llamamos al método run(), sin parámetros, del
RequestThread que acabamos de crear.
6. Tras el método makeRequest(), debemos añadir el siguiente código:
Handler myHandler = new Handler() {
public void handleMessage(Message msg) {
if (msg.what == 0) {
// response resultET.setText((String)msg.getData().get(Request
Activity.__RESPONSE_KEY ));
}
if (msg.what == 1) {
resultET.setText(getString(R.string.error));
}
// activamos todo latitudeET.setEnabled(true); longitudeET.setEnabled(true); requestButton.setEnabled(true);
}
};
Como podéis ver, el método es muy sencillo. Si el what es cero, recogemos el String y se lo asignamos al EditText de resultado; si es 1, es que ocurrió una excepción, luego debemos mostrar un mensaje de error (dejamos este String a los alumnos). En cualquier caso, la ejecución del hilo termina, luego podemos volver a activar los EditText y el botón de petición.
E. Cuestiones y Conclusiones Finales
1. ¿Por qué utilizamos el método start() en vez de llamar al método run()
directamente desde el makeRequest() en RequestActivity? Probarlo.
¿Por qué se comporta de esta forma?
2. ¿Por qué utilizamos un Handler? ¿Por qué no podemos realizar lo que hace el
Handler directamente desde el método run() de la clase RequestThread?
¿Qué ocurre si lo hacemos?
3. ¿Por qué tenemos tanto miedo de que la llamada al Web Service detenga nuestra
aplicación? ¿Qué consecuencias tiene esto para el usuario?
Vayamos por partes. Lo primero es que sí, son muchos conceptos para asimilar en un único ejercicio, pero ésta, en realidad, es la forma más simple y real de encontrarse con el problema de bloquear (o detener) el hilo de la interfaz gráfica. Este problema lo vivimos constantemente con las aplicaciones de escritorio que utilizamos, sin embargo, en un smartphone la situación resulta ser mucho más estresante, ya que el usuario no tiene ningún sitio al que correr si nuestra aplicación satura al sistema operativo (dependiendo del teléfono,
es posible volverlo tan lento que el propio proceso HOME tenga que cerrarse), al contrario que en Windows y en Mac, donde podemos cerrar las aplicaciones y volver a abrirlas si nos hace
falta. Esta estrategia de Thread(s) y Handler(s) no es exclusiva de Android, y puede
utilizarse perfectamente en Java. Sin embargo, cabe mencionar que no es la única forma de realizar esta gestión en Android; hay dos tipos más que esperamos ver en un futuro curso Avanzado de Android.
Lo siguiente es reforzar la importancia de los Message. Cuando se envía un mensaje a un Handler, éste es colocado al final de una cola para ser procesado por el método handleMessage() que nosotros sustituimos en nuestro Handler. Esto ocurre de forma asíncrona, provocando que el hilo en el que se encuentra el handler ejecute un
handleMessage() con el mensaje correspondiente. Esto nos permite utilizar esta
estrategia con mucha flexibilidad; podemos crear tantos eventos (valores del argumento what) como queramos, permitiéndonos conocer en cualquier momento en qué estado se encuentra nuestro hilo en segundo plano. Naturalmente, la utilidad de esta estrategia se ve
mejor cuanto más complicada es la interacción entre el hilo en segundo plano y el hilo de la interfaz gráfica. Podría incluso utilizarse un único handler para recibir eventos de múltiples hilos, pues todos van a la misma cola. Ahora bien, las acciones que llevemos a cabo en el
handler no pueden ser lentas (es decir, no pueden detener el hilo de la interfaz gráfica), o
estaremos de vuelta en el mismo punto del que partimos.
Ahora vienen los hilos. El hilo de tipo RequestThread que creamos desde la
RequestActivity, en realidad, se ejecuta desde el mismo hilo que lo creó (es decir, el de
la interfaz gráfica) Si nosotros llamásemos al método run() directamente, sería como si el
hilo no existiese, porque sería como si ejecutásemos un método más de un objeto cualquiera;
es decir, se ejecutaría desde el hilo de la interfaz, luego el esfuerzo que hemos hecho habrá
sido en vano.
En realidad, las clases Thread y similares (Runnable) lo que hacen es
proporcionarnos una forma fácil de ejecutar código en otro hilo sin que nosotros tengamos que realizar ninguna gestión. Por eso la utilidad de la clase Thread radica en que utilicemos el método start(), porque se encarga de crear un nuevo hilo y de mantenerlo en funcionamiento ejecutando código, y el código que ejecuta es, por supuesto, el del método run().
Nota: La clase Thread proporciona más métodos para controlar la ejecución del hilo que ejecuta el método run(). Se han obviado para simplificar el texto y transmitir las ideas principales.
Sin embargo, ejecutar código en segundo plano es un poco más complicado de lo que
hemos dicho hasta ahora. Si nosotros intentáramos llamar a las Views desde el método
run() del hilo, obtendríamos una excepción en tiempo de ejecución, una excepción medianamente grave. Esto se debe a que las Views no pertenecen al hilo que está ejecutando el código (RequestThread), sino a otro (hilo de interfaz de RequestActivity) Es una convención en frameworks de programación con interfaz gráfica (.NET, Cocoa, Cocoa Touch, y por supuesto Android) que los elementos de la interfaz sólo puedan ser llamados desde el propio hilo que los creó. Esto da lugar a un concepto muy
utilizado, el de thread safety. Cuando un elemento es thread safe, significa que puede ser utilizado (llamado) desde cualquier hilo. En el caso de Android los elementos de la interfaz (Views) no son thread safe. Es aquí donde el handler comienza a tener sentido; los
métodos que permiten enviarle mensajes al handler son thread safe, y como ya dijimos, el
método que procesa los mensajes se ejecuta desde el hilo que creó el handler (en nuestro
caso, el de la interfaz de RequestActivity) Así de cómo hemos decidido solucionar el problema de comunicarnos desde el código ejecutando en segundo plano con la interfaz en este ejercicio, pero como ya hemos dicho, hay más soluciones.
Existen otros métodos para realizar comunicaciones con un servidor; Android, al implementar un subconjunto de Java 5, suele ser muy flexible importando librerías hechas para Java (archivos .jar), lo que nos permite utilizar otra librería con la que nos sintamos
más cómodos o incluso aportar algo que no permitía la librería que incluye Android, como por
ejemplo, el envío de archivos mediante un POST (recomendamos utilizar la librería Apache
Commons para esto) Sin embargo, llegados a cierto punto, simplemente conocer cómo
funciona la plataforma o librería con la que estemos trabajando no nos llevará más lejos, sino que será nuestro ingenio el que tendrá que resolver los problemas que no han sido resueltos.