Lire et écrire des données sur Android

Ce document couvre les bases de la lecture et de l'écriture de données Firebase.

Les données Firebase sont écrites dans une référence FirebaseDatabase et récupérées en attachant un écouteur asynchrone à la référence. L'écouteur est déclenché une fois pour l'état initial des données et de nouveau à chaque fois que les données changent.

(Facultatif) Prototyper et tester avec Firebase Local Emulator Suite

Avant de parler de la façon dont votre application lit et écrit dans la base de données en temps réel, présentons un ensemble d'outils que vous pouvez utiliser pour prototyper et tester les fonctionnalités de la base de données en temps réel : Firebase Local Emulator Suite. Si vous essayez différents modèles de données, optimisez vos règles de sécurité ou cherchez le moyen le plus rentable d'interagir avec le back-end, pouvoir travailler localement sans déployer de services en direct peut être une excellente idée.

Un émulateur de base de données en temps réel fait partie de Local Emulator Suite, qui permet à votre application d'interagir avec le contenu et la configuration de votre base de données émulée, ainsi qu'éventuellement avec les ressources de votre projet émulé (fonctions, autres bases de données et règles de sécurité).

L'utilisation de l'émulateur Realtime Database ne nécessite que quelques étapes :

  1. Ajout d'une ligne de code à la configuration de test de votre application pour vous connecter à l'émulateur.
  2. À partir de la racine du répertoire de votre projet local, exécutez firebase emulators:start .
  3. Effectuer des appels à partir du code prototype de votre application à l'aide d'un SDK de plateforme Realtime Database comme d'habitude ou à l'aide de l'API REST Realtime Database.

Une présentation détaillée impliquant la base de données en temps réel et les fonctions cloud est disponible. Vous devriez également jeter un œil à l’introduction de Local Emulator Suite .

Obtenir une référence de base de données

Pour lire ou écrire des données à partir de la base de données, vous avez besoin d'une instance de DatabaseReference :

Kotlin+KTX

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

Java

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

Écrire des données

Opérations d'écriture de base

Pour les opérations d'écriture de base, vous pouvez utiliser setValue() pour enregistrer les données dans une référence spécifiée, en remplaçant toutes les données existantes à ce chemin. Vous pouvez utiliser cette méthode pour :

  • Transmettez les types qui correspondent aux types JSON disponibles comme suit :
    • String
    • Long
    • Double
    • Boolean
    • Map<String, Object>
    • List<Object>
  • Transmettez un objet Java personnalisé, si la classe qui le définit a un constructeur par défaut qui ne prend aucun argument et dispose de getters publics pour les propriétés à attribuer.

Si vous utilisez un objet Java, le contenu de votre objet est automatiquement mappé aux emplacements enfants de manière imbriquée. L'utilisation d'un objet Java rend également généralement votre code plus lisible et plus facile à maintenir. Par exemple, si vous disposez d’une application avec un profil utilisateur de base, votre objet User peut ressembler à ceci :

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

}

Vous pouvez ajouter un utilisateur avec setValue() comme suit :

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

L'utilisation setValue() de cette manière écrase les données à l'emplacement spécifié, y compris les nœuds enfants. Cependant, vous pouvez toujours mettre à jour un enfant sans réécrire l'intégralité de l'objet. Si vous souhaitez autoriser les utilisateurs à mettre à jour leur profil, vous pouvez mettre à jour le nom d'utilisateur comme suit :

Kotlin+KTX

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

Java

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

Lire des données

Lire des données avec des écouteurs persistants

Pour lire des données sur un chemin et écouter les modifications, utilisez la méthode addValueEventListener() pour ajouter un ValueEventListener à un DatabaseReference .

Auditeur Rappel d'événement Utilisation typique
ValueEventListener onDataChange() Lisez et écoutez les modifications apportées à l'ensemble du contenu d'un chemin.

Vous pouvez utiliser la méthode onDataChange() pour lire un instantané statique du contenu à un chemin donné, tel qu'il existait au moment de l'événement. Cette méthode est déclenchée une fois lorsque l'écouteur est attaché et à nouveau chaque fois que les données, y compris les enfants, changent. Le rappel d'événement reçoit un instantané contenant toutes les données à cet emplacement, y compris les données enfants. S'il n'y a pas de données, l'instantané renverra false lorsque vous appellerez exists() et null lorsque vous appellerez getValue() dessus.

