Trabaja con listas de datos en Android

En este documento, se aborda el trabajo con listas de datos en Firebase. Para conocer los conceptos básicos de lectura y escritura de datos de Firebase, consulta Lee y escribe datos en Android.

Obtén una DatabaseReference

Para leer y escribir datos en la base de datos, necesitas una instancia de DatabaseReference:

Java

private DatabaseReference mDatabase;// ...
mDatabase = FirebaseDatabase.getInstance().getReference();

Kotlin

private lateinit var database: DatabaseReference// ...
database = FirebaseDatabase.getInstance().reference

Lee y escribe listas

Agrega datos a una lista de datos

Usa el método push() para agregar datos a una lista en aplicaciones multiusuario. Cada vez que se agrega un elemento secundario nuevo a la referencia de Firebase especificada, el método push() genera una clave única. Cuando se usan estas claves generadas de forma automática para cada elemento nuevo de la lista, varios clientes pueden agregar elementos secundarios a la misma ubicación, al mismo tiempo y sin conflictos de escritura. La clave única que genera push() se basa en una marca de tiempo. Por lo tanto, los elementos de las listas se ordenan cronológicamente de forma automática.

Puedes usar la referencia a los datos nuevos que muestra el método push() para obtener el valor de la clave generada automáticamente del elemento secundario o configurar los datos de dicho elemento. Si llamas a getKey() en una referencia push(), se muestra la clave generada de forma automática.

Puedes usar estas claves generadas de manera automática para simplificar la compactación de tu estructura de datos. Para obtener más información, consulta el ejemplo de fan-out de datos.

Detecta eventos secundarios

Cuando trabajes con listas, tu aplicación debe escuchar eventos secundarios en lugar de los eventos de valor que se usan para objetos individuales.

Los eventos secundarios se activan en respuesta a operaciones específicas que se ejecutan en los elementos secundarios de un nodo a partir de una operación, como un elemento secundario nuevo que se agrega a través del método push() o un elemento secundario que se actualiza a través del método updateChildren(). Cada uno de estos, en combinación, puede ser útil para escuchar los cambios en un nodo específico de una base de datos.

Para escuchar eventos secundarios en DatabaseReference, agrega un ChildEventListener:

Objeto de escucha Devolución de llamada de evento Uso común
ChildEventListener onChildAdded() Recupera listas de elementos o detecta elementos agregados a una lista. Esta devolución de llamada se activa una vez por cada elemento secundario existente y otra vez cuando se agrega un elemento secundario nuevo a la ruta de acceso especificada. La DataSnapshot que se pasa al objeto de escucha contiene los datos del elemento secundario nuevo.
onChildChanged() Detecta cambios en los elementos de una lista. Este evento se activa cada vez que un nodo secundario se modifica, incluidas todas las modificaciones en los descendientes del nodo secundario. La DataSnapshot pasada al objeto de escucha de eventos contiene los datos actualizados del elemento secundario.
onChildRemoved() Detecta cuando se quitan elementos de una lista. La DataSnapshot pasada a la devolución de llamada de eventos contiene los datos para el campo secundario que se quitó.
onChildMoved() Detecta cambios en el orden de los elementos de una lista ordenada. Este evento se activa cada vez que una actualización activa la devolución de llamada onChildChanged() que cambia el orden del elemento secundario. Se usa con datos que se ordenan con orderByChild o orderByValue.

Por ejemplo, una app social de blogs podría usar estos métodos en combinación para supervisar la actividad en los comentarios de una publicación, como se muestra a continuación:

Java

ChildEventListener childEventListener = new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
        Log.d(TAG, "onChildAdded:" + dataSnapshot.getKey());

        // A new comment has been added, add it to the displayed list
        Comment comment = dataSnapshot.getValue(Comment.class);

        // ...
    }

    @Override
    public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) {
        Log.d(TAG, "onChildChanged:" + dataSnapshot.getKey());

        // A comment has changed, use the key to determine if we are displaying this
        // comment and if so displayed the changed comment.
        Comment newComment = dataSnapshot.getValue(Comment.class);
        String commentKey = dataSnapshot.getKey();

        // ...
    }

    @Override
    public void onChildRemoved(DataSnapshot dataSnapshot) {
        Log.d(TAG, "onChildRemoved:" + dataSnapshot.getKey());

        // A comment has changed, use the key to determine if we are displaying this
        // comment and if so remove it.
        String commentKey = dataSnapshot.getKey();

        // ...
    }

    @Override
    public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
        Log.d(TAG, "onChildMoved:" + dataSnapshot.getKey());

        // A comment has changed position, use the key to determine if we are
        // displaying this comment and if so move it.
        Comment movedComment = dataSnapshot.getValue(Comment.class);
        String commentKey = dataSnapshot.getKey();

        // ...
    }

    @Override
    public void onCancelled(DatabaseError databaseError) {
        Log.w(TAG, "postComments:onCancelled", databaseError.toException());
        Toast.makeText(mContext, "Failed to load comments.",
                Toast.LENGTH_SHORT).show();
    }
};
ref.addChildEventListener(childEventListener);

