martes, 10 de enero de 2017

Firebase para Android: Base de Datos en Tiempo Real (2)

En el artículo anterior del curso repasamos todos los preparativos necesarios para empezar a trabajar en un proyecto Android con soporte para los servicios proporcionados por Firebase, centrándonos en esta ocasión en la base de datos en tiempo real (realtime database). Por un lado vimos cómo crear el proyecto de Firebase desde su consola de administración, y por otro cómo crear y configurar el proyecto Android en Android Studio. Por último explicamos algunas particularidades de la base de datos de Firebase, como por ejemplo su organización de los datos en forma de árbol JSON, y cómo manipular dichos datos desde la propia consola. En este nuevo artículo nos centraremos ya en el proyecto Android y veremos las distintas formas que tenemos disponibles para leer los datos de la base de datos y mostrarlos en nuestra aplicación.
Cuando hablamos de las características de la base de datos de Firebase hubo un punto que destacamos sobre el resto, y era su sincronización en tiempo real. Aunque ya explicamos brevemente qué significaba esto voy a intentar explicarlo de nuevo comparando la base de datos de Firebase con una base de datos tradicional.
Cuando utilizamos una base de datos tradicional como backend de una aplicación Android es muy probable que nos encontremos con un problema similar a éste: imaginemos que todos los clientes conectados a la base de datos leen de ella una determinada información y la muestran al usuario. En algún momento del tiempo uno de los clientes modifica dicha información, la envía al servidor, y se actualiza en la base de datos. Tras esto, ¿cómo se enteran de los cambios el resto de clientes que usan la base de datos? Algunas de las soluciones típicas ante estas situaciones son las siguientes:
  • Dotar a las aplicaciones de alguna opción de refresco para que, bien sea de forma manual por el usuario o automáticamente de forma periódica, la aplicación vuelva a consultar los datos al servidor y obtenga la nueva información si ésta ha cambiado.
  • Implementar algún sistema de notificaciones push, de forma que cada vez que un cliente actualiza información relevante en la base de datos, el resto de clientes conectados sean notificados automáticamente para que vuelvan a recuperar dicha información del servidor.
