Daten auf Android lesen und schreiben

In diesem Dokument werden die Grundlagen zum Lesen und Schreiben von Firebase-Daten erläutert.

Firebase-Daten werden in eine FirebaseDatabase-Referenz geschrieben und abgerufen, indem ein asynchroner Listener an die Referenz angehängt wird. Der Listener wird einmal für den anfänglichen Status der Daten und dann bei jeder Änderung der Daten ausgelöst.

Optional: Prototyp erstellen und mit Firebase Local Emulator Suite testen

Bevor wir uns damit befassen, wie Ihre App Daten aus Realtime Database liest und in Realtime Database schreibt, stellen wir Ihnen einige Tools vor, mit denen Sie Prototypen erstellen und die Realtime Database-Funktionen testen können: Firebase Local Emulator Suite. Wenn Sie verschiedene Datenmodelle ausprobieren, Ihre Sicherheitsregeln optimieren oder die kostengünstigste Möglichkeit zur Interaktion mit dem Back-End finden möchten, kann es sehr hilfreich sein, lokal arbeiten zu können, ohne Live-Dienste bereitzustellen.

Ein Realtime Database-Emulator ist Teil des Local Emulator Suite. Er ermöglicht es Ihrer App, mit den emulierten Datenbankinhalten und der emulierten Konfiguration sowie optional mit den emulierten Projektressourcen (Funktionen, anderen Datenbanken und Sicherheitsregeln) zu interagieren.

Die Verwendung des Realtime Database-Emulators ist ganz einfach:

  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. Sie können wie gewohnt über ein Realtime Database-Plattform-SDK oder die Realtime Database REST API Aufrufe aus dem Prototypcode Ihrer App starten.

Eine detaillierte Anleitung für Realtime Database und Cloud Functions ist verfügbar. Sehen Sie sich auch die Local Emulator SuiteEinführung an.

Datenbankreferenz abrufen

Wenn Sie Daten aus der Datenbank lesen oder schreiben möchten, benötigen Sie eine Instanz von DatabaseReference:

Kotlin+KTX

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

Java

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

Daten schreiben

Grundlegende Schreibvorgänge

Bei einfachen Schreibvorgängen können Sie mit setValue() Daten in einer bestimmten Referenz speichern und alle vorhandenen Daten an diesem Pfad ersetzen. Mit dieser Methode können Sie Folgendes tun:

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

Wenn Sie ein Java-Objekt verwenden, werden die Inhalte des Objekts automatisch verschachtelt auf untergeordnete Standorte zugeordnet. Außerdem ist Ihr Code mit einem Java-Objekt in der Regel leichter lesbar und einfacher zu verwalten. Wenn Sie beispielsweise eine App mit einem einfachen Nutzerprofil haben, könnte Ihr User-Objekt so aussehen:

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

}

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

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

Wenn Sie setValue() auf diese Weise verwenden, werden die Daten an diesem Speicherort überschrieben, einschließlich aller untergeordneten Knoten. Sie können ein untergeordnetes Objekt jedoch 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+KTX

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 unter einem Pfad lesen und auf Änderungen warten möchten, fügen Sie mit der Methode addValueEventListener() einem DatabaseReference ein ValueEventListener hinzu.

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

Mit der Methode onDataChange() können Sie einen statischen Snapshot des Inhalts an einem bestimmten Pfad 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. Dem 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.

Im folgenden Beispiel wird eine Social-Blogging-Anwendung veranschaulicht, die die Details eines Beitrags aus der Datenbank abruft:

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

Der Listener empfängt ein DataSnapshot, das 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 an diesem Speicherort keine Daten vorhanden sind, wird beim Aufruf von getValue() null zurückgegeben.

In diesem Beispiel wird in ValueEventListener auch die Methode onCancelled() definiert, die aufgerufen wird, wenn die Lesevorgänge abgebrochen werden. Ein Lesevorgang kann beispielsweise abgebrochen werden, wenn der Client keine Leseberechtigung für einen Firebase-Datenbankspeicherort hat. Dieser Methode wird ein DatabaseError-Objekt übergeben, das angibt, warum der Fehler aufgetreten ist.

Daten einmal lesen

Einmal mit get() lesen

Das SDK wurde entwickelt, um Interaktionen mit Datenbankservern zu verwalten, unabhängig davon, ob Ihre App online oder offline ist.

Im Allgemeinen sollten Sie die oben beschriebenen ValueEventListener-Methoden zum Lesen von Daten verwenden, um über Aktualisierungen der Daten aus dem Backend benachrichtigt zu werden. Mit den Listener-Methoden wird die Nutzung und Abrechnung reduziert. Außerdem sind sie so optimiert, dass Nutzer beim Wechsel zwischen Online- und Offlinemodus möglichst wenig Einschränkungen erleben.

Wenn Sie die Daten nur einmal benötigen, können Sie mit get() einen Snapshot der Daten aus der Datenbank abrufen. Wenn get() den Serverwert aus irgendeinem Grund nicht zurückgeben kann, sucht der Client im Cache des lokalen Speichers 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 kann durch die Verwendung eines Echtzeit-Listeners wie oben gezeigt verhindert werden.

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

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 nach einem aktualisierten Wert auf dem Server zu suchen. In diesen Fällen können Sie addListenerForSingleValueEvent verwenden, um die Daten sofort aus dem lokalen Laufwerkcache abzurufen.

Das ist nützlich für Daten, die nur einmal geladen werden müssen und sich voraussichtlich nicht häufig ändern oder für die aktives Zuhören erforderlich ist. In der Blogging-App in den vorherigen Beispielen wird diese Methode beispielsweise verwendet, um das Profil eines Nutzers zu laden, wenn er mit dem Verfassen eines neuen Beitrags beginnt.

