Questo documento illustra le nozioni di base per la lettura e la scrittura dei dati di Firebase.
I dati di Firebase vengono scritti in un riferimento FirebaseDatabase
e recuperati collegando un listener asincrono al riferimento. Il listener viene attivato
una volta per lo stato iniziale dei dati e di nuovo ogni volta che i dati cambiano.
(Facoltativo) Prototipo e test con Firebase Local Emulator Suite
Prima di parlare di come la tua app legge e scrive su Realtime Database, introduciamo un insieme di strumenti che puoi utilizzare per prototipare e testare la funzionalità Realtime Database: Firebase Local Emulator Suite. Se stai provando diversi modelli di dati, ottimizzando le regole di sicurezza o cercando il modo più conveniente per interagire con il backend, poter lavorare in locale senza implementare servizi live può essere un'ottima idea.
Un emulatore Realtime Database fa parte di Local Emulator Suite, che consente alla tua app di interagire con i contenuti 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 pochi passaggi:
- Aggiungendo una riga di codice alla configurazione di test dell'app per connettersi all'emulatore.
- Dalla radice della directory del progetto locale, esegui
firebase emulators:start
. - Effettuare chiamate dal codice prototipo della tua app utilizzando un SDK della piattaforma Realtime Database come di consueto o utilizzando l'API REST Realtime Database.
È disponibile una procedura dettagliata che coinvolge Realtime Database e Cloud Functions. Dai un'occhiata anche all'introduzione di Local Emulator Suite.
Recuperare un DatabaseReference
Per leggere o scrivere dati dal database, devi disporre di un'istanza di DatabaseReference
:
Kotlin
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, puoi utilizzare setValue()
per salvare i dati in un riferimento specificato, sostituendo tutti i dati esistenti nel percorso. Puoi utilizzare questo metodo per:
- Tipi di tessere 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 ha getter pubblici per le proprietà da assegnare.
Se utilizzi un oggetto Java, i contenuti dell'oggetto vengono mappati automaticamente
alle posizioni secondarie in modo nidificato. L'utilizzo di un oggetto Java in genere rende
il codice più leggibile e più facile da gestire. Ad esempio, se hai un'app con un profilo utente di base, l'oggetto User
potrebbe avere il seguente aspetto:
Kotlin
@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()
nel seguente modo:
Kotlin
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 di setValue()
in questo modo sovrascrive i dati nella posizione specificata, inclusi tutti i nodi secondari. Tuttavia, puoi comunque aggiornare un figlio senza
riscrivere l'intero oggetto. Se vuoi consentire agli utenti di aggiornare i propri profili,
puoi aggiornare il nome utente nel seguente modo:
Kotlin
database.child("users").child(userId).child("username").setValue(name)
Java
mDatabase.child("users").child(userId).child("username").setValue(name);
Lettura di dati
Lettura di dati con listener permanenti
Per leggere i dati in un percorso e rilevare le modifiche, utilizza il metodo addValueEventListener()
per aggiungere un ValueEventListener
a un DatabaseReference
.
modulo | Callback evento | Utilizzo tipico |
---|---|---|
ValueEventListener |
onDataChange() |
Leggi e ascolta le modifiche all'intero contenuto di un percorso. |
Puoi 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 il listener è collegato e di nuovo ogni volta che i dati,
inclusi gli elementi secondari, cambiano. Alla callback dell'evento viene passato uno snapshot contenente
tutti i dati in quella posizione, inclusi i dati secondari. Se non sono presenti dati, lo
snapshot restituirà false
quando chiami exists()
e null
quando chiami
getValue()
.
L'esempio seguente mostra un'applicazione di blogging sociale che recupera i dettagli di un post dal database:
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>() // ... } 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'ascoltatore riceve un DataSnapshot
che contiene i dati nella posizione specificata
nel database al momento dell'evento. La chiamata di getValue()
su uno snapshot restituisce la rappresentazione dell'oggetto Java dei dati. Se non esistono dati
nella posizione, la chiamata di 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 dell'errore.
Lettura dei dati una sola volta
Lettura una sola volta utilizzando get()
L'SDK è progettato per gestire le interazioni con i server di database indipendentemente dal fatto che la tua app sia online o offline.
In genere, devi utilizzare le tecniche ValueEventListener
descritte sopra
per leggere i dati e ricevere una notifica degli 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 quando vanno online e offline.
Se hai bisogno dei dati una sola volta, puoi utilizzare get()
per ottenere uno snapshot dei dati dal database. Se per qualsiasi motivo get()
non è in grado di restituire il valore del server, il client eseguirà il probing della cache di archiviazione locale e restituirà un errore se il valore non viene ancora trovato.
L'utilizzo non necessario di get()
può aumentare l'utilizzo della larghezza di banda e causare una perdita di
rendimento, che può essere evitata utilizzando un listener in tempo reale come mostrato sopra.
Kotlin
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()));
}
}
});
Lettura una sola volta utilizzando un listener
In alcuni casi, potresti voler che il valore della cache locale venga restituito
immediatamente, anziché controllare se è presente un valore aggiornato sul server. In questi
casi puoi utilizzare addListenerForSingleValueEvent
per ottenere immediatamente i dati dalla
cache del disco locale.
Questo è utile per i dati che devono essere caricati una sola volta e non dovrebbero cambiare frequentemente o richiedere un ascolto attivo. Ad esempio, l'app di blogging negli esempi precedenti utilizza questo metodo per caricare il profilo di un utente quando inizia a scrivere un nuovo post.
Aggiornamento o eliminazione dei dati
Aggiornare campi specifici
Per scrivere contemporaneamente a nodi secondari specifici di un nodo senza sovrascrivere altri
nodi secondari, utilizza 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 migliorare la scalabilità, puoi aggiornare tutte le istanze di questi dati utilizzando la distribuzione dei dati. Ad esempio, un'app
di social blogging potrebbe avere una classe Post
come questa:
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, ) } }
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 nel feed delle attività recenti e nel feed delle attività dell'utente che ha pubblicato il post, l'applicazione di blogging utilizza un codice simile a questo:
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 = 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 i post per
tutti gli utenti all'indirizzo /posts/$postid
e recuperare contemporaneamente la chiave con
getKey()
. La chiave può essere utilizzata per creare una seconda voce nei post dell'utente all'indirizzo /user-posts/$userid/$postid
.
Utilizzando questi percorsi, puoi eseguire aggiornamenti simultanei a più posizioni nell'albero JSON con una singola chiamata a updateChildren()
, ad esempio come questo esempio crea il nuovo post in entrambe le posizioni. Gli aggiornamenti simultanei eseguiti in questo modo
sono atomici: o tutti gli aggiornamenti vanno a buon fine o tutti gli aggiornamenti non vanno a buon fine.
Aggiungere un callback di completamento
Se vuoi sapere quando sono stati salvati i dati, puoi aggiungere un listener di completamento. Sia setValue()
sia updateChildren()
accettano un listener di completamento facoltativo che viene chiamato quando la scrittura è stata eseguita correttamente nel database. Se la chiamata non è andata a buon fine, al listener viene passato un oggetto di errore che indica il motivo del problema.
Kotlin
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 questi 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ù figli in una singola chiamata API.
Stacca i listener
I callback vengono rimossi chiamando il metodo removeEventListener()
sul riferimento al database Firebase.
Se un listener è stato aggiunto più volte a una posizione dei dati, viene chiamato più volte per ogni evento e devi scollegarlo lo stesso numero di volte per rimuoverlo completamente.
La chiamata di removeEventListener()
su un listener genitore non
rimuove automaticamente i listener registrati nei relativi nodi secondari;
removeEventListener()
deve essere chiamato anche su tutti i listener secondari
per rimuovere il callback.
Salvare i dati come transazioni
Quando lavori con dati che potrebbero essere danneggiati da modifiche simultanee, come i contatori incrementali, puoi utilizzare un'operazione di transazione. Fornisci a questa operazione due argomenti: una funzione di aggiornamento e un callback di completamento facoltativo. La funzione di aggiornamento accetta lo stato attuale dei dati come argomento e restituisce il nuovo stato desiderato che vuoi scrivere. Se un altro client scrive nella posizione prima che il nuovo valore venga scritto correttamente, la funzione di aggiornamento viene chiamata di nuovo 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 e rimuovere le stelle dai post e tenere traccia di quante stelle ha ricevuto un post come segue:
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?, 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 aggiungono una stella allo stesso post contemporaneamente o se il client disponeva di dati obsoleti. Se la transazione viene rifiutata, il server restituisce il valore corrente al client, che esegue nuovamente la transazione con il valore aggiornato. Questa operazione si ripete finché la transazione non viene accettata o non vengono effettuati troppi tentativi.
Incrementi lato server atomici
Nello scenario d'uso riportato sopra, scriviamo due valori nel database: l'ID dell'utente che aggiunge/rimuove la stella dal post e il conteggio delle stelle incrementato. Se sappiamo già che l'utente ha aggiunto il post ai preferiti, possiamo utilizzare un'operazione di incremento atomico anziché una transazione.
Kotlin
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, pertanto non viene eseguito nuovamente in caso di aggiornamento in conflitto. Tuttavia, poiché l'operazione di incremento viene eseguita direttamente sul server di database, non esiste la possibilità di un conflitto.
Se vuoi rilevare e rifiutare conflitti specifici dell'applicazione, ad esempio un utente che aggiunge una stella a un post a cui l'aveva già aggiunta, devi scrivere regole di sicurezza personalizzate per questo caso d'uso.
Lavorare 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 i listener o che sono contrassegnati per essere mantenuti sincronizzati con il server. Quando i dati vengono letti o scritti, viene utilizzata prima questa versione locale dei dati. Il client Firebase sincronizza quindi i dati con i server di database remoti e con altri client in base al principio del "best effort".
Di conseguenza, tutte le scritture nel database attivano immediatamente gli 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 ripristinata la connettività, la tua app riceve il set appropriato di eventi in modo che il client si sincronizzi con lo stato attuale del server, senza dover scrivere codice personalizzato.
Parleremo più nel dettaglio del comportamento offline in Scopri di più sulle funzionalità online e offline.