Cualquiera de estas soluciones es válida y pueden ser suficiente en muchos casos, pero implican que en un momento u otro todos los clientes deben volver a consultar de forma explícita al servidor para ver si la información ha cambiado, con el trabajo de codificación que ello conlleva, más el trabajo adicional de tener que utilizar otras tecnologías en paralelo como las notificaciones push de la segunda opción.
Cuando utilizamos la base de datos en tiempo real de Firebase la estrategia de obtención de información será distinta. Ya no necesitaremos que la aplicación consulte en distintos momentos un determinado dato por si éste ha cambiado, sino que directamente nos suscribiremos a dicho dato de forma que seamos notificados y recibamos su nuevo valor automáticamente cada vez que éste cambie. Podríamos decir de alguna forma que con las bases de datos tradicionales la iniciativa la debe llevar siempre la aplicación, y con Firebase gran parte de esa iniciativa pasa al lado de la base de datos. De esta forma, cualquier cambio que se produzca en los datos (por ejemplo cuando un cliente actualiza parte de la información) se trasladará de forma automática e inmediata a todos los clientes interesados en dicho dato, sin necesidad de implementar este mecanismo de notificación y refresco por nuestra parte.
¿Pero cómo hacemos esto? ¿Realmente es tan sencillo? Sí que lo es, veamos cómo conseguirlo. Vamos a empezar escribiendo desde la consola de Firebase una serie de datos en la base de datos que podamos leer después desde nuestra aplicación Android. Ya vimos cómo hacer esto en el artículo anterior por lo que no entraré en mucho detalle. Como ejemplo, imaginemos que estamos construyendo una aplicación para mostrar las condiciones meteorológicas en nuestra ciudad y queremos mostrar el estado del cielo, la temperatura, y la humedad en el día de hoy. Para almacenar esta información podemos crear un nuevo nodo en la base de datos llamado “prediccion-hoy” y como subelementos de éste tres nuevos nodos “cielo”, “temperatura” y “humedad” con sus valores correspondientes (ficticios, por supuesto):
datos-iniciales
Hecho esto ya podemos dirigirnos a nuestro proyecto Android en Android Studio (en el artículo anterior vimos cómo crearlo y configurarlo). Lo primero que haremos será crear una layout muy sencillo para nuestra pantalla principal, donde mostremos los tres datos de la predicción meteorológica. Simplemente añadiré tres campos de texto a los que llamaré lblCielolblTemperatura y lblHumedad. Si necesitas consultar el código del layout encontrarás al final del artículo el enlace al código completo del ejemplo en github.
Y con esto terminado vamos ya con Firebase. Lo primero que tendremos que hacer será crear una referencia a nuestra base de datos. Esta referencia podría entenderse como un indicador que apunta a un lugar concreto de nuestra base de datos, aquel en el que estemos interesados. Aunque es posible definir una referencia al elemento raíz de la base de datos, casi nunca necesitaremos acceder a la totalidad de los datos, sino que nos bastará con un fragmento o sección concreta, o dicho de otra forma, solo necesitaremos los datos que cuelguen de un determinado nodo de nuestro árbol JSON.
Por empezar por el caso más sencillo, vamos a crear por ejemplo una referencia al nodo “cielo” de forma que podamos suscribirnos a él para conocer su valor actual y estar informados de sus posibles cambios. Para ello crearemos en primer lugar un objeto de tipo DatabaseReference que almacenará la referencia a la base de datos. Para obtener ésta obtendremos primero una instancia a la base de datos de Firebase mediante getInstance() y después una referencia al nodo raíz de los datos con getReference() sin parámetros. A partir de esta primera referencia podemos crear cualquier otra más específica “navegando” entre los nodos hijo mediante el método child(), que recibe como parámetro el nombre del subnodo al que queremos bajar en el árbol. En nuestro caso tendremos que acceder primero al subnodo “prediccion-hoy” y posteriormente a su subnodo “cielo“:
Obtenida la referencia ya podríamos suscribirnos a ella asignándole un listener de tipo ValueEventListener. Este listener tendrá dos métodos:
  • onDataChange(). Se llamará automáticamente cada vez que se actualice la información del nodo actual o se produzca cualquier cambio en cualquiera de sus nodos descendientes. Se llamará por primera vez en el momento de suscribirnos, de forma que recibamos el valor actual del nodo y todos sus descendientes (si los tiene).
  • onCancelled(). Se llamará cuando la lectura de los datos sea cancelada por cualquier motivo, por ejemplo porque el usuario no tiene permiso para acceder a los datos.