Daten aktualisieren oder löschen

Bestimmte Felder aktualisieren

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

Wenn Sie updateChildren() aufrufen, können Sie untergeordnete Werte der unteren Ebene aktualisieren, indem Sie einen Pfad für den Schlüssel angeben. Wenn Daten zur besseren Skalierung an mehreren Orten gespeichert werden, können Sie alle Instanzen dieser Daten mithilfe der Datenfan-out-Funktion aktualisieren. Eine App für soziales Bloggen könnte beispielsweise eine Post-Klasse wie diese haben:

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

Um einen Beitrag zu erstellen und gleichzeitig im Feed der letzten Aktivitäten und im Aktivitätsfeed des Nutzers zu aktualisieren, verwendet die Blog-Anwendung Code wie diesen:

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

In diesem Beispiel wird mit push() ein Beitrag im Knoten erstellt, der Beiträge für alle Nutzer unter /posts/$postid enthält. Gleichzeitig wird der Schlüssel mit getKey() abgerufen. Mit dem Schlüssel kann dann ein zweiter Eintrag in den Beiträgen des Nutzers unter /user-posts/$userid/$postid erstellt werden.

Mit diesen Pfaden kannst du mit einem einzigen Aufruf von updateChildren() gleichzeitig mehrere Stellen im JSON-Baum aktualisieren. In diesem Beispiel wird beispielsweise der neue Beitrag an beiden Stellen erstellt. Simultane Aktualisierungen auf diese Weise sind atomar: Entweder sind alle Aktualisierungen erfolgreich oder alle schlagen fehl.

Abschluss-Callback hinzufügen

Wenn Sie wissen möchten, wann Ihre Daten verbindlich gespeichert wurden, können Sie einen Abschluss-Listener hinzufügen. Sowohl setValue() als auch updateChildren() akzeptieren einen optionalen Abschluss-Listener, der aufgerufen wird, wenn die Daten erfolgreich in der Datenbank verbindlich gespeichert wurden. Wenn der Aufruf fehlgeschlagen ist, wird dem Listener ein Fehlerobjekt übergeben, das angibt, warum der Fehler aufgetreten ist.

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

Daten löschen

Am einfachsten lassen sich Daten löschen, indem Sie removeValue() auf eine Referenz zum Speicherort dieser Daten anwenden.

Sie können auch löschen, indem Sie null als Wert für einen anderen Schreibvorgang wie setValue() oder updateChildren() angeben. Mit dieser Methode und updateChildren() können Sie mehrere untergeordnete Elemente in einem einzigen API-Aufruf löschen.

Listener trennen

Callbacks werden entfernt, indem Sie die removeEventListener()-Methode der Firebase-Datenbankreferenz aufrufen.

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

Wenn removeEventListener() auf einen übergeordneten Listener aufgerufen wird, werden die bei den untergeordneten Knoten registrierten Listener nicht automatisch entfernt. removeEventListener() muss auch auf alle untergeordneten Listener aufgerufen werden, um den Rückruf zu entfernen.

Daten als Transaktionen speichern

Wenn Sie mit Daten arbeiten, die durch gleichzeitige Änderungen beschädigt werden können, z. B. mit inkrementellen Zählern, können Sie einen Transaktionsvorgang verwenden. Sie geben diesem Vorgang zwei Argumente: eine Aktualisierungsfunktion und einen optionalen Rückruf bei Abschluss. Die Update-Funktion nimmt 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 an den Speicherort schreibt, bevor der neue Wert erfolgreich geschrieben wurde, wird die Aktualisierungsfunktion noch einmal mit dem neuen aktuellen Wert aufgerufen und der Schreibvorgang wird noch einmal versucht.

In der Beispiel-App für soziales Bloggen könnten Sie Nutzern beispielsweise erlauben, Beiträge zu bewerten und die Anzahl der Sterne zu verfolgen, die ein Beitrag erhalten hat. Dazu gehen Sie so vor:

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

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

Atomare serverseitige Increments

Im obigen Anwendungsfall schreiben wir zwei Werte in die Datenbank: die ID des Nutzers, der dem Beitrag ein bzw. kein Sternchen gibt, und die inkrementierte Anzahl der Sterne. Wenn wir bereits wissen, dass der Nutzer den Beitrag mit einem Stern markiert, können wir anstelle einer Transaktion einen atomaren Inkrementierungsvorgang verwenden.

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

Da in diesem Code kein Transaktionsvorgang verwendet wird, wird er nicht automatisch neu ausgeführt, wenn es ein in Konflikt stehendes Update gibt. Da der Inkrementenvorgang jedoch direkt auf dem Datenbankserver ausgeführt wird, besteht keine Gefahr eines Konflikts.

Wenn Sie anwendungsspezifische Konflikte erkennen und ablehnen möchten, z. B. wenn ein Nutzer einem Beitrag ein Sternchen hinzufügt, das er ihm bereits zuvor gegeben 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 seine eigenen internen Datenversionen, für die Listener verwendet werden oder die mit dem Server synchronisiert werden sollen. Beim Lesen oder Schreiben von Daten 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 anderen Clients.

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

Sobald die Verbindung wiederhergestellt ist, erhält Ihre App die entsprechenden Ereignisse, damit der Client mit dem aktuellen Serverstatus synchronisiert wird, ohne dass benutzerdefinierter Code geschrieben werden muss.

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

Nächste Schritte