Catch up on everything announced at Firebase Summit, and learn how Firebase can help you accelerate app development and run your app with confidence. Learn More

Leer y escribir datos en Android

Organiza tus páginas con colecciones Guarda y categoriza el contenido según tus preferencias.

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

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

(Opcional) Realice prototipos y pruebas 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 Realtime Database 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 tu proyecto local, ejecuta firebase emulators:start .
  3. Hacer llamadas desde el código prototipo de su aplicación usando un SDK de la plataforma Realtime Database como de costumbre, o usando la API REST de Realtime Database.

Se encuentra disponible un tutorial detallado que involucra Realtime Database y Cloud Functions . También debería echar un vistazo a la introducción de 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 :

Java

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

Kotlin+KTX

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

Escribir datos

Operaciones básicas de escritura

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

  • Pase los tipos que correspondan 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 toma argumentos y tiene captadores públicos para las propiedades que se van a asignar.

Si usa 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 su 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:

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;
    }

}

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.
}

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

Java

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

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

Kotlin+KTX

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

    database.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 puede actualizar un elemento secundario sin volver a escribir todo el objeto. Si desea permitir que los usuarios actualicen sus perfiles, puede actualizar el nombre de usuario de la siguiente manera:

Java

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

Kotlin+KTX

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

Leer datos

Leer datos con oyentes persistentes

Para leer datos en una ruta y escuchar los 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 usar el método onDataChange() para leer una instantánea estática de los contenidos en una ruta determinada, tal como existían 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 elementos secundarios. La devolución de llamada del evento recibe 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 a exists() y null cuando llame a getValue() en ella.

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

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);

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)

El oyente recibe un 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 a getValue() devuelve null .

En este ejemplo, ValueEventListener también define el método onCancelled() al que se llama si se cancela la lectura. Por ejemplo, se puede cancelar una lectura 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é ocurrió la falla.

Leer datos una vez

Leer una vez usando get()

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

En general, debe usar las técnicas de ValueEventListener descritas anteriormente para leer datos y recibir notificaciones de actualizaciones de datos desde el backend. Las técnicas de escucha reducen su uso y facturación, y están optimizadas para brindarles a sus usuarios la mejor experiencia mientras 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 alguna razón 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 mediante el uso de un oyente en tiempo real como se muestra arriba.

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()));
        }
    }
});

Kotlin+KTX

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

Leer una vez usando un oyente

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

Esto es útil para los 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 usa este método para cargar el perfil de un usuario cuando comienza a crear una nueva publicación.

Actualizar o eliminar datos

Actualizar campos específicos

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

Al llamar a 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 distribución de datos . Por ejemplo, una aplicación de blogs sociales podría tener una clase Post como esta:

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;
    }
}

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
        )
    }
}

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

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);
}

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)
}

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: todas las actualizaciones se realizan correctamente o todas fallan.

Agregar una devolución de llamada de finalización

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

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
                // ...
            }
        });

Kotlin+KTX

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

Borrar datos

La forma más sencilla de eliminar datos es llamar a 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 usar esta técnica con updateChildren() para eliminar varios elementos secundarios en una sola llamada a la API.

Separar oyentes

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

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

Llamar a removeEventListener() en un agente de escucha principal no elimina automáticamente los agentes de escucha registrados en sus nodos secundarios; removeEventListener() también debe llamarse en cualquier oyente secundario para eliminar la devolución de llamada.

Guardar datos como transacciones

Cuando trabaje con datos que podrían corromperse por modificaciones simultáneas, como contadores incrementales, puede usar una operación de transacción . Le da a esta operación 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, su función de actualización se vuelve a llamar con el nuevo valor actual y se vuelve a intentar la escritura.

Por ejemplo, en la aplicación de blogs sociales de ejemplo, puede permitir que los usuarios destaquen y no destaquen publicaciones y realicen un seguimiento de cuántas estrellas ha recibido una publicación de la siguiente manera:

Java

private void onStarClicked(DatabaseReference postRef) {
    postRef.runTransaction(new Transaction.Handler() {
        @Override
        public Transaction.Result doTransaction(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);
        }
    });
}

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!!)
        }
    })
}

El uso de una transacción evita que los recuentos de estrellas sean incorrectos si varios usuarios protagonizan la misma publicación al mismo tiempo o si el cliente tenía datos obsoletos. Si se rechaza la transacción, el servidor devuelve el valor actual al cliente, que vuelve a ejecutar la transacción 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, estamos escribiendo dos valores en la base de datos: la ID del usuario que destaca/quita la estrella de la publicación y el número de estrellas incrementado. Si ya sabemos que el usuario está protagonizando la publicación, podemos usar una operación de incremento atómico en lugar de una transacción.

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);
}

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)
}

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 conflicto.

Si desea detectar y rechazar conflictos específicos de la aplicación, como un usuario que destaca una publicación que ya ha destacado 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 utilicen escuchas o que esté marcado para mantenerse sincronizado con el servidor. Cuando se leen o escriben datos, esta versión local de los datos se usa primero. Luego, el cliente de Firebase sincroniza esos datos con los servidores de base de datos remotos y con otros clientes en base 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 de eventos adecuado 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 Obtenga más información sobre las capacidades en línea y fuera de línea .

Próximos pasos