Titanium: Tablas y callbacks
En el último evento de Titanium, desarrollé un pequeño ejemplo para iOS y Android que consistía en una tabla con imágenes de un feed de flickr. Al pulsar una fila de la tabla se abre una ventana con la foto ampliada. Es un ejemplo sencillo que me permite introducir un par de conceptos sobre Titanium y commonJS.
Primero veremos cómo personalizar objetos de Titanium para posteriormente crear múltiples instancias del mismo. Crearemos nuestro propio módulo commonJS que devuelve un TableViewRow ya personalizado con los datos de cada fila.
En segundo lugar, veremos cómo llamar a un servicio web remoto, cuya respuesta es asíncrona (esto es, no ocurre en el orden normal del hilo de ejecución, si no cuando llega la respuesta del servidor). Para gestionar esta respuesta asíncrona haremos uso de las funciones callback y haré una breve discusión sobre cuándo usar funciones callback y cuando usar eventos a nivel de aplicación, por si a Miguel Ángel no le quedó del todo claro.
Si no has seguido los anteriores posts, te recomiendo echar un vistazo al blog con la introducción de JavaScript y commonJS para Titanium.
Como siempre, empezamos con app.js y los módulos de la aplicación declarados en ModulePaths.js. La discusión de estos ficheros es la misma que hice en esta entrada, así que no la repito.
App.js
gist id=3554525 file=app.js]
ModulePaths.js
gist id=3554525 file=ModulePaths.js]
Styles.js
gist id=3554525 file=Styles.js]
AppWindow.js
Esta es la ventana que se crea desde app.js, simplemente añade la tabla de fotos a la ventana y la devuelve para que app.js cree una instancia y la abra.
gist id=3554525 file=AppWindow.js]
PhotosTableView.js
Este módulo devuelve un tableView de Titanium, que llama a Feed.getData
(que veremos a continuación) y le pasa como argumento callbackTableView
, que es el nombre de una función definida previamente. Feed se conecta con los servicios de Flickr y recibe un listado de fotografías. Si la información que devuelve Feed.getData
fuera local (de un fichero o una base de datos), podríamos hacer algo como tableview.data = Feed.getData()
(suponiendo que la info viniera ya formateada). Pero como Feed
va a conectar con un servidor remoto y no sabemos cuándo va a responder, lo que hacemos es pasarle como argumento la función que queremos que ejecute cuando ya tenga la respuesta del servidor.
De momento, basta saber que cuando Feed.getData
termine de hacer lo que tenga que hacer, llamará a callbackTableView
gist id=3554525 file=PhotosTableView.js]
Cuando Feed
tiene respuesta del servidor, llama a callbackTableView
y le pasa como argumento dicha respuesta. Dentro de callbackTableView
, si la respuesta es válida, se crean las instancias de PhotoRow
, que son cada una de las filas de la tabla. Finalmente se añaden a la tabla.
Después de la función de callback, digo a la tabla que escuche los eventos click que pueda recibir y, si la fila es válida, creará una instancia de PhotoViewer
para visualizar la foto en grande. Siempre es mejor añadir el eventListener al tableView y no a las tableViewRow, es más eficiente gestionar un sólo eventListener (uno por la tabla) que decenas o cientos de eventListeners (tantos como filas tenga la tabla). Por tanto, evita la tentación de usar eventListeners a nivel de fila.
e.row
es el objeto fila que ha sido clickado y Titanium se encarga de devolvérnoslo siempre que se hace click en la tabla. e.row.data
es una propiedad creada por nosotros mismos en el módulo PhotoRow. Lo que hago es crear esta propiedad con toda la información del elemento devuelta por el servidor para poderla recuperar más adelante (en el siguiente apartado se termina de entender esto).
PhotosTableViewRow.js
Es cada una de las filas de la tabla. Este componente se va a reutilizar varias veces (al contrario que el resto, que sólo se usan una vez en la aplicación). Para hacerlo reutilizable, el componente recibe como argumento el item con la información que tiene que rellenar (en nuestro ejemplo, título y una foto) y usamos esta información para completar la creación de los componentes que forman la fila. En la fila 9, creo una propiedad data
en el TableViewRow
que me permitirá recuperar toda la información del elemento más tarde. Este es el mismo “data” que recupero en el eventListener de la tabla.
gist id=3554525 file=PhotosTableViewRow.js]
PhotoViewer.js
Este fichero no tiene mucha complejidad. Se tiene en cuenta si la plataforma es iOS o Android, para colocar o no un botón de cerrar (android no lo necesita, ya que dispones de el botón “volver”).
Para facilitar la escritura, en el fichero estilos he creado una propiedad iOS que será true
si el dispositivo es iPhone o iPad, así resulta más fácil de escribir (y de leer), tan simple como ver el valor de $$.iOS
.
Otro punto interesante, podemos agregar nuestros propios métodos a objetos Titanium. A win
le añado el método showImage
, que es una función que abrirá la ventana en forma modal. (Ojo! No es la mejor manera de hacer esto, pero para el ejemplo queda bien y es suficiente). Así, podemos extender el funcionamiento nativo de los componentes Titanium agregando métodos y propiedades.
Importante saber que cuando añadamos métodos a objetos Titanium estos no deben nunca empezar con get
o set
. Es una limitación de la plataforma, fallará en silencio y podemos tirarnos un buen raton sin saber qué pasa o porqué no funciona.
gist id=3554525 file=PhotoViewer.js]
flickrFeed.js
Vistos todos los ficheros relativos a la interfaz de usuario, pasamos a ver cómo se gestiona la conexión con el servidor.
El cliente http establece una conexión con un puerto web y hace una petición GET o POST a la url que le indiquemos. HTTPclient permite definir varias funciones callback para gestionar la respuesta del servidor. En este ejemplo uso onload
y onerror
, pero tambien podemos capturar ondatastream
y onsendstream
para saber la cantidad de información enviada y recibida.
gist id=3554525 file=flickrFeed.js]
He definido la función exports.getData
para que reciba un argumento llamado callback
. Titanium llamará la función definida en onload
cuando tenga respuesta del servidor. Tras procesar la respuesta, se ejecuta callback
, pasándole como argumento la respuesta – ya procesada – del servidor. getData
no necesita saber qué es o a quién pertenece callback
, simplemente la intentará ejecutar suponiendo que se trata de una función y que sabrá como ejecutarse. En el ejemplo, lo que ocurrirá es que se llamará a callbackTableview
, que es la función que pasamos como argumento a Feed.getData
y si todo va bien, rellenará la tabla con las fotos.
He observardo que algunas tags en flickr generan una respuesta json que titanium no puede procesar, si haces pruebas cambiando las palabras en la URL, puede que falle por esto.
Eventos vs. callbacks
Espero dedicar en algún momento una entrada sólo a la creación y gestión de eventos, pero quiero hacer una pequeña aclaración de porqué es preferible usar callbacks a eventos a nivel de app. Primero, hay que diferenciar entre lo que son eventos de componentes gráficos (como click
, load
, etc.) y eventos de aplicación. Los eventos de aplicación son los que se gestionan desde Ti.App.addEventListener()
y Ti.App.fireEvent()
. Lo que explico a continuación afecta a ambos, pero especialmente a los segundos.
Por explicarlo de forma gráfica, una función callback es una conexión directa entre dos objetos. Un objeto llama a un método de otro objeto. No hay intermediarios. Fin de la historia.
Un evento de aplicación es más complejo. Se dispara un evento desde un punto de la aplicación, la aplicación recoge el evento y lo “reparte” entre los otros módulos que previamente estarán escuchando. El simple hecho de estar escuchando eventos ya tiene un coste. Como además, el evento es a nivel de app, salta de un módulo al objeto Ti.App y de éste a los que estén escuchando.
En casos en los que sólo queremos comunicar dos objetos es preferible usar callbacks.
Si estamos creando componentes reutilizables, normalmente componentes UI, podemos disparar nuestros propios eventos de componentes (por ejemplo, si nos inventamos un botón que implementa un evento tripleclick
)
Y sólo en los casos en que necesitemos llamar la atención de varios objetos no conectados entre sí desde un único punto de la aplicación nos plantearemos usar eventos de aplicación.
Tienes los ficheros de este proyecto disponibles para descargar aquí.
Xurde Appio says
Hola Javier,
felicidades por MyZings, vi que fue reconocida por Appcelerator como best next-gen app. Yo llevo bastante tiempo usando Appcelerator y aun más javascript / ajax. Me gusta mucho tu estilo de programación y la manera en que utilizas commonJS y los callbacks.
Echo en falta en este ejemplo una vuelta más, el manejo de datos locales vs datos remotos. En tu ejemplo, siempre que se abre la aplicación se solicitarían los datos remotamente. Siempre he tenido dudas de cómo manejar de la mejor manera este tipo de situaciones. Estructuralmente, entiendo que el módulo FLICKRFEED se encargaría de evaluar si tomamos esos datos de una base local o los pedimos al servidor, encapsulando esta solicitud de datos. Pero cómo evaluarías si es necesario pedir los datos, o si pedir todos los datos o solamente los X últimos, o simplemente siempre pedirías los datos al abrir la app para tener los más frescos?
Supongo que no es tan fácil de responder, pero en definitiva me gustaría saber cómo manejas tú este tipo de situación, y saber si tienes algún ejemplo por ahí, o tienes pensado hacer alguno.
Saludos!
J. Rayon says
Hola Xurde,
efectivamente, este código va a hacer la consulta al servidor cada vez que se ejecute. Aunque el httpclient tiene su propio cache, igual que las imágenes remotas, hay ocasiones en que queremos tener nuestro propio caché local (por ejemplo, para mostrar los últimos resultados consultados si no existe conexión).
La solución depende del tipo de datos que tenemos que almacenar, pero una solución común es crear una base de datos sqllite en el dispositivo en el que almacenamos los últimos resultados consultados. La clave de la tabla es la url y el contenido la respuesta del servidor. También podemos incorporar un campo de timestamp para saber si los datos son muy antiguos.
En el código que conecta al servidor, en este caso en flickrfeed.js, antes de consultar el servidor miraría si la entrada ya existe en la base de datos y si es así devuelvo esos resultados en el callback y después refresco la base de datos con los datos actuales.
A grosso modo, esta sería la manera, pero cada caso puede requerir una solución distinta. Crearé un tutorial sobre esto más adelante.