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

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

por sgoliver
firebase-database-2

[mensaje-curso]

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é lblCielo, lblTemperatura 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«:

DatabaseReference dbCielo =
    FirebaseDatabase.getInstance().getReference()
        .child("prediccion-hoy")
            .child("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.

dbCielo.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        String valor = dataSnapshot.getValue();
        lblCielo.setText(valor);
    }

    @Override
    public void onCancelled(DatabaseError databaseError) {
        Log.e(TAGLOG, "Error!", databaseError.toException());
    }
});

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:

private DatabaseReference dbPrediccion;
private ValueEventListener eventListener;

//...

dbPrediccion =
    FirebaseDatabase.getInstance().getReference()
        .child("prediccion-hoy");

eventListener = new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {

        lblCielo.setText(dataSnapshot.child("cielo").getValue().toString());
        lblTemperatura.setText(dataSnapshot.child("temperatura").getValue().toString());
        lblHumedad.setText(dataSnapshot.child("humedad").getValue().toString());
    }

    @Override
    public void onCancelled(DatabaseError databaseError) {
        Log.e(TAGLOG, "Error!", databaseError.toException());
    }
};

dbPrediccion.addValueEventListener(eventListener);

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:

public class Prediccion {
    private String cielo;
    private long temperatura;
    private double humedad;

    public Prediccion() {
        //Es obligatorio incluir constructor por defecto
    }

    public Prediccion(String cielo, long temperatura, double humedad)
    {
        this.cielo = cielo;
        this.temperatura = temperatura;
        this.humedad = humedad;
    }

    public String getCielo() {
        return cielo;
    }

    public void setCielo(String cielo) {
        this.cielo = cielo;
    }

    public long getTemperatura() {
        return temperatura;
    }

    public void setTemperatura(long temperatura) {
        this.temperatura = temperatura;
    }

    public double getHumedad() {
        return humedad;
    }

    public void setHumedad(double humedad) {
        this.humedad = humedad;
    }

    @Override
    public String toString() {
        return "Prediccion{" +
                "cielo='" + cielo + '\'' +
                ", temperatura=" + temperatura +
                ", humedad=" + humedad +
                '}';
    }
}

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:

private DatabaseReference dbPrediccion;
private ValueEventListener eventListener;

//...

dbPrediccion =
    FirebaseDatabase.getInstance().getReference()
        .child("prediccion-hoy");

eventListener = new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {

        Prediccion pred = dataSnapshot.getValue(Prediccion.class);
        lblCielo.setText(pred.getCielo());
        lblTemperatura.setText(pred.getTemperatura() + "ºC");
        lblHumedad.setText(pred.getHumedad() + "%");
    }

    @Override
    public void onCancelled(DatabaseError databaseError) {
        Log.e(TAGLOG, "Error!", databaseError.toException());
    }
};

dbPrediccion.addValueEventListener(eventListener);

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.

btnEliminarListener.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        dbPrediccion.removeEventListener(eventListener);
    }
});

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().

eventListener = new ValueEventListener() {
    //...
};

dbPrediccion.addListenerForSingleValueEvent(eventListener);

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.

Puedes consultar y/o descargar el código completo de los ejemplos desarrollados en este artículo accediendo a la página del curso en GitHub.

[mensaje-curso]

También te puede interesar

11 comentarios

Firebase para Android [Serie] | sgoliver.net 16/11/2016 - 17:51

[…] Base de datos Firebase en Android (2) [Noviembre 2016] […]

Responder
Javier 16/11/2016 - 21:06

Hola, excelente artículo. Quería preguntarte si se puede poner la desactivación del listener en el método o en el método onStop, así no hace falta una vez que la actividad se pare, tener que hacerlo a mano, un saludo

Responder
Firebase para Android: Base de Datos en Tiempo Real (3) | sgoliver.net 21/11/2016 - 19:45

[…] el último artículo sobre la base de datos en tiempo real de Firebase ya aprendimos a suscribirnos a un nodo de nuestra […]

Responder
Firebase para Android: Base de Datos en Tiempo Real (4) | sgoliver.net 06/12/2016 - 19:52

[…] los dos artículos anteriores (parte 2 y parte 3) de la serie, nos hemos ocupado de repasar las diferentes formas que tenemos disponibles […]

Responder
MAMUGI1492 02/03/2017 - 1:22

Hola Javier, si te refieres a meter el removeEventListener() dentro del método onDataChange() o onCancelled(), la respuesta es NO.

El porque, es debido a que el objeto eventListener es el que tiene la capacidad de RECIBIR los datos cuando cambian, o realizar una determinada ACCIÓN cuando la conexión se interrumpe. Pero la capacidad de realizar la conexión o interrumpirla la tiene el objeto DatabaseReference a través de los métodos addValueEventListener()/addListenerForSingleValueEvent() o removeEventListener() respectivamente, a los cuales se les pasa como parámetro el objeto eventListener.

Es decir, que el objeto eventListener no puede incluir la funcionalidad que le corresponde al objeto DatabaseReference (crear o destruir conexiones a la bb. dd.), porque el objeto DatabaseReference necesita ser sobrecargado con un objeto que escuche los datos generados por esas conexiones (el objeto eventListener), y no al revés. No puedes construir un coche que conduzca a un conductor, será al revés.

Igual no consigo ser muy claro, pero la base es esa.

Un saludo

Responder
Delmirio Segura 24/04/2017 - 3:04

Faltó mensionar que necesitas la libreria:
compile ‘com.google.firebase:firebase-database:10.2.1’

Responder
Jose Ramon Vaño Calabuig 29/06/2017 - 17:55

Hola buenas tardes, muy buen manual pero no consigo que funcione tu código de ninguna de las maneras, he seguido los pasos línea por línea, he visto tu código en el GitHub y no logro mostrar el String del cielo.
¿Hay que añadir algo más o hacer algo?

Responder
Nestor Perez 16/08/2017 - 21:59

Hola, muy buen tutorial por cierto.

A lo que Javier se refiere, es que si es prudente remover el Listener desde el método OnStop del activity (Ciclo de Vida).

Si es recomendable remover el Listener en el método OnStop de hecho es una buena practica.

Saludos!

Responder
Vladimir 06/07/2018 - 23:15

Hola, como mi movil puede leer una base de datos , como la base de datos Neptuno.mdb que viene por defecto en Access, que de buscar y me salga una imagen con todos sus datos. Saludos

Responder
Cyc 10/01/2021 - 9:46

Hola
El articulo es muy interesante, supongo que esta actualizado, pero no entiendo porque esta en java y no en kotlin.
Un saludo

Responder
nombre 14/02/2021 - 20:18

que pesada la gente que pide la actualización en Kotlin, pero si aún se puede usar con Java, no entiendo tanto la necesidad de pasar de uno a otro

Responder

Dejar un comentario

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información. Aceptar Más Información

Política de Privacidad y Cookies