Leer y escribir datos en Android

Este documento cubre los conceptos básicos de lectura y escritura de datos de Firebase.

Los datos de Firebase se escriben en una referencia FirebaseDatabase y se recuperan adjuntando un detector asincrónico a la referencia. El oyente se activa una vez para el estado inicial de los datos y nuevamente cada vez que cambian los datos.

(Opcional) Prototipo y prueba con Firebase Local Emulator Suite

Antes de hablar sobre cómo su aplicación lee y escribe en Realtime Database, presentemos un conjunto de herramientas que puede usar para crear prototipos y probar la funcionalidad de Realtime Database: Firebase Local Emulator Suite. Si está probando diferentes modelos de datos, optimizando sus reglas de seguridad o trabajando para encontrar la forma más rentable de interactuar con el back-end, poder trabajar localmente sin implementar servicios en vivo puede ser una gran idea.

Un emulador de base de datos en tiempo real es parte de Local Emulator Suite, que permite que su aplicación interactúe con el contenido y la configuración de su base de datos emulada, así como, opcionalmente, con los recursos de su proyecto emulado (funciones, otras bases de datos y reglas de seguridad).

Usar el emulador de Realtime Database implica solo unos pocos pasos:

  1. Agregar una línea de código a la configuración de prueba de su aplicación para conectarse al emulador.
  2. Desde la raíz del directorio de su proyecto local, ejecute firebase emulators:start .
  3. Realizar llamadas desde el código prototipo de su aplicación usando un SDK de plataforma Realtime Database como de costumbre, o usando la API REST de Realtime Database.

Está disponible un tutorial detallado sobre la base de datos en tiempo real y las funciones de la nube . También deberías echar un vistazo a la introducción a Local Emulator Suite .

Obtener una referencia de base de datos

Para leer o escribir datos de la base de datos, necesita una instancia de DatabaseReference :

Kotlin+KTX

private lateinit var database: DatabaseReference
// ...
database = Firebase.database.reference

Java

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

Escribir datos

Operaciones básicas de escritura

Para operaciones de escritura básicas, puede usar setValue() para guardar datos en una referencia específica, reemplazando cualquier dato existente en esa ruta. Puede utilizar este método para:

  • Tipos de pase que corresponden a los tipos JSON disponibles de la siguiente manera:
    • String
    • Long
    • Double
    • Boolean
    • Map<String, Object>
    • List<Object>
  • Pase un objeto Java personalizado, si la clase que lo define tiene un constructor predeterminado que no acepta argumentos y tiene captadores públicos para las propiedades que se asignarán.

Si utiliza un objeto Java, el contenido de su objeto se asigna automáticamente a ubicaciones secundarias de forma anidada. El uso de un objeto Java también suele hacer que el código sea más legible y más fácil de mantener. Por ejemplo, si tiene una aplicación con un perfil de usuario básico, su objeto User podría tener el siguiente aspecto:

Kotlin+KTX

@IgnoreExtraProperties
data class User(val username: String? = null, val email: String? = null) {
    // Null default values create a no-argument default constructor, which is needed
    // for deserialization from a DataSnapshot.
}

Java

@IgnoreExtraProperties
public class User {

    public String username;
    public String email;

    public User() {
        // Default constructor required for calls to DataSnapshot.getValue(User.class)
    }

    public User(String username, String email) {
        this.username = username;
        this.email = email;
    }

}

Puede agregar un usuario con setValue() de la siguiente manera:

Kotlin+KTX

fun writeNewUser(userId: String, name: String, email: String) {
    val user = User(name, email)

    database.child("users").child(userId).setValue(user)
}

Java

public void writeNewUser(String userId, String name, String email) {
    User user = new User(name, email);

    mDatabase.child("users").child(userId).setValue(user);
}

El uso setValue() de esta manera sobrescribe los datos en la ubicación especificada, incluidos los nodos secundarios. Sin embargo, aún puedes actualizar un elemento secundario sin tener que reescribir todo el objeto. Si desea permitir que los usuarios actualicen sus perfiles, puede actualizar el nombre de usuario de la siguiente manera:

