(Facoltativo) Creare prototipi ed eseguire test con Firebase Emulator Suite
Prima di parlare di come la tua app legge e scrive in Realtime Database, presentiamo un insieme di strumenti che puoi utilizzare per creare prototipi e testare le funzionalità di Realtime Database: Firebase 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, può essere una buona idea poter lavorare localmente senza eseguire il deployment dei servizi live.
Un emulatore di Realtime Database fa parte di 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).emulator_suite_short
L'utilizzo dell'emulatore di Realtime Database prevede solo alcuni passaggi:
- Aggiungere una riga di codice alla configurazione di test dell'app per connettersi all'emulatore.
- Dalla radice della directory del progetto locale, eseguire
firebase emulators:start. - Effettuare chiamate dal codice del prototipo dell'app utilizzando un SDK della piattaforma Realtime Database come di consueto o utilizzando l'API REST di Realtime Database.
È disponibile una procedura dettagliata che coinvolge Realtime Database e Cloud Functions. Consulta anche l'introduzione a Emulator Suite.
Recuperare un DatabaseReference
Per leggere o scrivere dati nel database, devi avere un'istanza di DatabaseReference:
DatabaseReference ref = FirebaseDatabase.instance.ref();
Scrivere dati
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 DatabaseReference e recuperati in attesa o in ascolto degli eventi emessi dal riferimento. Gli eventi vengono emessi una volta per lo stato iniziale dei dati e di nuovo ogni volta che i dati cambiano.
Operazioni di scrittura di base
Per le operazioni di scrittura di base, puoi utilizzare set() per salvare i dati in un riferimento specificato, sostituendo eventuali dati esistenti nel percorso. Puoi impostare un riferimento ai seguenti tipi: String, boolean, int, double, Map, List.
Ad esempio, puoi aggiungere un utente con set() nel seguente modo:
DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
await ref.set({
"name": "John",
"age": 18,
"address": {
"line1": "100 Mountain View"
}
});
L'utilizzo di set() in questo modo sovrascrive i dati nella posizione specificata, inclusi tutti i nodi secondari. Tuttavia, puoi comunque aggiornare un elemento secondario senza riscrivere l'intero oggetto. Se vuoi consentire agli utenti di aggiornare i propri profili, puoi aggiornare il nome utente nel seguente modo:
DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
// Only update the age, leave the name and address!
await ref.update({
"age": 19,
});
Il metodo update() accetta un sotto-percorso ai nodi, consentendoti di aggiornare più nodi nel database contemporaneamente:
DatabaseReference ref = FirebaseDatabase.instance.ref("users");
await ref.update({
"123/age": 19,
"123/address/line1": "1 Mountain View",
});
Leggere dati
Leggere i dati ascoltando gli eventi di valore
Per leggere i dati in un percorso e ascoltare le modifiche, utilizza la proprietà onValue di DatabaseReference per ascoltare i DatabaseEvent.
Puoi utilizzare DatabaseEvent per leggere i dati in un determinato percorso, così come esistono al momento dell'evento. Questo evento viene attivato una volta quando il listener è collegato e di nuovo ogni volta che i dati, inclusi gli elementi secondari, cambiano. L'evento ha una proprietà snapshot che contiene tutti i dati in quella posizione, inclusi i dati secondari. Se non sono presenti dati, la proprietà exists dello snapshot sarà false e la proprietà value sarà null.
L'esempio seguente mostra un'applicazione di social blogging che recupera i dettagli di un post dal database:
DatabaseReference starCountRef =
FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
final data = event.snapshot.value;
updateStarCount(data);
});
Il listener riceve un DataSnapshot che contiene i dati nella posizione specificata nel database al momento dell'evento nella proprietà value.
Leggere i dati una sola volta
Leggere una sola volta utilizzando get()
L'SDK è progettato per gestire le interazioni con i server di database, sia che l'app sia online sia offline.
In genere, per leggere i dati e ricevere una notifica degli aggiornamenti dei dati dal backend, devi utilizzare le tecniche degli eventi di valore descritte sopra. Queste tecniche riducono l'utilizzo e la fatturazione e sono ottimizzate per offrire agli 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'esempio seguente mostra il recupero una sola volta del nome utente pubblico di un utente dal database:
final ref = FirebaseDatabase.instance.ref();
final snapshot = await ref.child('users/$userId').get();
if (snapshot.exists) {
print(snapshot.value);
} else {
print('No data available.');
}
L'utilizzo non necessario di get() può aumentare l'utilizzo della larghezza di banda e causare una perdita di prestazioni, che può essere evitata utilizzando un listener in tempo reale come mostrato sopra.
Leggere i dati una sola volta con once()
In alcuni casi, potresti voler che il valore della cache locale venga restituito immediatamente, anziché verificare la presenza di un valore aggiornato sul server. In questi casi, puoi utilizzare once() per recuperare immediatamente i dati dalla cache del disco locale.
Questa opzione è 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:
final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';
Aggiornare o eliminare i dati
Aggiornare campi specifici
Per scrivere contemporaneamente in elementi secondari specifici di un nodo senza sovrascrivere altri nodi secondari, utilizza il metodo update().
Quando chiami update(), puoi aggiornare i valori secondari di livello inferiore specificando un percorso per la chiave. Se i dati sono archiviati in più posizioni per scalare
meglio, puoi aggiornare tutte le istanze di questi dati utilizzando
il fan-out dei dati. Ad esempio, un'app di social blogging potrebbe voler creare un post e aggiornarlo contemporaneamente al feed delle attività recenti e al feed delle attività dell'utente che ha pubblicato il post. Per farlo, l'applicazione di blogging utilizza un codice simile al seguente:
void writeNewPost(String uid, String username, String picture, String title,
String body) async {
// A post entry.
final postData = {
'author': username,
'uid': uid,
'body': body,
'title': title,
'starCount': 0,
'authorPic': picture,
};
// Get a key for a new Post.
final newPostKey =
FirebaseDatabase.instance.ref().child('posts').push().key;
// Write the new post's data simultaneously in the posts list and the
// user's post list.
final Map<String, Map> updates = {};
updates['/posts/$newPostKey'] = postData;
updates['/user-posts/$uid/$newPostKey'] = postData;
return FirebaseDatabase.instance.ref().update(updates);
}
Questo esempio utilizza push() per creare un post nel nodo contenente i post di tutti gli utenti in /posts/$postid e recuperare contemporaneamente la chiave con key. 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 update(), ad esempio come questo esempio crea il nuovo post in entrambe le posizioni. Gli aggiornamenti simultanei eseguiti in questo modo sono atomici: 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 i dati sono stati sottoposti a commit, puoi registrare i callback di completamento. Sia set() sia update() restituiscono Future a cui puoi collegare callback di successo ed errore che vengono chiamati quando la scrittura è stata sottoposta a commit nel database e quando la chiamata non è andata a buon fine.
FirebaseDatabase.instance
.ref('users/$userId/email')
.set(emailAddress)
.then((_) {
// Data saved successfully!
})
.catchError((error) {
// The write failed...
});
Eliminare dati
Il modo più semplice per eliminare i dati è chiamare remove() su un riferimento alla posizione di questi dati.
Puoi anche eliminare i dati specificando null come valore per un'altra operazione di scrittura, ad esempio set() o update(). Puoi utilizzare questa tecnica con update() per eliminare più elementi secondari in una singola chiamata API.
Salvare i dati come transazioni
Quando lavori con dati che potrebbero essere danneggiati da modifiche simultanee,
ad esempio contatori incrementali, puoi utilizzare una transazione passando un
gestore di transazioni a runTransaction(). Un gestore di transazioni 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, puoi consentire agli utenti di aggiungere e rimuovere le stelle dai post e tenere traccia del numero di stelle ricevute da un post nel seguente modo:
void toggleStar(String uid) async {
DatabaseReference postRef =
FirebaseDatabase.instance.ref("posts/foo-bar-123");
TransactionResult result = await postRef.runTransaction((Object? post) {
// Ensure a post at the ref exists.
if (post == null) {
return Transaction.abort();
}
Map<String, dynamic> _post = Map<String, dynamic>.from(post as Map);
if (_post["stars"] is Map && _post["stars"][uid] != null) {
_post["starCount"] = (_post["starCount"] ?? 1) - 1;
_post["stars"][uid] = null;
} else {
_post["starCount"] = (_post["starCount"] ?? 0) + 1;
if (!_post.containsKey("stars")) {
_post["stars"] = {};
}
_post["stars"][uid] = true;
}
// Return the new data.
return Transaction.success(_post);
});
}
Per impostazione predefinita, gli eventi vengono generati ogni volta che viene eseguita la funzione di aggiornamento della transazione, quindi se esegui la funzione più volte, potresti visualizzare stati intermedi.
Puoi impostare applyLocally su false per eliminare questi stati intermedi e attendere invece il completamento della transazione prima che vengano generati gli eventi:
await ref.runTransaction((Object? post) {
// ...
}, applyLocally: false);
Il risultato di una transazione è un TransactionResult, che contiene informazioni come se la transazione sia stata sottoposta a commit e il nuovo snapshot:
DatabaseReference ref = FirebaseDatabase.instance.ref("posts/123");
TransactionResult result = await ref.runTransaction((Object? post) {
// ...
});
print('Committed? ${result.committed}'); // true / false
print('Snapshot? ${result.snapshot}'); // DataSnapshot
Annullare una transazione
Se vuoi annullare in modo sicuro una transazione, chiama Transaction.abort() per
generare un AbortTransactionException:
TransactionResult result = await ref.runTransaction((Object? user) {
if (user !== null) {
return Transaction.abort();
}
// ...
});
print(result.committed); // false
Incrementi atomici lato server
Nel caso d'uso precedente, stiamo scrivendo 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 sta aggiungendo la stella al post, possiamo utilizzare un'operazione di incremento atomico anziché una transazione.
void addStar(uid, key) async {
Map<String, Object?> updates = {};
updates["posts/$key/stars/$uid"] = true;
updates["posts/$key/starCount"] = ServerValue.increment(1);
updates["user-posts/$key/stars/$uid"] = true;
updates["user-posts/$key/starCount"] = ServerValue.increment(1);
return FirebaseDatabase.instance.ref().update(updates);
}
Questo codice non utilizza un'operazione di transazione, quindi non viene eseguito automaticamente di nuovo se è presente un aggiornamento in conflitto. Tuttavia, poiché l'operazione di incremento avviene 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 in precedenza, devi scrivere regole di sicurezza personalizzate per questo caso d'uso.
Utilizzare i dati offline
Se un client perde la connessione di rete, l'app continuerà a funzionare correttamente.
Ogni client connesso a un database Firebase mantiene la propria versione interna di tutti i dati attivi. Quando i dati vengono scritti, vengono scritti prima in questa versione locale. 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 che i dati vengano scritti nel server. Ciò significa che l'app rimane reattiva indipendentemente dalla latenza o dalla connettività di rete.
Una volta ristabilita la connettività, l'app riceve l'insieme appropriato di eventi in modo che il client si sincronizzi con lo stato attuale del server, senza dover scrivere codice personalizzato.
Parleremo di più sul comportamento offline in Scopri di più sulle funzionalità online e offline.