Leggere e scrivere dati su Android

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

I dati di Firebase vengono scritti in un riferimento FirebaseDatabase e recuperati collegando un ascoltatore asincrono al riferimento. L'ascoltatore viene attivato una volta per lo stato iniziale dei dati e di nuovo ogni volta che i dati cambiano.

(Facoltativo) Crea un prototipo e testa con Firebase Local Emulator Suite

Prima di parlare di come la tua app legge e scrive in Realtime Database, introduciamo un insieme di strumenti che puoi utilizzare per realizzare prototipi e testare la funzionalità di Realtime Database: Firebase Local 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 Realtime Database fa parte di Local Emulator Suite, che consente all'app di interagire con i contenuti e la configurazione del database emulati, nonché, facoltativamente, con le risorse del progetto emulate (funzioni, altri database e regole di sicurezza).

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. Esegui chiamate dal codice del prototipo dell'app utilizzando un SDK della piattaforma Realtime Database come di consueto o l'API REST Realtime Database.

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

Recuperare un DatabaseReference

Per leggere o scrivere dati dal database, devi avere 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, puoi utilizzare setValue() per salvare i dati in un riferimento specificato, sostituendo eventuali dati esistenti in quel percorso. Puoi utilizzare questo metodo per:

  • Passa i tipi 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 rende inoltre 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+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 di setValue() in questo modo sovrascrive i dati nella posizione specificata, inclusi eventuali 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 come segue:

Kotlin+KTX

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

Java

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

Lettura di dati

Leggere i dati con gli ascoltatori 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() Leggere e ascoltare le modifiche a tutti i contenuti 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 l'ascoltatore è collegato e di nuovo ogni volta che i dati, inclusi i figli, cambiano. Al 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 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);

L'ascoltatore riceve un DataSnapshot contenente i dati nella posizione specificata nel database al momento dell'evento. La chiamata a 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 ha l'autorizzazione di lettura da una posizione del database Firebase. A questo metodo viene passato un oggetto DatabaseError che indica il motivo dell'errore.

Leggere i dati 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, devi utilizzare le tecniche ValueEventListener descritte sopra per leggere i dati e ricevere notifiche degli aggiornamenti dei dati dal backend. Le tecniche di ascolto riducono l'utilizzo e la fatturazione e sono ottimizzate per offrire agli utenti la migliore esperienza quando sono 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'uso non necessario di get() può aumentare l'utilizzo della larghezza di banda e comportare una perdita di prestazioni, che può essere evitata 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()));
        }
    }
});

Lettura una volta utilizzando un ascoltatore

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 recuperare immediatamente i dati dalla cache locale del disco.

Questa opzione è utile per i dati che devono essere caricati una sola volta e che non dovrebbero cambiare spesso o richiedere l'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.

Aggiornamento o eliminazione dei dati

Aggiornare campi specifici

Per scrivere contemporaneamente in 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 una scalabilità migliore, puoi aggiornare tutte le istanze utilizzando la distribuzione dei dati. Ad esempio, un'app di blogging social 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 nel feed delle attività recenti e nel feed delle attività dell'utente che pubblica, 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 i post per tutti gli utenti in /posts/$postid e contemporaneamente recuperare la chiave con getKey(). 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 in più posizioni nella struttura ad albero JSON con una singola chiamata a updateChildren(), ad esempio come in questo esempio che 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 i dati sono stati sottoposti a commit, puoi aggiungere un ascoltatore di completamento. Sia setValue() sia updateChildren() accettano un optional completion listener che viene chiamato quando la scrittura è stata eseguita correttamente nel database. Se la chiamata non è andata a buon fine, all'ascoltatore 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 loro posizione.

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

Scollegare gli ascoltatori

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

Se un ascoltatore è 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 a removeEventListener() su un ascoltatore principale nonrimuove automaticamente gli ascoltatori registrati sui relativi nodi secondari.removeEventListener() deve essere chiamato anche su tutti gli ascoltatori secondari per rimuovere il callback.

Salvare i dati come transazioni

Quando lavori con dati che potrebbero essere danneggiati da modifiche contemporaneamente, ad esempio contatori incrementali, puoi utilizzare un'operazione di transazione. A questa operazione vengono assegnati due argomenti: una funzione di aggiornamento e una funzione di richiamata facoltativa al completamento. La funzione di aggiornamento prende 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 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:

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 i conteggi delle stelle siano errati se più utenti assegnano una stella allo stesso post contemporaneamente o se il cliente ha dati non aggiornati. Se la transazione viene rifiutata, il server restituisce il valore corrente al client, che esegue di nuovo la transazione con il valore aggiornato. L'operazione viene ripetuta fino a quando la transazione non viene accettata o non sono stati effettuati troppi tentativi.

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.

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, 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.

Lavorare con 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 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 prima questa versione locale dei dati. Il client Firebase sincronizza quindi i dati con i server di database remoti e con altri client secondo il criterio "best effort".

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

Una volta ripristinata 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.

Scopriremo di più sul comportamento offline in Scopri di più sulle funzionalità online e offline.

Passaggi successivi