Kotlin+KTX

database.child("users").child(userId).child("username").setValue(name)

Java

mDatabase.child("users").child(userId).child("username").setValue(name);

leer datos

Leer datos con oyentes persistentes

Para leer datos en una ruta y escuchar cambios, use el método addValueEventListener() para agregar un ValueEventListener a una DatabaseReference .

Oyente Devolución de llamada de evento Uso típico
ValueEventListener onDataChange() Lea y escuche los cambios en todo el contenido de una ruta.

Puede utilizar el método onDataChange() para leer una instantánea estática del contenido en una ruta determinada, tal como existía en el momento del evento. Este método se activa una vez cuando se adjunta el oyente y nuevamente cada vez que cambian los datos, incluidos los secundarios. A la devolución de llamada del evento se le pasa una instantánea que contiene todos los datos en esa ubicación, incluidos los datos secundarios. Si no hay datos, la instantánea devolverá false cuando llame exists() y null cuando llame a getValue() .

El siguiente ejemplo demuestra una aplicación de blogs sociales que recupera los detalles de una publicación de la base de datos:

Kotlin+KTX

val postListener = object : ValueEventListener {
    override fun onDataChange(dataSnapshot: DataSnapshot) {
        // Get Post object and use the values to update the UI
        val post = dataSnapshot.getValue<Post>()
        // ...
    }

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

Java

ValueEventListener postListener = new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        // Get Post object and use the values to update the UI
        Post post = dataSnapshot.getValue(Post.class);
        // ..
    }

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

El oyente recibe una DataSnapshot que contiene los datos en la ubicación especificada en la base de datos en el momento del evento. Llamar a getValue() en una instantánea devuelve la representación del objeto Java de los datos. Si no existen datos en la ubicación, llamar getValue() devuelve null .

En este ejemplo, ValueEventListener también define el método onCancelled() que se llama si se cancela la lectura. Por ejemplo, una lectura se puede cancelar si el cliente no tiene permiso para leer desde una ubicación de base de datos de Firebase. A este método se le pasa un objeto DatabaseError que indica por qué se produjo el error.

Leer datos una vez

Leer una vez usando get()

El SDK está diseñado para gestionar interacciones con servidores de bases de datos, ya sea que su aplicación esté en línea o fuera de línea.

Generalmente, debe utilizar las técnicas ValueEventListener descritas anteriormente para leer datos y recibir notificaciones sobre las actualizaciones de los datos desde el backend. Las técnicas de escucha reducen su uso y facturación y están optimizadas para brindar a sus usuarios la mejor experiencia cuando se conectan y desconectan.

Si necesita los datos solo una vez, puede usar get() para obtener una instantánea de los datos de la base de datos. Si por algún motivo get() no puede devolver el valor del servidor, el cliente sondeará el caché de almacenamiento local y devolverá un error si aún no se encuentra el valor.

El uso innecesario de get() puede aumentar el uso del ancho de banda y provocar una pérdida de rendimiento, lo que se puede evitar utilizando un escucha en tiempo real como se muestra arriba.

Kotlin+KTX

mDatabase.child("users").child(userId).get().addOnSuccessListener {
    Log.i("firebase", "Got value ${it.value}")
}.addOnFailureListener{
    Log.e("firebase", "Error getting data", it)
}

Java

mDatabase.child("users").child(userId).get().addOnCompleteListener(new OnCompleteListener<DataSnapshot>() {
    @Override
    public void onComplete(@NonNull Task<DataSnapshot> task) {
        if (!task.isSuccessful()) {
            Log.e("firebase", "Error getting data", task.getException());
        }
        else {
            Log.d("firebase", String.valueOf(task.getResult().getValue()));
        }
    }
});

Leer una vez usando un oyente