Kotlin

val childEventListener = object : ChildEventListener {
    override fun onChildAdded(dataSnapshot: DataSnapshot, previousChildName: String?) {
        Log.d(TAG, "onChildAdded:" + dataSnapshot.key!!)

        // A new comment has been added, add it to the displayed list
        val comment = dataSnapshot.getValue(Comment::class.java)

        // ...
    }

    override fun onChildChanged(dataSnapshot: DataSnapshot, previousChildName: String?) {
        Log.d(TAG, "onChildChanged: ${dataSnapshot.key}")

        // A comment has changed, use the key to determine if we are displaying this
        // comment and if so displayed the changed comment.
        val newComment = dataSnapshot.getValue(Comment::class.java)
        val commentKey = dataSnapshot.key

        // ...
    }

    override fun onChildRemoved(dataSnapshot: DataSnapshot) {
        Log.d(TAG, "onChildRemoved:" + dataSnapshot.key!!)

        // A comment has changed, use the key to determine if we are displaying this
        // comment and if so remove it.
        val commentKey = dataSnapshot.key

        // ...
    }

    override fun onChildMoved(dataSnapshot: DataSnapshot, previousChildName: String?) {
        Log.d(TAG, "onChildMoved:" + dataSnapshot.key!!)

        // A comment has changed position, use the key to determine if we are
        // displaying this comment and if so move it.
        val movedComment = dataSnapshot.getValue(Comment::class.java)
        val commentKey = dataSnapshot.key

        // ...
    }

    override fun onCancelled(databaseError: DatabaseError) {
        Log.w(TAG, "postComments:onCancelled", databaseError.toException())
        Toast.makeText(context, "Failed to load comments.",
                Toast.LENGTH_SHORT).show()
    }
}
databaseReference.addChildEventListener(childEventListener)

Detecta eventos de valores

Si bien se recomienda usar un ChildEventListener para leer listas de datos, hay situaciones en las que agregar un ValueEventListener a una referencia de lista puede ser útil.

Si agregas un ValueEventListener a una lista de datos, el resultado será una lista completa de datos en forma de una sola DataSnapshot. Si le aplicas un bucle a ese resultado, puedes acceder a cada elemento secundario.

Incluso cuando hay una sola coincidencia para la consulta, la instantánea es una lista, aunque contenga un solo elemento. Para acceder al elemento, debes aplicar un bucle al resultado como se muestra a continuación:

Java

// My top posts by number of stars
myTopPostsQuery.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        for (DataSnapshot postSnapshot: dataSnapshot.getChildren()) {
            // TODO: handle the post
        }
    }

    @Override
    public void onCancelled(DatabaseError databaseError) {
        // Getting Post failed, log a message
        Log.w(TAG, "loadPost:onCancelled", databaseError.toException());
        // ...
    }
});

Kotlin

// My top posts by number of stars
myTopPostsQuery.addValueEventListener(object : ValueEventListener {
    override fun onDataChange(dataSnapshot: DataSnapshot) {
        for (postSnapshot in dataSnapshot.children) {
            // TODO: handle the post
        }
    }

    override fun onCancelled(databaseError: DatabaseError) {
        // Getting Post failed, log a message
        Log.w(TAG, "loadPost:onCancelled", databaseError.toException())
        // ...
    }
})

Este patrón puede ser útil cuando quieres recuperar todos los elementos secundarios de una lista en una sola operación, en lugar de escuchar para detectar eventos onChildAdded adicionales.

Desvincula objetos de escucha

Para quitar las devoluciones de llamada, llama al método removeEventListener() en tu referencia de la base de datos de Firebase.

Si un objeto de escucha se agregó varias veces a una ubicación de datos, se llama varias veces para cada evento y debes desvincularlo la misma cantidad de veces para quitarlo por completo.

Si llamas a removeEventListener() en un objeto de escucha primario, no se quitan automáticamente los objetos de escucha registrados en sus nodos secundarios. Deberás llamar a también en todos los objetos de escucha secundarios para quitar la devolución de llamada.

Ordena y filtra datos

Puedes usar la clase Query de Realtime Database para recuperar los datos organizados por clave, por valor o por valor del elemento secundario. También puedes filtrar el resultado ordenado de acuerdo con una cantidad específica de resultados o un rango de claves o valores.

