Ir a la consola

Lee y escribe datos en Android

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

Los datos de Firebase se escriben en una referencia de FirebaseDatabase; para recuperarlos, se debe adjuntar un agente de escucha asíncrono a la referencia. El agente de escucha se activa una vez para el estado inicial de los datos y otra vez cuando los datos cambian.

Obtén una DatabaseReference

Para leer o escribir 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 datos

Operaciones básicas de escritura

Para ejecutar operaciones de escritura básicas, puedes usar setValue() para guardar datos en una referencia que especifiques y reemplazar los datos existentes en esa ruta de acceso. Puedes usar este método para lo siguiente:

  • Pasar tipos que corresponden a los tipos disponibles de JSON de la siguiente manera:
    • String
    • Long
    • Double
    • Boolean
    • Map<String, Object>
    • List<Object>
  • Pasar un objeto Java personalizado, si la clase que lo define tiene un constructor predeterminado que no recibe argumentos y tiene métodos de obtención públicos para que se asignen las propiedades.

Si usas un objeto Java, el contenido de tu objeto se asigna automáticamente a las ubicaciones secundarias de forma anidada. Por lo general, el uso de un objeto Java hace que tu código sea más fácil de leer y mantener. Por ejemplo, si tienes una app con un perfil básico de usuario, tu objeto User podría ser similar al siguiente:

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

@IgnoreExtraProperties
data class User(
    var username: String? = "",
    var email: String? = ""
)

Puedes agregar un usuario con setValue(), como se muestra a continuación:

Java

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

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

Kotlin

private fun writeNewUser(userId: String, name: String, email: String?) {
    val user = User(name, email)
    database.child("users").child(userId).setValue(user)
}

Si usas setValue() de esta forma, se sobrescriben los datos en la ubicación especificada, incluidos los nodos secundarios. Sin embargo, es posible actualizar un elemento secundario sin volver a escribir el objeto entero. Si deseas permitir que los usuarios actualicen sus perfiles, podrías actualizar el nombre de usuario de la siguiente forma:

Java

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

Kotlin

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

Detecta eventos de valores

Para leer datos en una ruta de acceso y escuchar para detectar cambios, usa el método addValueEventListener() o el método addListenerForSingleValueEvent() para agregar un ValueEventListener a DatabaseReference.

Agente de escucha Devolución de llamada de evento Uso común
ValueEventListener onDataChange() Lee y detecta cambios en todo el contenido de una ruta.

Puedes usar el método onDataChange() para leer una instantánea estática del contenido de una ruta de acceso determinada, en el estado en que se encontraba en el momento del evento. Este método se activa cuando se adjunta el agente de escucha y se vuelve a activar cada vez que cambian los datos (incluidos los de nivel secundario). La devolución de llamada del evento recibe una instantánea que contiene todos los datos de esa ubicación, incluidos los datos secundarios. Si no hay datos, la instantánea mostrará el valor false cuando llames a exists()null cuando llames 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:

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

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::class.java)
        // ...
    }

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

El objeto de escucha recibe una DataSnapshot que contiene los datos de la ubicación específica de la base de datos en el momento del evento. Si llamas a getValue() en una instantánea, se muestra la representación de los datos del objeto Java. Si no hay datos en la ubicación, el resultado de la llamada a getValue() será null.

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

Lee los datos una sola vez

En algunos casos, es posible que desees obtener una llamada de devolución una vez y después se la quite de inmediato (p. ej., cuando inicializas un elemento de IU que no tienes previsto modificar). Para simplificar ese caso, puedes usar el método addListenerForSingleValueEvent(): se activa una vez y luego no se vuelve a activar.

Esto resulta útil para los datos que solo se deben cargar una vez y que, según lo esperado, no cambiarán con frecuencia ni necesitarán una escucha activa. Por ejemplo, en la app de blogs de los ejemplos anteriores se usa este método para cargar el perfil de un usuario cuando este comienza a crear una publicación nueva:

Actualiza o borra datos

Actualiza campos específicos

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

Cuando llamas a updateChildren(), puedes especificar una ruta de acceso para la clave a fin de actualizar valores secundarios de nivel inferior. Si se almacenan datos en varias ubicaciones para obtener un escalamiento mejor, puedes actualizar todas las instancias de esos datos mediante la distribución de datos. Por ejemplo, una app 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