En algunos casos, es posible que desee que el valor de la caché local se devuelva inmediatamente, en lugar de buscar un valor actualizado en el servidor. En esos casos, puede utilizar addListenerForSingleValueEvent para obtener los datos del caché del disco local inmediatamente.

Esto es útil para datos que solo deben cargarse una vez y no se espera que cambien con frecuencia ni requieran una escucha activa. Por ejemplo, la aplicación de blogs de los ejemplos anteriores utiliza este método para cargar el perfil de un usuario cuando comienza a escribir una nueva publicación.

Actualizar o eliminar datos

Actualizar campos específicos

Para escribir simultáneamente en hijos específicos de un nodo sin sobrescribir otros nodos hijos, utilice el método updateChildren() .

Al llamar updateChildren() , puede actualizar los valores secundarios de nivel inferior especificando una ruta para la clave. Si los datos se almacenan en varias ubicaciones para escalar mejor, puede actualizar todas las instancias de esos datos mediante la distribución en abanico de datos . Por ejemplo, una aplicación de blogs sociales podría tener una clase Post como esta:

Kotlin+KTX

@IgnoreExtraProperties
data class Post(
    var uid: String? = "",
    var author: String? = "",
    var title: String? = "",
    var body: String? = "",
    var starCount: Int = 0,
    var stars: MutableMap<String, Boolean> = HashMap(),
) {

    @Exclude
    fun toMap(): Map<String, Any?> {
        return mapOf(
            "uid" to uid,
            "author" to author,
            "title" to title,
            "body" to body,
            "starCount" to starCount,
            "stars" to stars,
        )
    }
}

Java

@IgnoreExtraProperties
public class Post {

    public String uid;
    public String author;
    public String title;
    public String body;
    public int starCount = 0;
    public Map<String, Boolean> stars = new HashMap<>();

    public Post() {
        // Default constructor required for calls to DataSnapshot.getValue(Post.class)
    }

    public Post(String uid, String author, String title, String body) {
        this.uid = uid;
        this.author = author;
        this.title = title;
        this.body = body;
    }

    @Exclude
    public Map<String, Object> toMap() {
        HashMap<String, Object> result = new HashMap<>();
        result.put("uid", uid);
        result.put("author", author);
        result.put("title", title);
        result.put("body", body);
        result.put("starCount", starCount);
        result.put("stars", stars);

        return result;
    }
}

Para crear una publicación y actualizarla simultáneamente a la fuente de actividad reciente y a la fuente de actividad del usuario que publica, la aplicación de blogs usa un código como este:

Kotlin+KTX

private fun writeNewPost(userId: String, username: String, title: String, body: String) {
    // Create new post at /user-posts/$userid/$postid and at
    // /posts/$postid simultaneously
    val key = database.child("posts").push().key
    if (key == null) {
        Log.w(TAG, "Couldn't get push key for posts")
        return
    }

    val post = Post(userId, username, title, body)
    val postValues = post.toMap()

    val childUpdates = hashMapOf<String, Any>(
        "/posts/$key" to postValues,
        "/user-posts/$userId/$key" to postValues,
    )

    database.updateChildren(childUpdates)
}

Java

private void writeNewPost(String userId, String username, String title, String body) {
    // Create new post at /user-posts/$userid/$postid and at
    // /posts/$postid simultaneously
    String key = mDatabase.child("posts").push().getKey();
    Post post = new Post(userId, username, title, body);
    Map<String, Object> postValues = post.toMap();

    Map<String, Object> childUpdates = new HashMap<>();
    childUpdates.put("/posts/" + key, postValues);
    childUpdates.put("/user-posts/" + userId + "/" + key, postValues);

    mDatabase.updateChildren(childUpdates);
}

Este ejemplo usa push() para crear una publicación en el nodo que contiene publicaciones para todos los usuarios en /posts/$postid y simultáneamente recupera la clave con getKey() . Luego, la clave se puede usar para crear una segunda entrada en las publicaciones del usuario en /user-posts/$userid/$postid .