Ordena los datos

Para recuperar datos ordenados, comienza por especificar uno de los métodos de ordenamiento, a fin de determinar cómo se presentarán los resultados:

Método Uso
orderByChild() Ordena los resultados según el valor de una clave secundaria especificada o una ruta de acceso secundaria anidada.
orderByKey() Ordena los resultados según las claves secundarias.
orderByValue() Ordena los resultados según los valores secundarios.

Puedes usar solo un método de ordenamiento a la vez. Si llamas un método de ordenamiento varias veces en la misma consulta, se genera un error.

En el siguiente ejemplo, se demuestra cómo podrías recuperar una lista de las principales publicaciones de un usuario según la cantidad de estrellas que tienen:

Java

// My top posts by number of stars
String myUserId = getUid();
Query myTopPostsQuery = databaseReference.child("user-posts").child(myUserId)
        .orderByChild("starCount");
myTopPostsQuery.addChildEventListener(new ChildEventListener() {
    // TODO: implement the ChildEventListener methods as documented above
    // ...
});

Kotlin

// My top posts by number of stars
val myUserId = uid
val myTopPostsQuery = databaseReference.child("user-posts").child(myUserId)
    .orderByChild("starCount")

myTopPostsQuery.addChildEventListener(object : ChildEventListener {
    // TODO: implement the ChildEventListener methods as documented above
    // ...
})

Esto define una consulta que, cuando se combina con un objeto de escucha de elementos secundarios, sincroniza el cliente con las publicaciones del usuario en la ruta de la base de datos según el ID del usuario, ordenadas según la cantidad de estrellas que recibió cada publicación. Esta técnica de usar ID como claves de índice se denomina "fan-out de datos”. Puedes obtener más información al respecto en Estructura tu base de datos.

La llamada al método orderByChild() especifica la clave secundaria según la que se deben ordenar los resultados. En este caso, las publicaciones se ordenan según el valor del elemento secundario "starCount" respectivo. Las consultas también se pueden ordenar por campos secundarios anidados si tienes datos con la estructura siguiente:

"posts": {
  "ts-functions": {
    "metrics": {
      "views" : 1200000,
      "likes" : 251000,
      "shares": 1200,
    },
    "title" : "Why you should use TypeScript for writing Cloud Functions",
    "author": "Doug",
  },
  "android-arch-3": {
    "metrics": {
      "views" : 900000,
      "likes" : 117000,
      "shares": 144,
    },
    "title" : "Using Android Architecture Components with Firebase Realtime Database (Part 3)",
    "author": "Doug",
  }
},

En este ejemplo, podemos ordenar los elementos de nuestra lista según los valores anidados en la clave metrics. Para ello, debemos especificar la ruta de acceso relativa al elemento secundario anidado presente en nuestra llamada a orderByChild().

Java

// Most viewed posts
Query myMostViewedPostsQuery = databaseReference.child("posts")
        .orderByChild("metrics/views");
myMostViewedPostsQuery.addChildEventListener(new ChildEventListener() {
    // TODO: implement the ChildEventListener methods as documented above
    // ...
});

Kotlin

// Most viewed posts
val myMostViewedPostsQuery = databaseReference.child("posts")
        .orderByChild("metrics/views")
myMostViewedPostsQuery.addChildEventListener(object : ChildEventListener {
    // TODO: implement the ChildEventListener methods as documented above
    // ...
})

Para obtener más información sobre cómo se ordenan otros tipos de datos, consulta la sección Cómo se ordenan los datos de las consultas.

Cómo filtrar datos

Para filtrar datos, puedes combinar cualquiera de los métodos de límite o rango con un método de ordenamiento cuando generes una consulta.

Método Uso
limitToFirst() Configura la cantidad máxima de elementos que pueden mostrarse desde el comienzo de la lista de resultados ordenada.
limitToLast() Configura la cantidad máxima de elementos que pueden mostrarse desde el final de la lista de resultados ordenada.
startAt() Muestra elementos con un valor igual o superior a la clave o el valor que se especifica según el método de ordenamiento seleccionado.
endAt() Muestra elementos con un valor inferior o igual a la clave o el valor que se especifica según el método de ordenamiento seleccionado.
equalTo() Muestra elementos con un valor igual a la clave o el valor que se especifica según el método de ordenamiento seleccionado.

A diferencia de los métodos de ordenamiento, las funciones de límite y rango pueden combinarse. Por ejemplo, puedes combinar los métodos startAt() y endAt() para limitar los resultados a un rango de valores específico.