L'exemple suivant montre une application de blog social récupérant les détails d'une publication de la base de données :

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

L'écouteur reçoit un DataSnapshot qui contient les données à l'emplacement spécifié dans la base de données au moment de l'événement. L'appel de getValue() sur un instantané renvoie la représentation objet Java des données. Si aucune donnée n'existe à l'emplacement, l'appel getValue() renvoie null .

Dans cet exemple, ValueEventListener définit également la méthode onCancelled() qui est appelée si la lecture est annulée. Par exemple, une lecture peut être annulée si le client n'est pas autorisé à lire à partir d'un emplacement de base de données Firebase. Cette méthode reçoit un objet DatabaseError indiquant la raison pour laquelle l'échec s'est produit.

Lire les données une fois

Lire une fois en utilisant get()

Le SDK est conçu pour gérer les interactions avec les serveurs de bases de données, que votre application soit en ligne ou hors ligne.

En règle générale, vous devez utiliser les techniques ValueEventListener décrites ci-dessus pour lire les données afin d'être informé des mises à jour des données depuis le backend. Les techniques d'écoute réduisent votre utilisation et votre facturation et sont optimisées pour offrir à vos utilisateurs la meilleure expérience lorsqu'ils se connectent et hors ligne.

Si vous n'avez besoin des données qu'une seule fois, vous pouvez utiliser get() pour obtenir un instantané des données de la base de données. Si, pour une raison quelconque get() ne parvient pas à renvoyer la valeur du serveur, le client sondera le cache de stockage local et renverra une erreur si la valeur n'est toujours pas trouvée.

L'utilisation inutile de get() peut augmenter l'utilisation de la bande passante et entraîner une perte de performances, ce qui peut être évité en utilisant un écouteur en temps réel comme indiqué ci-dessus.

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

Lire une fois à l'aide d'un auditeur

Dans certains cas, vous souhaiterez peut-être que la valeur du cache local soit renvoyée immédiatement, au lieu de rechercher une valeur mise à jour sur le serveur. Dans ces cas, vous pouvez utiliser addListenerForSingleValueEvent pour obtenir immédiatement les données du cache disque local.

Ceci est utile pour les données qui ne doivent être chargées qu’une seule fois et qui ne devraient pas changer fréquemment ni nécessiter une écoute active. Par exemple, l'application de blog dans les exemples précédents utilise cette méthode pour charger le profil d'un utilisateur lorsqu'il commence à rédiger un nouveau message.

Mise à jour ou suppression de données

Mettre à jour des champs spécifiques

Pour écrire simultanément sur des enfants spécifiques d'un nœud sans écraser les autres nœuds enfants, utilisez la méthode updateChildren() .

Lorsque vous appelez updateChildren() , vous pouvez mettre à jour les valeurs enfants de niveau inférieur en spécifiant un chemin pour la clé. Si les données sont stockées à plusieurs emplacements pour mieux évoluer, vous pouvez mettre à jour toutes les instances de ces données à l'aide de la répartition des données . Par exemple, une application de blog social peut avoir une classe Post comme celle-ci :

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

Pour créer une publication et la mettre simultanément à jour avec le flux d'activité récente et le flux d'activité de l'utilisateur qui a publié la publication, l'application de blog utilise un code comme celui-ci :

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

Cet exemple utilise push() pour créer une publication dans le nœud contenant des publications pour tous les utilisateurs de /posts/$postid et récupérer simultanément la clé avec getKey() . La clé peut ensuite être utilisée pour créer une deuxième entrée dans les publications de l'utilisateur à /user-posts/$userid/$postid .

En utilisant ces chemins, vous pouvez effectuer des mises à jour simultanées vers plusieurs emplacements de l'arborescence JSON avec un seul appel à updateChildren() , par exemple comment cet exemple crée la nouvelle publication aux deux emplacements. Les mises à jour simultanées effectuées de cette manière sont atomiques : soit toutes les mises à jour réussissent, soit toutes les mises à jour échouent.

Ajouter un rappel d'achèvement

Si vous souhaitez savoir quand vos données ont été validées, vous pouvez ajouter un écouteur d'achèvement. setValue() et updateChildren() prennent tous deux un écouteur d'achèvement facultatif qui est appelé lorsque l'écriture a été validée avec succès dans la base de données. Si l'appel échoue, l'auditeur reçoit un objet d'erreur indiquant la raison de l'échec.

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