Con estas rutas, puede realizar actualizaciones simultáneas en varias ubicaciones en el árbol JSON con una sola llamada a updateChildren() , como en este ejemplo se crea la nueva publicación en ambas ubicaciones. Las actualizaciones simultáneas realizadas de esta manera son atómicas: o todas las actualizaciones tienen éxito o todas fallan.

Agregar una devolución de llamada de finalización

Si desea saber cuándo se confirmaron sus datos, puede agregar un detector de finalización. Tanto setValue() como updateChildren() toman un detector de finalización opcional que se llama cuando la escritura se ha confirmado correctamente en la base de datos. Si la llamada no tuvo éxito, se pasa al oyente un objeto de error que indica por qué se produjo el error.

Kotlin+KTX

database.child("users").child(userId).setValue(user)
    .addOnSuccessListener {
        // Write was successful!
        // ...
    }
    .addOnFailureListener {
        // Write failed
        // ...
    }

Java

mDatabase.child("users").child(userId).setValue(user)
        .addOnSuccessListener(new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(Void aVoid) {
                // Write was successful!
                // ...
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // Write failed
                // ...
            }
        });

Borrar datos

La forma más sencilla de eliminar datos es llamar removeValue() en una referencia a la ubicación de esos datos.

También puede eliminar especificando null como valor para otra operación de escritura como setValue() o updateChildren() . Puede utilizar esta técnica con updateChildren() para eliminar varios elementos secundarios en una única llamada API.

Separar oyentes

Las devoluciones de llamada se eliminan llamando al método removeEventListener() en la referencia de tu base de datos de Firebase.

Si un oyente se ha agregado varias veces a una ubicación de datos, se llama varias veces para cada evento y debe separarlo la misma cantidad de veces para eliminarlo por completo.

Llamar removeEventListener() en un oyente principal no elimina automáticamente los oyentes registrados en sus nodos secundarios; También se debe invocar removeEventListener() en cualquier oyente secundario para eliminar la devolución de llamada.

Guardar datos como transacciones

Cuando trabaje con datos que podrían dañarse debido a modificaciones simultáneas, como contadores incrementales, puede utilizar una operación de transacción . A esta operación se le dan dos argumentos: una función de actualización y una devolución de llamada de finalización opcional. La función de actualización toma el estado actual de los datos como argumento y devuelve el nuevo estado deseado que le gustaría escribir. Si otro cliente escribe en la ubicación antes de que su nuevo valor se escriba correctamente, se llama nuevamente a su función de actualización con el nuevo valor actual y se vuelve a intentar la escritura.

Por ejemplo, en la aplicación de blogs sociales de ejemplo, podría permitir a los usuarios destacar y quitar publicaciones y realizar un seguimiento de cuántas estrellas ha recibido una publicación de la siguiente manera:

Kotlin+KTX

private fun onStarClicked(postRef: DatabaseReference) {
    // ...
    postRef.runTransaction(object : Transaction.Handler {
        override fun doTransaction(mutableData: MutableData): Transaction.Result {
            val p = mutableData.getValue(Post::class.java)
                ?: return Transaction.success(mutableData)

            if (p.stars.containsKey(uid)) {
                // Unstar the post and remove self from stars
                p.starCount = p.starCount - 1
                p.stars.remove(uid)
            } else {
                // Star the post and add self to stars
                p.starCount = p.starCount + 1
                p.stars[uid] = true
            }

            // Set value and report transaction success
            mutableData.value = p
            return Transaction.success(mutableData)
        }

        override fun onComplete(
            databaseError: DatabaseError?,
            committed: Boolean,
            currentData: DataSnapshot?,
        ) {
            // Transaction completed
            Log.d(TAG, "postTransaction:onComplete:" + databaseError!!)
        }
    })
}

Java