@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 de forma simultánea con el feed de actividad reciente y el de actividad de las entradas del usuario, 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

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 = HashMap<String, Any>()
    childUpdates["/posts/$key"] = postValues
    childUpdates["/user-posts/$userId/$key"] = postValues

    database.updateChildren(childUpdates)
}

En este ejemplo, se usa push() para crear una entrada en el nodo que contiene las entradas de todos los usuarios en /posts/$postid y recupera la clave con getKey() de manera simultánea. Esta clave se puede usar para crear una segunda entrada en las publicaciones de usuarios en /user-posts/$userid/$postid.

Con estas rutas de acceso, puedes hacer actualizaciones simultáneas en varias ubicaciones del árbol JSON con una única llamada a updateChildren(), tal como este ejemplo crea la publicación nueva en ambas ubicaciones. Las actualizaciones simultáneas que se hacen de esta forma son atómicas: todas las actualizaciones se aplican correctamente o todas fallan.

Agrega una devolución de llamada de finalización

Si quieres saber en qué momento se confirma la escritura de los datos, puedes agregar un objeto de escucha de finalización. Tanto setValue() como updateChildren() toman un objeto de escucha de finalización opcional que se llama cuando la escritura se confirma con éxito en la base de datos. Si la llamada no funciona correctamente, el objeto de escucha recibirá un objeto de error que indicará el motivo.

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

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

Borra datos

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

También puedes borrar datos si especificas null como el valor de otra operación de escritura, como setValue() o updateChildren(). Puedes usar esta técnica con updateChildren() para borrar varios elementos secundarios con una única llamada de la API.

Desvincula agentes de escucha

Para borrar 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 borrarlo por completo.

Si llamas a removeEventListener() en un agente de escucha primario, no se quitan de manera automática los agentes de escucha registrados en sus nodos secundarios. Deberás llamar a removeEventListener() también en todos los agentes de escucha secundarios para borrar la devolución de llamada.

Guarda datos como transacciones

Cuando trabajas con datos que se podrían dañar si se hacen cambios simultáneos (por ejemplo, contadores incrementales) puedes usar una operación de transacción. Esta operación acepta dos argumentos: una función de actualización y una devolución de llamada opcional de finalización. La función de actualización toma el estado actual de los datos como argumento y genera el nuevo estado que deseas escribir. Si otro cliente escribe en la ubicación antes de que tu valor nuevo se escriba correctamente, se hace un nuevo llamado a la función de actualización con el nuevo valor actual y se vuelve a intentar la operación de escritura.

Por ejemplo, en la app social de blogs que mencionamos antes, podrías permitir que los usuarios agreguen o quiten estrellas en las entradas y llevar un seguimiento de cuántas estrellas recibió una entrada de la siguiente forma:

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 b,
                               DataSnapshot dataSnapshot) {
            // Transaction completed
            Log.d(TAG, "postTransaction:onComplete:" + databaseError);
        }
    });
}

Kotlin

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?,
            b: Boolean,
            dataSnapshot: DataSnapshot?
        ) {
            // Transaction completed
            Log.d(TAG, "postTransaction:onComplete:" + databaseError!!)
        }
    })
}

Si usas una transacción, evitas que el recuento de estrellas sea incorrecto en caso de que varios usuarios agreguen estrellas a la publicación al mismo tiempo o el cliente tenga datos inactivos. Si se rechaza la transacción, el servidor muestra el valor actual al cliente, que vuelve a ejecutar la transacción con el valor actualizado. Esto se repite hasta que se acepte la transacción o hasta que se registren demasiados intentos.

Escribe datos sin conexión

Si un cliente pierde la conexión de red, la app continúa funcionando de manera correcta.

Todos los clientes conectados a una base de datos de Firebase mantienen su propia versión interna de los datos activos. Cuando se escriben datos, se hace primero en esta versión local. Después, el cliente de Firebase sincroniza esos datos con los servidores de bases de datos remotas y con otros clientes según el “mejor esfuerzo”.

Como resultado, todas las operaciones de escritura en la base de datos activan eventos locales al instante, antes de que se escriban datos en el servidor. Esto significa que la app conserva la capacidad de respuesta, sin importar la latencia o el estado de conexión de la red.

Cuando se restablece la conexión, tu app recibe el conjunto de eventos adecuado, de manera que el cliente se sincronice con el estado actual del servidor sin tener que escribir código personalizado.

Pasos siguientes