Daten auf Android lesen und schreiben

In diesem Dokument werden die Grundlagen des Lesens und Schreibens von Firebase-Daten behandelt.

Firebase-Daten werden in eine FirebaseDatabase-Referenz geschrieben und durch Anhängen eines asynchronen Listeners an die Referenz abgerufen. Der Listener wird einmal für den Anfangszustand der Daten und dann jedes Mal ausgelöst, wenn sich die Daten ändern.

(Optional) Prototypen erstellen und mit Firebase Local Emulator Suite testen

Bevor wir darüber sprechen, wie Ihre App Daten aus liest und in Realtime Database schreibt, stellen wir Ihnen eine Reihe von Tools vor, mit denen Sie Realtime Database -Funktionen prototypisieren und testen können: Firebase Local Emulator Suite. Wenn Sie verschiedene Datenmodelle ausprobieren, Ihre Sicherheitsregeln optimieren oder nach der kostengünstigsten Möglichkeit suchen, mit dem Backend zu interagieren, kann es eine gute Idee sein, lokal zu arbeiten, ohne Live-Dienste bereitzustellen.

Ein Realtime Database-Emulator ist Teil der Local Emulator Suite. Damit kann Ihre App mit Ihren emulierten Datenbankinhalten und ‑konfigurationen sowie optional mit Ihren emulierten Projektressourcen (Funktionen, andere Datenbanken und Sicherheitsregeln) interagieren.

Die Verwendung des Realtime Database-Emulators umfasst nur wenige Schritte:

  1. Fügen Sie der Testkonfiguration Ihrer App eine Codezeile hinzu, um eine Verbindung zum Emulator herzustellen.
  2. Führen Sie im Stammverzeichnis Ihres lokalen Projektverzeichnisses firebase emulators:start aus.
  3. Aufrufe aus dem Prototypcode Ihrer App mit einem Realtime Database-Plattform-SDK wie gewohnt oder mit der Realtime Database REST API ausführen.

Eine detaillierte Anleitung mit Realtime Database und Cloud Functions ist verfügbar. Sehen Sie sich auch die Einführung in Local Emulator Suite an.

DatabaseReference abrufen

Zum Lesen oder Schreiben von Daten aus der Datenbank benötigen Sie eine Instanz von DatabaseReference:

Kotlin

private lateinit var database: DatabaseReference
// ...
database = Firebase.database.reference

Java

private DatabaseReference mDatabase;
// ...
mDatabase = FirebaseDatabase.getInstance().getReference();

Daten schreiben

Grundlegende Schreibvorgänge

Für einfache Schreibvorgänge können Sie setValue() verwenden, um Daten in einer bestimmten Referenz zu speichern. Dabei werden alle vorhandenen Daten in diesem Pfad ersetzt. Mit dieser Methode können Sie Folgendes tun:

  • Übergeben Sie Typen, die den verfügbaren JSON-Typen entsprechen, so:
    • String
    • Long
    • Double
    • Boolean
    • Map<String, Object>
    • List<Object>
  • Übergeben Sie ein benutzerdefiniertes Java-Objekt, wenn die Klasse, die es definiert, einen Standardkonstruktor hat, der keine Argumente akzeptiert und öffentliche Getter für die zuzuweisenden Attribute hat.

Wenn Sie ein Java-Objekt verwenden, werden die Inhalte des Objekts automatisch verschachtelt untergeordneten Speicherorten zugeordnet. Die Verwendung eines Java-Objekts macht Ihren Code in der Regel auch lesbarer und einfacher zu verwalten. Wenn Sie beispielsweise eine App mit einem einfachen Nutzerprofil haben, könnte Ihr User-Objekt so aussehen:

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

}

So fügen Sie einen Nutzer mit setValue() hinzu:

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