private void onStarClicked(DatabaseReference postRef) {
    postRef.runTransaction(new Transaction.Handler() {
        @NonNull
        @Override
        public Transaction.Result doTransaction(@NonNull MutableData mutableData) {
            Post p = mutableData.getValue(Post.class);
            if (p == null) {
                return Transaction.success(mutableData);
            }

            if (p.stars.containsKey(getUid())) {
                // Unstar the post and remove self from stars
                p.starCount = p.starCount - 1;
                p.stars.remove(getUid());
            } else {
                // Star the post and add self to stars
                p.starCount = p.starCount + 1;
                p.stars.put(getUid(), true);
            }

            // Set value and report transaction success
            mutableData.setValue(p);
            return Transaction.success(mutableData);
        }

        @Override
        public void onComplete(DatabaseError databaseError, boolean committed,
                               DataSnapshot currentData) {
            // Transaction completed
            Log.d(TAG, "postTransaction:onComplete:" + databaseError);
        }
    });
}

El uso de una transacción evita que el recuento de estrellas sea incorrecto si varios usuarios destacan la misma publicación al mismo tiempo o si el cliente tenía datos obsoletos. Si la transacción es rechazada, el servidor devuelve el valor actual al cliente, que ejecuta la transacción nuevamente con el valor actualizado. Esto se repite hasta que se acepta la transacción o se han realizado demasiados intentos.

Incrementos atómicos del lado del servidor

En el caso de uso anterior, escribimos dos valores en la base de datos: el ID del usuario que destaca o quita la estrella de la publicación y el recuento de estrellas incrementado. Si ya sabemos que el usuario protagoniza la publicación, podemos usar una operación de incremento atómico en lugar de una transacción.

Kotlin+KTX

private fun onStarClicked(uid: String, key: String) {
    val updates: MutableMap<String, Any> = hashMapOf(
        "posts/$key/stars/$uid" to true,
        "posts/$key/starCount" to ServerValue.increment(1),
        "user-posts/$uid/$key/stars/$uid" to true,
        "user-posts/$uid/$key/starCount" to ServerValue.increment(1),
    )
    database.updateChildren(updates)
}

Java

private void onStarClicked(String uid, String key) {
    Map<String, Object> updates = new HashMap<>();
    updates.put("posts/"+key+"/stars/"+uid, true);
    updates.put("posts/"+key+"/starCount", ServerValue.increment(1));
    updates.put("user-posts/"+uid+"/"+key+"/stars/"+uid, true);
    updates.put("user-posts/"+uid+"/"+key+"/starCount", ServerValue.increment(1));
    mDatabase.updateChildren(updates);
}

Este código no utiliza una operación de transacción, por lo que no se vuelve a ejecutar automáticamente si hay una actualización conflictiva. Sin embargo, dado que la operación de incremento ocurre directamente en el servidor de la base de datos, no hay posibilidad de que se produzca un conflicto.

Si desea detectar y rechazar conflictos específicos de una aplicación, como un usuario que destaca una publicación que ya destacó anteriormente, debe escribir reglas de seguridad personalizadas para ese caso de uso.

Trabajar con datos sin conexión

Si un cliente pierde su conexión de red, su aplicación seguirá funcionando correctamente.

Cada cliente conectado a una base de datos de Firebase mantiene su propia versión interna de cualquier dato en el que se estén utilizando oyentes o que esté marcado para mantenerse sincronizado con el servidor. Cuando se leen o escriben datos, esta versión local de los datos se utiliza primero. Luego, el cliente de Firebase sincroniza esos datos con los servidores de bases de datos remotos y con otros clientes al "mejor esfuerzo".

Como resultado, todas las escrituras en la base de datos desencadenan eventos locales inmediatamente, antes de cualquier interacción con el servidor. Esto significa que su aplicación sigue respondiendo independientemente de la latencia o la conectividad de la red.

Una vez que se restablece la conectividad, su aplicación recibe el conjunto apropiado de eventos para que el cliente se sincronice con el estado actual del servidor, sin tener que escribir ningún código personalizado.

Hablaremos más sobre el comportamiento fuera de línea en Más información sobre las capacidades en línea y fuera de línea .

Próximos pasos