Suprimmer les données

Le moyen le plus simple de supprimer des données consiste à appeler removeValue() sur une référence à l'emplacement de ces données.

Vous pouvez également supprimer en spécifiant null comme valeur pour une autre opération d'écriture telle que setValue() ou updateChildren() . Vous pouvez utiliser cette technique avec updateChildren() pour supprimer plusieurs enfants en un seul appel API.

Détacher les auditeurs

Les rappels sont supprimés en appelant la méthode removeEventListener() sur votre référence de base de données Firebase.

Si un écouteur a été ajouté plusieurs fois à un emplacement de données, il est appelé plusieurs fois pour chaque événement et vous devez le détacher le même nombre de fois pour le supprimer complètement.

L'appel removeEventListener() sur un écouteur parent ne supprime pas automatiquement les écouteurs enregistrés sur ses nœuds enfants ; removeEventListener() doit également être appelé sur tous les écouteurs enfants pour supprimer le rappel.

Enregistrer les données en tant que transactions

Lorsque vous travaillez avec des données susceptibles d'être corrompues par des modifications simultanées, telles que des compteurs incrémentiels, vous pouvez utiliser une opération de transaction . Vous donnez à cette opération deux arguments : une fonction de mise à jour et un rappel d'achèvement facultatif. La fonction de mise à jour prend l'état actuel des données comme argument et renvoie le nouvel état souhaité que vous souhaitez écrire. Si un autre client écrit à l'emplacement avant que votre nouvelle valeur ne soit écrite avec succès, votre fonction de mise à jour est à nouveau appelée avec la nouvelle valeur actuelle et l'écriture est réessayée.

Par exemple, dans l'exemple d'application de blog social, vous pouvez autoriser les utilisateurs à suivre et à supprimer des publications et à suivre le nombre d'étoiles qu'une publication a reçue comme suit :

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

L'utilisation d'une transaction évite que le nombre d'étoiles soit incorrect si plusieurs utilisateurs marquent la même publication en même temps ou si le client dispose de données obsolètes. Si la transaction est rejetée, le serveur renvoie la valeur actuelle au client, qui exécute à nouveau la transaction avec la valeur mise à jour. Cela se répète jusqu'à ce que la transaction soit acceptée ou que trop de tentatives aient été effectuées.

Incréments atomiques côté serveur

Dans le cas d'utilisation ci-dessus, nous écrivons deux valeurs dans la base de données : l'ID de l'utilisateur qui marque/désactive la publication et le nombre d'étoiles incrémenté. Si nous savons déjà que cet utilisateur met en vedette la publication, nous pouvons utiliser une opération d'incrémentation atomique au lieu d'une transaction.

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

Ce code n'utilise pas d'opération de transaction, il n'est donc pas automatiquement réexécuté en cas de mise à jour conflictuelle. Cependant, étant donné que l'opération d'incrémentation s'effectue directement sur le serveur de base de données, il n'y a aucun risque de conflit.

Si vous souhaitez détecter et rejeter les conflits spécifiques à une application, tels qu'un utilisateur mettant en vedette une publication qu'il a déjà mise en vedette auparavant, vous devez écrire des règles de sécurité personnalisées pour ce cas d'utilisation.

Travailler avec des données hors ligne

Si un client perd sa connexion réseau, votre application continuera à fonctionner correctement.

Chaque client connecté à une base de données Firebase conserve sa propre version interne de toutes les données sur lesquelles les écouteurs sont utilisés ou qui sont marquées pour être synchronisées avec le serveur. Lorsque des données sont lues ou écrites, cette version locale des données est utilisée en premier. Le client Firebase synchronise ensuite ces données avec les serveurs de base de données distants et avec d'autres clients dans la mesure du possible.

En conséquence, toutes les écritures dans la base de données déclenchent des événements locaux immédiatement, avant toute interaction avec le serveur. Cela signifie que votre application reste réactive quelle que soit la latence ou la connectivité du réseau.

Une fois la connectivité rétablie, votre application reçoit l'ensemble d'événements approprié afin que le client se synchronise avec l'état actuel du serveur, sans avoir à écrire de code personnalisé.

Nous parlerons davantage du comportement hors ligne dans En savoir plus sur les fonctionnalités en ligne et hors ligne .

Prochaines étapes