Wenn Sie setValue() auf diese Weise verwenden, werden Daten am angegebenen Speicherort überschrieben, einschließlich aller untergeordneten Knoten. Sie können jedoch ein untergeordnetes Element aktualisieren, ohne das gesamte Objekt neu zu schreiben. Wenn Sie Nutzern erlauben möchten, ihre Profile zu aktualisieren, können Sie den Nutzernamen so aktualisieren:

Kotlin

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

Java

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

Daten lesen

Daten mit persistenten Listenern lesen

Wenn Sie Daten in einem Pfad lesen und auf Änderungen reagieren möchten, verwenden Sie die Methode addValueEventListener(), um einen ValueEventListener zu einem DatabaseReference hinzuzufügen.

Listener Ereignis-Callback Typische Verwendung
ValueEventListener onDataChange() Änderungen am gesamten Inhalt eines Pfads lesen und darauf warten.

Mit der Methode onDataChange() können Sie einen statischen Snapshot des Inhalts eines bestimmten Pfads lesen, wie er zum Zeitpunkt des Ereignisses vorhanden war. Diese Methode wird einmal ausgelöst, wenn der Listener angehängt wird, und dann jedes Mal, wenn sich die Daten, einschließlich der untergeordneten Elemente, ändern. An den Ereignis-Callback wird ein Snapshot übergeben, der alle Daten an diesem Speicherort enthält, einschließlich untergeordneter Daten. Wenn keine Daten vorhanden sind, gibt der Snapshot false zurück, wenn Sie exists() aufrufen, und null, wenn Sie getValue() aufrufen.

Das folgende Beispiel zeigt eine Social-Blogging-Anwendung, die die Details eines Beitrags aus der Datenbank abruft:

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

Der Listener empfängt einen DataSnapshot, der die Daten am angegebenen Speicherort in der Datenbank zum Zeitpunkt des Ereignisses enthält. Wenn Sie getValue() für einen Snapshot aufrufen, wird die Java-Objektdarstellung der Daten zurückgegeben. Wenn am Speicherort keine Daten vorhanden sind, wird beim Aufrufen von getValue() der Wert null zurückgegeben.

In diesem Beispiel definiert ValueEventListener auch die Methode onCancelled(), die aufgerufen wird, wenn der Lesevorgang abgebrochen wird. Ein Lesevorgang kann beispielsweise abgebrochen werden, wenn der Client nicht berechtigt ist, aus einem Firebase-Datenbankspeicherort zu lesen. An diese Methode wird ein DatabaseError-Objekt übergeben, das angibt, warum der Fehler aufgetreten ist.

Daten einmalig lesen

Einmal mit get() lesen

Das SDK ist so konzipiert, dass es Interaktionen mit Datenbankservern verwaltet, unabhängig davon, ob Ihre App online oder offline ist.

Im Allgemeinen sollten Sie die oben beschriebenen ValueEventListener-Techniken verwenden, um Daten zu lesen und über Aktualisierungen der Daten vom Backend benachrichtigt zu werden. Die Listener-Techniken reduzieren die Nutzung und Abrechnung und sind so optimiert, dass Nutzer die bestmögliche Erfahrung haben, wenn sie online und offline gehen.

Wenn Sie die Daten nur einmal benötigen, können Sie mit get() einen Snapshot der Daten aus der Datenbank abrufen. Wenn get() aus irgendeinem Grund den Serverwert nicht zurückgeben kann, prüft der Client den lokalen Speichercache und gibt einen Fehler zurück, wenn der Wert immer noch nicht gefunden wird.

Die unnötige Verwendung von get() kann die Bandbreitennutzung erhöhen und zu Leistungseinbußen führen. Dies lässt sich durch die Verwendung eines Echtzeit-Listeners wie oben gezeigt verhindern.

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

Einmal mit einem Listener lesen

In einigen Fällen möchten Sie möglicherweise, dass der Wert aus dem lokalen Cache sofort zurückgegeben wird, anstatt auf dem Server nach einem aktualisierten Wert zu suchen. In diesen Fällen können Sie addListenerForSingleValueEvent verwenden, um die Daten sofort aus dem lokalen Datenträger-Cache abzurufen.