Incluso cuando hay una sola coincidencia para la consulta, la instantánea es una lista, aunque contenga un solo elemento. Para acceder al elemento, debes aplicar un bucle al resultado como se muestra a continuación:

Java

// My top posts by number of stars
myTopPostsQuery.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        for (DataSnapshot postSnapshot: dataSnapshot.getChildren()) {
            // TODO: handle the post
        }
    }

    @Override
    public void onCancelled(DatabaseError databaseError) {
        // Getting Post failed, log a message
        Log.w(TAG, "loadPost:onCancelled", databaseError.toException());
        // ...
    }
});

Kotlin

// My top posts by number of stars
myTopPostsQuery.addValueEventListener(object : ValueEventListener {
    override fun onDataChange(dataSnapshot: DataSnapshot) {
        for (postSnapshot in dataSnapshot.children) {
            // TODO: handle the post
        }
    }

    override fun onCancelled(databaseError: DatabaseError) {
        // Getting Post failed, log a message
        Log.w(TAG, "loadPost:onCancelled", databaseError.toException())
        // ...
    }
})

Limita la cantidad de resultados

Puedes usar los métodos limitToFirst() y limitToLast() a fin de establecer una cantidad máxima de elementos secundarios que se sincronicen para una devolución de llamada determinada. Por ejemplo, si usas limitToFirst() para establecer un límite de 100, inicialmente solo recibes hasta 100 devoluciones de llamada de tipo onChildAdded(). Si hay menos de 100 elementos almacenados en la base de datos de Firebase, se activa una devolución de llamada onChildAdded() por cada elemento.

A medida que los elementos cambien, recibirás devoluciones de llamadas de tipo onChildAdded() por los elementos que ingresen en la consulta y devoluciones de llamadas de tipo onChildRemoved() por los elementos que queden fuera para que el número total continúe siendo 100.

En el siguiente ejemplo, se demuestra cómo una app de blog define una consulta para recuperar una lista de las 100 publicaciones más recientes de todos los usuarios:

Java

// Last 100 posts, these are automatically the 100 most recent
// due to sorting by push() keys
Query recentPostsQuery = databaseReference.child("posts")
        .limitToFirst(100);

Kotlin

// Last 100 posts, these are automatically the 100 most recent
// due to sorting by push() keys.
return databaseReference.child("posts")
        .limitToFirst(100)

En este ejemplo, solo se define una consulta. Para sincronizar los datos realmente, se le debe adjuntar un objeto de escucha.

Filtra por clave o valor

Puedes usar startAt(), endAt() y equalTo() a fin de elegir puntos de inicio, finalización y equivalencia arbitrarios para las consultas. Esto puede ser útil para paginar datos o para encontrar elementos con campos secundarios que tengan un valor específico.

Cómo se ordenan los datos de las consultas

En esta sección, se explica cómo se ordenan los datos en cada uno de los métodos de ordenamiento de la clase Query.

orderByChild

Cuando usas orderByChild(), los datos que contienen la clave secundaria especificada se ordenan de la siguiente manera:

  1. Los elementos secundarios cuyas claves secundarias especificadas posean el valor null irán en primer lugar.
  2. A continuación, aparecerán los elementos secundarios que tengan el valor false en la clave secundaria especificada. Si hay varios elementos secundarios con el valor false, se ordenan lexicográficamente por clave.
  3. A continuación, aparecerán los elementos secundarios que tengan el valor true en la clave secundaria especificada. Si hay varios elementos secundarios con el valor true, se ordenan lexicográficamente por clave.
  4. Luego vienen los elementos secundarios con valor numérico, que se ordenan en sentido ascendente. Si varios elementos secundarios tienen el mismo valor numérico en el nodo secundario especificado, se ordenan según la clave.
  5. Las strings van después de los números y se ordenan de manera lexicográfica, en sentido ascendente. Si varios elementos secundarios tienen el mismo valor en el nodo secundario especificado, se ordenan de manera lexicográfica según la clave.
  6. Los objetos quedan en último lugar y se ordenan de manera lexicográfica según su clave, en orden ascendente.

orderByKey

Cuando ordenas los datos con orderByKey(), los datos se presentan en orden ascendente, según la clave.

  1. Los elementos secundarios con una clave que puede analizarse como un número entero de 32 bits van primero, ordenados en sentido ascendente.
  2. Los campos secundarios con un valor de string como clave van después y ordenados de manera lexicográfica, en sentido ascendente.

orderByValue

Cuando usas orderByValue(), los elementos secundarios se ordenan según el valor. Se utiliza el mismo criterio de orden que para orderByChild(), excepto que se usa el valor del nodo en lugar del valor de una clave secundaria especificada.

Próximos pasos