Lettura e scrittura di dati

(Facoltativo) Prototipazione e 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 un prototipo e testare la funzionalità di Realtime Database: Firebase Emulator Suite. Se stai provando diversi modelli di dati, ottimizzando le regole di sicurezza o cercando di trovare il modo più economico per interagire con il back-end, la possibilità di lavorare localmente senza implementare servizi in produzione può essere un'ottima idea.

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

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

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

È disponibile una procedura dettagliata che coinvolge Realtime Database e Cloud Functions. Consulta anche l'introduzione a Emulator Suite.

Ottieni un riferimento Database

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

DatabaseReference ref = FirebaseDatabase.instance.ref();

Scrivi dati

Questo documento illustra le nozioni di base per leggere e scrivere i dati di Firebase.

I dati di Firebase vengono scritti in un DatabaseReference e recuperati rimanendo in attesa o ascoltando gli 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 sostituendo eventuali dati esistenti in quel 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 eventuali nodi secondari. Tuttavia, puoi comunque aggiornare un bambino senza e riscrivere l'intero oggetto. Se vuoi consentire agli utenti di aggiornare i propri profili, puoi aggiornare il nome utente come segue:

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 percorso secondario ai nodi, consentendoti di aggiornare più nodi sul database contemporaneamente:

DatabaseReference ref = FirebaseDatabase.instance.ref("users");

await ref.update({
  "123/age": 19,
  "123/address/line1": "1 Mountain View",
});

Lettura di dati

Leggere i dati ascoltando gli eventi relativi ai valori

Per leggere i dati in un percorso e rilevare le modifiche, utilizza la proprietà onValue di DatabaseReference per rilevare i DatabaseEvent.

Puoi utilizzare DatabaseEvent per leggere i dati in un determinato percorso, come esistente al momento dell'evento. Questo evento viene attivato una volta quando viene collegato e ogni volta che i dati, inclusi eventuali elementi figlio, modifiche. L'evento ha una proprietà snapshot contenente tutti i dati in quella posizione, inclusi i dati secondari. Se non esistono dati, lo snapshot La proprietà exists sarà false e la relativa proprietà value sarà nulla.

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

L'ascoltatore riceve un DataSnapshot che contiene i dati nella posizione specificata nel database al momento dell'evento nella sua proprietà value.

I dati vengono letti una volta

Leggi una 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, è consigliabile utilizzare le tecniche degli eventi di valore descritte in precedenza per per ricevere notifiche sugli aggiornamenti dei dati dal backend. Queste tecniche ridurre l'utilizzo e la fatturazione e sono ottimizzati per offrire esperienza 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 esaminerà la cache dello spazio di archiviazione locale e restituirà un errore se il valore non viene ancora trovato.

L'esempio seguente mostra il recupero del nome utente visibile al pubblico di un utente una sola volta 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'uso non necessario di get() può aumentare l'utilizzo della larghezza di banda e comportare una perdita di rendimento, che può essere evitata utilizzando un ascoltatore in tempo reale come mostrato sopra.

Leggere i dati una volta con once()

In alcuni casi potresti voler restituire il valore della cache locale immediatamente, invece di cercare un valore aggiornato sul server. In questi casi, puoi utilizzare once() per recuperare immediatamente i dati dalla cache del disco locale.

È utile per i dati che devono essere caricati una sola volta e non è previsto che cambiano frequentemente o richiedono un ascolto attivo. Ad esempio, l'app di blogging indicata 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';

Aggiornamento o eliminazione di dati

Aggiornare campi specifici

Per scrivere contemporaneamente in nodi 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ù località per scalare puoi aggiornare tutte le istanze dei dati utilizzando fan-out dei dati. Ad esempio, un'app di blogging social potrebbe voler creare un post e aggiornarlo contemporaneamente nel feed delle attività recenti e nel feed delle attività dell'utente che ha pubblicato il post. A questo scopo, l'applicazione di blogging utilizza un codice come questo:

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 post per tutti gli utenti in /posts/$postid e recuperare contemporaneamente la chiave con key. La chiave può essere utilizzata per creare una seconda voce nei post dell'utente su /user-posts/$userid/$postid.

Utilizzando questi percorsi, puoi eseguire aggiornamenti simultanei a più località in la struttura JSON con una singola chiamata a update(), come in 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 non vanno a buon fine.

Aggiungere un callback di completamento

Se vuoi sapere quando è stato eseguito il commit dei tuoi dati, puoi registrarti di completamento. Sia set() che update() restituiscono Future, a cui puoi collegare i callback di successo ed errore che vengono chiamati quando l'istanza 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...
    });

Elimina dati

Il modo più semplice per eliminare i dati è chiamare remove() su un riferimento alla loro posizione.

Puoi anche eliminare 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, come i contatori incrementali, puoi utilizzare una transazione passando un gestore delle transazioni a runTransaction(). Un gestore delle transazioni prende come argomento lo stato corrente dei dati 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 richiamata di nuovo con il nuovo valore corrente e la scrittura viene riprovata.

Ad esempio, nell'esempio di app di blogging social, potresti consentire agli utenti di assegnare e togliere stelle ai post e tenere traccia del numero di stelle ricevute da un post come segue:

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 delle transazioni, quindi se la esegui più volte, potresti visualizzare stati intermedi. Puoi impostare applyLocally su false per sopprimere questi stati intermedi e attendono il completamento della transazione prima che vengano generati eventi:

await ref.runTransaction((Object? post) {
  // ...
}, applyLocally: false);

Il risultato di una transazione è un TransactionResult, che contiene informazioni come l'eventuale commit della transazione 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

Annullamento di una transazione

Se vuoi annullare in sicurezza 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 riportato sopra, scriviamo due valori nel database: l'ID dell'utente che aggiunge/rimuove una stella al post e il conteggio delle stelle incrementato. Se sappiamo già che l'utente aggiunge il post ai preferiti, 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, pertanto non viene eseguito nuovamente automaticamente 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 un post a Speciali dopo averlo già aggiunto in precedenza, devi scrivere regole di sicurezza personalizzate per questo caso d'uso.

Utilizza i dati offline

Se un client perde la connessione di rete, la tua app continua a funzionare in modo corretto.

Ogni client connesso a un database Firebase mantiene la propria versione interna dei dati attivi. Quando i dati vengono scritti, vengono scritti in questa versione locale per prima cosa. Il client Firebase sincronizza quindi i dati con i server database remoto e con altri client in base al criterio "best effort".

Di conseguenza, tutte le scritture nel database attivano immediatamente gli eventi locali, prima che qualsiasi dato venga scritto sul server. Ciò significa che la tua app rimane reattivo, indipendentemente dalla latenza o dalla connettività di rete.

Una volta ristabilita la connettività, la tua app riceve l'insieme appropriato di in modo che il client si sincronizzi con lo stato attuale del server, senza dover scrivere codice personalizzato.

Approfondiremo il comportamento offline nel Scopri di più sulle funzionalità online e offline.

Passaggi successivi