Das ist nützlich für Daten, die nur einmal geladen werden müssen und sich voraussichtlich nicht häufig ändern oder kein aktives Listening erfordern. In den vorherigen Beispielen wird diese Methode von der Blogging-App verwendet, um das Profil eines Nutzers zu laden, wenn er einen neuen Beitrag verfasst.

Daten aktualisieren oder löschen

Bestimmte Felder aktualisieren

Wenn Sie gleichzeitig in bestimmte untergeordnete Knoten eines Knotens schreiben möchten, ohne andere untergeordnete Knoten zu überschreiben, verwenden Sie die Methode updateChildren().

Beim Aufrufen von updateChildren() können Sie untergeordnete Werte auf niedrigerer Ebene aktualisieren, indem Sie einen Pfad für den Schlüssel angeben. Wenn Daten zur besseren Skalierung an mehreren Orten gespeichert sind, können Sie alle Instanzen dieser Daten mithilfe von Daten-Fan-out aktualisieren. Eine Social-Blogging-App könnte beispielsweise eine Post-Klasse wie diese haben:

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

Um einen Beitrag zu erstellen und gleichzeitig den Feed mit den letzten Aktivitäten und den Aktivitätsfeed des Nutzers, der den Beitrag veröffentlicht, zu aktualisieren, verwendet die Blogging-Anwendung Code wie diesen:

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

In diesem Beispiel wird push() verwendet, um einen Beitrag im Knoten zu erstellen, der Beiträge für alle Nutzer unter /posts/$postid enthält. Gleichzeitig wird der Schlüssel mit getKey() abgerufen. Der Schlüssel kann dann verwendet werden, um einen zweiten Eintrag in den Beiträgen des Nutzers unter /user-posts/$userid/$postid zu erstellen.

Mit diesen Pfaden können Sie mit einem einzigen Aufruf von updateChildren() gleichzeitige Aktualisierungen an mehreren Stellen im JSON-Baum vornehmen. In diesem Beispiel wird der neue Beitrag an beiden Stellen erstellt. Gleichzeitige Aktualisierungen, die auf diese Weise vorgenommen werden, sind atomar: Entweder sind alle Aktualisierungen erfolgreich oder alle schlagen fehl.

Callback für Abschluss hinzufügen

Wenn Sie wissen möchten, wann Ihre Daten übertragen wurden, können Sie einen Completion Listener hinzufügen. Sowohl setValue() als auch updateChildren() akzeptieren einen optionalen Abschluss-Listener, der aufgerufen wird, wenn der Schreibvorgang erfolgreich in die Datenbank übertragen wurde. Wenn der Aufruf nicht erfolgreich war, wird dem Listener ein Fehlerobjekt übergeben, das den Grund für den Fehler angibt.

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

Daten löschen

Am einfachsten lassen sich Daten löschen, indem Sie removeValue() für einen Verweis auf den Speicherort dieser Daten aufrufen.

Sie können auch löschen, indem Sie null als Wert für einen anderen Schreibvorgang wie setValue() oder updateChildren() angeben. Sie können diese Technik mit updateChildren() verwenden, um mehrere untergeordnete Elemente in einem einzigen API-Aufruf zu löschen.

Listener trennen

Callbacks werden entfernt, indem die Methode removeEventListener() für Ihre Firebase-Datenbankreferenz aufgerufen wird.

Wenn ein Listener mehrmals zu einem Datenspeicherort hinzugefügt wurde, wird er für jedes Ereignis mehrmals aufgerufen. Sie müssen ihn dann genauso oft trennen, um ihn vollständig zu entfernen.

Wenn Sie removeEventListener() für einen übergeordneten Listener aufrufen, werden Listener, die für die untergeordneten Knoten registriert sind, nicht automatisch entfernt. removeEventListener() muss auch für alle untergeordneten Listener aufgerufen werden, um den Callback zu entfernen.

Daten als Transaktionen speichern