El primero de los métodos es por supuesto el más interesante, ya que es el que nos permitirá estar al tanto de cualquier cambio que se produzca en la información contenida a partir de la localización a la que apunta nuestra referencia. Como parámetro de este método recibiremos siempre un objeto de tipo DataSnapshot que contendrá toda la información del nodo. Un objeto DataSnapshot representa básicamente una rama del árbol JSON, es decir, contendrá toda la información de un nodo determinado, con su clave, su valor, y su listado de nodos hijos (que a su vez pueden tener otros descendientes), por el que podremos navegar libremente. Podremos obtener la clave y el valor mediante los métodos getKey() y getValue()respectivamente. Los subnodos los podremos obtener en su totalidad mediante getChildren() (los recibiremos en forma de listado de objetos DataSnapshot) o bien podremos navegar a subnodos concretos mediante child(“nombre-subnodo”).
Entendiendo esto, nuestro primer caso de ejemplo sería muy sencillo. Cada vez que se llame al método onDataChange() simplemente recuperaremos el valor del nodo con getValue() y actualizaremos el campo correspondiente de la interfaz de la aplicación.
Si ejecutamos en este momento la aplicación (recomiendo hacerlo en un dispositivo real), podremos comprobar como nuestro campo “Cielo” de la aplicación se informa correctamente con el valor actual almacenado la base de datos (ya que onDataChange() se ejecuta una primera vez al suscribirnos a la referencia definida).
demo-inicial-1
Si ahora modificamos dicho valor desde la consola de Firebase:
datos-finales
Nuestra aplicación debería reflejar inmediatamente el cambio:
demo-final-1
Sencillo, ¿verdad? Pues podríamos hacerlo de forma análoga para los otros dos campos de nuestra predicción, temperatura y humedad, y ya tendríamos la aplicación funcionando como pretendíamos. Sin embargo vamos a hacerlo de forma distinta para ver otras alternativas de acceso a los datos de Firebase.
No siempre tenemos que definir referencias a nodos finales del árbol (nodos sin hijos) como hemos hecho con el nodo “cielo”. También podemos por supuesto definir referencias a nodos intermedios del árbol de forma que podamos detectar cambios en cualquiera de sus descendientes asignando un sólo listener ValueEventListener. En nuestro caso particular la solución obvia será crear una referencia al nodo “prediccion-hoy” y suscribirnos a sus cambios. De esta forma, cada vez que cualquiera de los nodos “cielo”, “temperatura” o “humedad” sea actualizado recibiremos en el método onDataChange() la información del nodo “prediccion-hoy” completo. Para obtener toda la información de este nodo tendremos dos posibilidades: navegar de forma explícita por sus hijos mediante child() y obteniendo su valor con getValue(), o bien crear un objeto Java con la misma estructura (los mismos campos) y dejar que getValue() trabaje por nosotros convirtiendo la información recibida en un objeto de ese tipo.
La primera opción quedaría más o menos de la siguiente forma:
Para la segunda tendríamos que crear primero una clase con los mismos campos que el nodo al que nos hemos suscrito. En mi caso crearé una clase llamada Prediccion, con los tres campos indicados, y sus getters y setters. Es obligatorio incluir además un constructor por defecto para que todo funcione correctamente:
Una vez creada, podríamos hacer que el método getValue() del DataSnapshot nos devuelva directamente un objeto de este tipo pasándole como parámetro la clase que debe devolver, de la siguiente forma:
Por su parte, y por comentarlo brevemente, el método onCancelled()recibe como parámetro un objeto de tipo DatabaseError, que contiene toda la información del error que se haya producido. En nuestro caso de ejemplo me estoy limitando a añadir esta información de error, en forma de excepción llamando al método toException(), a un mensaje en el log de Android. En la práctica deberíamos mostrar el mensaje de error al usuario y adaptar nuestra interfaz a la posible ausencia de datos, si procede.
Si ejecutamos de nuevo la aplicación y modificamos algunos datos desde la consola de Firebase:
datos-finales-2
Deberíamos ver cómo se actualizan convenientemente en la aplicación Android todos los datos de nuestra predicción meteorológica:
demo-final-2
Otro tema importante a tener en cuenta es que la suscripción a una referencia de una base de datos de Firebase, es decir, el hecho de asignar un listener a una ubicación del árbol JSON para estar al tanto de sus cambios no es algo “gratuito” desde el punto de vista de consumo de recursos. Por tanto, es recomendable eliminar esa suscripción cuando ya no la necesitamos. Para hacer esto basta con llamar al método removeEventListener() sobre cada referencia a la base de datos que ya no sea necesaria y pasarle como parámetro el listener que deseamos eliminar. Para el ejemplo, añadiré un botón que realice esta desvinculación. Tras pulsarlo, si hacemos cualquier cambio en los datos desde la consola de Firebase éste ya no se sincronizará con la aplicación Android.
Con esto en cuenta, y considerando que no siempre es necesario el mecanismo de sincronización en tiempo real, por ejemplo para datos que sabemos que no van a cambiar o que no lo van a hacer frecuentemente, la API de Firebase nos ofrece una forma alternativa de asociar el listener a una referencia de la base de datos de forma que éste sólo se ejecutará una vez, es decir, recibiremos el valor inicial del nodo en el momento de la suscripción a su referencia y ya no volveremos a recibir más actualizaciones de ese nodo. Esto nos evitará, en el caso de datos que no cambian, el suscribirnos a una referencia y tener que eliminar el listener inmediatamente después de recuperar su valor inicial. Para conseguir esto, el procedimiento sería análogo al ya mencionado con la diferencia de que utilizaríamos el método addListenerForSingleValueEvent(), en vez del ya mencionado addValueEventListener().
Y con esto finalizaríamos este segundo artículo sobre la base de datos en tiempo real de Firebase. En la siguiente entrega seguiremos viendo nuevas formas de leer y mostrar datos desde la base de datos que pueden sernos de utilidad en muchas ocasiones.

No hay comentarios:

Publicar un comentario