Leggere e scrivere dati su Android

Questo documento copre le nozioni di base sulla lettura e scrittura dei dati Firebase.

I dati Firebase vengono scritti su un riferimento FirebaseDatabase e recuperati collegando un listener asincrono al riferimento. Il listener viene attivato una volta per lo stato iniziale dei dati e nuovamente ogni volta che i dati cambiano.

(Facoltativo) Prototipa e testa con Firebase Local Emulator Suite

Prima di parlare di come la tua app legge e scrive su Realtime Database, introduciamo una serie di strumenti che puoi utilizzare per prototipare e testare la funzionalità di Realtime Database: Firebase Local Emulator Suite. Se stai provando diversi modelli di dati, ottimizzando le regole di sicurezza o lavorando per trovare il modo più conveniente per interagire con il back-end, poter lavorare localmente senza distribuire servizi live può essere un'ottima idea.

Un emulatore di Realtime Database fa parte di Local Emulator Suite, che consente alla tua app di interagire con il contenuto e la configurazione del database emulato, nonché facoltativamente con le risorse del progetto emulato (funzioni, altri database e regole di sicurezza).

L'utilizzo dell'emulatore Realtime Database prevede solo pochi passaggi:

  1. Aggiunta di una riga di codice alla configurazione di test della tua app per connettersi all'emulatore.
  2. Dalla radice della directory del progetto locale, eseguendo firebase emulators:start .
  3. Effettuare chiamate dal codice prototipo della tua app utilizzando l'SDK della piattaforma Realtime Database come di consueto o utilizzando l'API REST di Realtime Database.

È disponibile una procedura dettagliata dettagliata che coinvolge Realtime Database e Cloud Functions . Dovresti anche dare un'occhiata all'introduzione della Local Emulator Suite .

Ottieni un riferimento al database

Per leggere o scrivere dati dal database, è necessaria un'istanza di DatabaseReference :

Kotlin+KTX

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

Java

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

Scrivi dati

Operazioni di scrittura di base

Per le operazioni di scrittura di base, è possibile utilizzare setValue() per salvare i dati in un riferimento specificato, sostituendo eventuali dati esistenti in quel percorso. Puoi utilizzare questo metodo per:

  • Tipi di passaggio che corrispondono ai tipi JSON disponibili come segue:
    • String
    • Long
    • Double
    • Boolean
    • Map<String, Object>
    • List<Object>
  • Passa un oggetto Java personalizzato, se la classe che lo definisce ha un costruttore predefinito che non accetta argomenti e dispone di getter pubblici per le proprietà da assegnare.

Se utilizzi un oggetto Java, il contenuto dell'oggetto viene automaticamente mappato alle posizioni secondarie in modo nidificato. L'utilizzo di un oggetto Java in genere rende inoltre il codice più leggibile e più semplice da mantenere. Ad esempio, se hai un'app con un profilo utente di base, il tuo oggetto User potrebbe apparire come segue:

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

}

Puoi aggiungere un utente con setValue() come segue:

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'utilizzo setValue() in questo modo sovrascrive i dati nella posizione specificata, inclusi eventuali nodi figlio. Tuttavia, puoi comunque aggiornare un figlio senza riscrivere l'intero oggetto. Se desideri consentire agli utenti di aggiornare i propri profili, puoi aggiornare il nome utente come segue:

Kotlin+KTX

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

Java

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

Leggi i dati

Leggi i dati con ascoltatori persistenti

Per leggere i dati in un percorso e ascoltare le modifiche, utilizzare il metodo addValueEventListener() per aggiungere un ValueEventListener a un DatabaseReference .

Ascoltatore Richiamata dell'evento Utilizzo tipico
ValueEventListener onDataChange() Leggere e ascoltare le modifiche all'intero contenuto di un percorso.

È possibile utilizzare il metodo onDataChange() per leggere un'istantanea statica dei contenuti in un determinato percorso, così come esistevano al momento dell'evento. Questo metodo viene attivato una volta quando l'ascoltatore è collegato e di nuovo ogni volta che i dati, inclusi i figli, cambiano. Al callback dell'evento viene passata uno snapshot contenente tutti i dati in quella posizione, inclusi i dati figlio. Se non sono presenti dati, l'istantanea restituirà false quando chiami exists() e null quando chiami getValue() su di esso.