Wenn Sie mit Daten arbeiten, die durch gleichzeitige Änderungen beschädigt werden könnten, z. B. inkrementelle Zähler, können Sie einen Transaktionsvorgang verwenden. Sie übergeben diesem Vorgang zwei Argumente: eine Aktualisierungsfunktion und einen optionalen Abschluss-Callback. Die Aktualisierungsfunktion verwendet den aktuellen Status der Daten als Argument und gibt den neuen gewünschten Status zurück, den Sie schreiben möchten. Wenn ein anderer Client in den Speicherort schreibt, bevor Ihr neuer Wert erfolgreich geschrieben wurde, wird Ihre Aktualisierungsfunktion noch einmal mit dem neuen aktuellen Wert aufgerufen und der Schreibvorgang wird wiederholt.

In der Beispiel-App für Social Blogging können Sie Nutzern beispielsweise erlauben, Beiträge mit einem Stern zu markieren und die Markierung aufzuheben. Außerdem können Sie die Anzahl der Sterne, die ein Beitrag erhalten hat, so erfassen:

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

Durch die Verwendung einer Transaktion wird verhindert, dass die Anzahl der Sterne falsch ist, wenn mehrere Nutzer denselben Beitrag gleichzeitig mit einem Stern markieren oder der Client veraltete Daten hatte. Wenn die Transaktion abgelehnt wird, gibt der Server den aktuellen Wert an den Client zurück, der die Transaktion dann mit dem aktualisierten Wert noch einmal ausführt. Dies wird wiederholt, bis die Transaktion akzeptiert wird oder zu viele Versuche unternommen wurden.

Atomare serverseitige Inkremente

Im obigen Anwendungsfall schreiben wir zwei Werte in die Datenbank: die ID des Nutzers, der den Beitrag mit einem Stern markiert oder die Markierung aufhebt, und die erhöhte Anzahl der Sterne. Wenn wir bereits wissen, dass der Nutzer den Beitrag mit einem Stern markiert, können wir anstelle einer Transaktion einen atomaren Inkrementvorgang verwenden.

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

In diesem Code wird kein Transaktionsvorgang verwendet. Er wird daher nicht automatisch noch einmal ausgeführt, wenn ein in Konflikt stehendes Update vorliegt. Da der Inkrementierungsvorgang jedoch direkt auf dem Datenbankserver erfolgt, besteht keine Möglichkeit eines Konflikts.

Wenn Sie anwendungsspezifische Konflikte erkennen und ablehnen möchten, z. B. wenn ein Nutzer einen Beitrag markiert, den er bereits markiert hat, sollten Sie benutzerdefinierte Sicherheitsregeln für diesen Anwendungsfall schreiben.

Offline mit Daten arbeiten

Wenn ein Client die Netzwerkverbindung verliert, funktioniert Ihre App weiterhin ordnungsgemäß.

Jeder Client, der mit einer Firebase-Datenbank verbunden ist, verwaltet eine eigene interne Version aller Daten, für die Listener verwendet werden oder die für die Synchronisierung mit dem Server gekennzeichnet sind. Wenn Daten gelesen oder geschrieben werden, wird zuerst diese lokale Version der Daten verwendet. Der Firebase-Client synchronisiert diese Daten dann nach dem Best-Effort-Prinzip mit den Remote-Datenbankservern und mit anderen Clients.

Daher lösen alle Schreibvorgänge in der Datenbank sofort lokale Ereignisse aus, bevor eine Interaktion mit dem Server stattfindet. So bleibt Ihre App unabhängig von Netzwerklatenz oder Konnektivität reaktionsfähig.

Sobald wieder eine Verbindung hergestellt wurde, erhält Ihre App die entsprechenden Ereignisse, sodass der Client mit dem aktuellen Serverstatus synchronisiert wird, ohne dass Sie benutzerdefinierten Code schreiben müssen.

Weitere Informationen zum Offlineverhalten finden Sie unter Weitere Informationen zu Online- und Offlinefunktionen.

Nächste Schritte