Leggere e scrivere dati

(Facoltativo) Prototipa e testa con Firebase 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 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 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).emulator_suite_short

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 di Emulator Suite .

Ottieni un riferimento al database

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

DatabaseReference ref = FirebaseDatabase.instance.ref();

Scrivi dati

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

I dati Firebase vengono scritti su un DatabaseReference e recuperati attendendo 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 riferimento specificato, sostituendo tutti i dati esistenti in quel percorso. È possibile impostare un riferimento ai seguenti tipi: String , boolean , int , double , Map , List .

Ad esempio, puoi aggiungere un utente con set() come segue:

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

await ref.set({
  "name": "John",
  "age": 18,
  "address": {
    "line1": "100 Mountain View"
  }
});

L'utilizzo set() 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:

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

// Only update the name, leave the age and address!
await ref.update({
  "age": 19,
});

Il metodo update() accetta un sottopercorso ai nodi, consentendo 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",
});

Leggi i dati

Leggere i dati ascoltando gli eventi valore

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

È possibile utilizzare DatabaseEvent per leggere i dati in un determinato percorso, così come esiste al momento dell'evento. Questo evento viene attivato una volta quando il listener è collegato e di nuovo ogni volta che i dati, inclusi eventuali figli, cambiano. L'evento ha una proprietà snapshot contenente tutti i dati in quella posizione, inclusi i dati figlio. Se non sono presenti dati, la proprietà exists dello snapshot sarà false e la relativa 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 sua proprietà value .

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 degli eventi valore descritte sopra per leggere i dati e ricevere notifiche sugli aggiornamenti dei dati dal backend. Queste tecniche 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'esempio seguente dimostra come recuperare il nome utente 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'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.

Leggi i dati una volta con once()

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 once() 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 blogging negli esempi precedenti utilizza questo metodo per caricare il profilo di un utente quando inizia a creare un nuovo post:

final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';

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 update() .

Quando chiami update() , 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 voler creare un post e contemporaneamente aggiornarlo al feed delle attività recenti e al feed delle attività dell'utente che ha pubblicato il post. Per fare ciò, l'applicazione di blog 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 su /posts/$postid e contemporaneamente recuperare 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() , 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 un callback di completamento

Se vuoi sapere quando i tuoi dati sono stati impegnati, puoi registrare i callback di completamento. Sia set() che update() restituiscono Future , a cui è possibile allegare callback di successo ed errore che vengono chiamati quando la scrittura è stata confermata nel database e quando la chiamata non ha avuto successo.

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 posizione di tali dati.

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

Salva i dati come transazioni

Quando si lavora con dati che potrebbero essere danneggiati da modifiche simultanee, come i contatori incrementali, è possibile utilizzare una transazione passando un gestore di transazioni a runTransaction() . Un gestore di transazioni accetta 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:

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, pertanto se si esegue la funzione più volte è possibile che vengano visualizzati 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 è TransactionResult , che contiene informazioni come se la transazione è stata confermata e la nuova istantanea:

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 desideri annullare una transazione in modo sicuro, chiama Transaction.abort() per lanciare 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 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.

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 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 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 "massimo sforzo".

Di conseguenza, tutte le scritture nel database attivano immediatamente eventi locali, prima che i dati vengano scritti sul 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