L'esempio seguente mostra un'applicazione di social blogging che recupera i dettagli di un post dal database:

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

Il listener riceve un DataSnapshot che contiene i dati nella posizione specificata nel database al momento dell'evento. La chiamata getValue() su uno snapshot restituisce la rappresentazione dell'oggetto Java dei dati. Se non esistono dati nella posizione, la chiamata a getValue() restituisce null .

In questo esempio, ValueEventListener definisce anche il metodo onCancelled() che viene chiamato se la lettura viene annullata. Ad esempio, una lettura può essere annullata se il client non dispone dell'autorizzazione per leggere da una posizione del database Firebase. A questo metodo viene passato un oggetto DatabaseError che indica il motivo per cui si è verificato l'errore.

Leggere i dati una volta

Leggi una volta usando get()

L'SDK è progettato per gestire le interazioni con i server di database sia che la tua app sia online che offline.

In genere, dovresti utilizzare le tecniche ValueEventListener descritte sopra per leggere i dati e ricevere notifiche sugli aggiornamenti dei dati dal backend. Le tecniche di ascolto riducono l'utilizzo e la fatturazione e sono ottimizzate per offrire ai tuoi utenti la migliore esperienza mentre vanno online e offline.

Se hai bisogno dei dati solo una volta, puoi utilizzare get() per ottenere un'istantanea dei dati dal database. Se per qualsiasi motivo get() non è in grado di restituire il valore del server, il client esaminerà la cache di archiviazione locale e restituirà un errore se il valore non viene ancora trovato.

L'uso non necessario di get() può aumentare l'uso della larghezza di banda e portare a una perdita di prestazioni, che può essere prevenuta utilizzando un ascoltatore in tempo reale come mostrato sopra.

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

Leggi una volta utilizzando un ascoltatore

In alcuni casi potresti voler restituire immediatamente il valore dalla cache locale, invece di verificare la presenza di un valore aggiornato sul server. In questi casi è possibile utilizzare addListenerForSingleValueEvent per ottenere immediatamente i dati dalla cache del disco locale.

Ciò è utile per i dati che devono essere caricati solo una volta e non si prevede che cambino frequentemente o richiedano un ascolto attivo. Ad esempio, l'app di blog negli esempi precedenti utilizza questo metodo per caricare il profilo di un utente quando inizia a creare un nuovo post.

Aggiornamento o cancellazione dei dati

Aggiorna campi specifici

Per scrivere simultaneamente su figli specifici di un nodo senza sovrascrivere altri nodi figli, utilizzare il metodo updateChildren() .

Quando chiami updateChildren() , puoi aggiornare i valori secondari di livello inferiore specificando un percorso per la chiave. Se i dati vengono archiviati in più posizioni per una migliore scalabilità, puoi aggiornare tutte le istanze di tali dati utilizzando il fan-out dei dati . Ad esempio, un'app di social blogging potrebbe avere una classe Post come questa:

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

Per creare un post e aggiornarlo contemporaneamente al feed delle attività recenti e al feed delle attività dell'utente che ha pubblicato il post, l'applicazione di blogging utilizza un codice come questo:

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

Questo esempio utilizza push() per creare un post nel nodo contenente post per tutti gli utenti su /posts/$postid e contemporaneamente recuperare la chiave con getKey() . La chiave può quindi essere utilizzata per creare una seconda voce nei post dell'utente in /user-posts/$userid/$postid .

Utilizzando questi percorsi, puoi eseguire aggiornamenti simultanei in più posizioni nell'albero JSON con una singola chiamata a updateChildren() , come in questo esempio crea il nuovo post in entrambe le posizioni. Gli aggiornamenti simultanei effettuati in questo modo sono atomici: tutti gli aggiornamenti hanno esito positivo oppure tutti gli aggiornamenti falliscono.

Aggiungi una richiamata di completamento

Se vuoi sapere quando i tuoi dati sono stati impegnati, puoi aggiungere un ascoltatore di completamento. Sia setValue() che updateChildren() accettano un ascoltatore di completamento opzionale che viene chiamato quando la scrittura è stata confermata con successo nel database. Se la chiamata non ha avuto successo, al listener viene passato un oggetto errore che indica il motivo dell'errore.

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

Elimina dati

Il modo più semplice per eliminare i dati è chiamare removeValue() su un riferimento alla posizione di tali dati.

Puoi anche eliminare specificando null come valore per un'altra operazione di scrittura come setValue() o updateChildren() . Puoi utilizzare questa tecnica con updateChildren() per eliminare più elementi secondari in una singola chiamata API.

Distacca gli ascoltatori

I callback vengono rimossi chiamando il removeEventListener() sul riferimento al database Firebase.

Se un ascoltatore è stato aggiunto più volte a una posizione dati, viene chiamato più volte per ciascun evento ed è necessario scollegarlo lo stesso numero di volte per rimuoverlo completamente.

La chiamata removeEventListener() su un ascoltatore genitore non rimuove automaticamente gli ascoltatori registrati sui suoi nodi figli; removeEventListener() deve essere chiamato anche su qualsiasi ascoltatore figlio per rimuovere la richiamata.

Salva i dati come transazioni

Quando si lavora con dati che potrebbero essere danneggiati da modifiche simultanee, come i contatori incrementali, è possibile utilizzare un'operazione di transazione . Assegna a questa operazione due argomenti: una funzione di aggiornamento e un callback di completamento opzionale. La funzione di aggiornamento prende lo stato corrente dei dati come argomento e restituisce il nuovo stato desiderato che desideri scrivere. Se un altro client scrive nella posizione prima che il nuovo valore venga scritto correttamente, la funzione di aggiornamento viene richiamata nuovamente con il nuovo valore corrente e la scrittura viene ritentata.

Ad esempio, nell'app di social blogging di esempio, potresti consentire agli utenti di aggiungere o rimuovere post da Speciali e tenere traccia di quante stelle ha ricevuto un post come segue:

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'utilizzo di una transazione impedisce che il conteggio delle stelle sia errato se più utenti contrassegnano contemporaneamente lo stesso post come Speciali o se i dati del client non sono aggiornati. Se la transazione viene rifiutata, il server restituisce il valore corrente al client, che esegue nuovamente la transazione con il valore aggiornato. Ciò si ripete finché la transazione non viene accettata o vengono effettuati troppi tentativi.

Incrementi atomici lato server

Nel caso d'uso precedente stiamo scrivendo due valori nel database: l'ID dell'utente che contrassegna/rimuove il post da Speciali e il numero di stelle incrementato. Se sappiamo già che l'utente è protagonista del post, possiamo utilizzare un'operazione di incremento atomico invece di una transazione.

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

Questo codice non utilizza un'operazione di transazione, quindi non viene rieseguito automaticamente in caso di aggiornamento in conflitto. Tuttavia, poiché l'operazione di incremento avviene direttamente sul server del database, non vi è alcuna possibilità che si verifichi un conflitto.

Se desideri rilevare e rifiutare conflitti specifici dell'applicazione, ad esempio un utente che contrassegna come Speciale un post che aveva già contrassegnato come Speciale in precedenza, devi scrivere regole di sicurezza personalizzate per quel caso d'uso.

Lavora con i dati offline

Se un client perde la connessione di rete, la tua app continuerà a funzionare correttamente.

Ogni client connesso a un database Firebase mantiene la propria versione interna di tutti i dati su cui vengono utilizzati gli ascoltatori o che sono contrassegnati per essere mantenuti sincronizzati con il server. Quando i dati vengono letti o scritti, viene utilizzata per prima questa versione locale dei dati. Il client Firebase sincronizza quindi tali dati con i server di database remoti e con altri client in base al "massimo sforzo".

Di conseguenza, tutte le scritture sul database attivano immediatamente eventi locali, prima di qualsiasi interazione con il server. Ciò significa che la tua app rimane reattiva indipendentemente dalla latenza di rete o dalla connettività.

Una volta ristabilita la connettività, la tua app riceve il set di eventi appropriato in modo che il client si sincronizzi con lo stato corrente del server, senza dover scrivere alcun codice personalizzato.

Parleremo più approfonditamente del comportamento offline in Ulteriori informazioni sulle funzionalità online e offline .

Prossimi passi