Odczyt i zapis danych na Androidzie

Ten dokument zawiera podstawowe informacje o odczytywaniu i zapisywaniu danych Firebase.

Dane Firebase są zapisywane w referencji FirebaseDatabase i odbierane przez dołączenie do niej asynchronicznego odbiornika. Detektor jest aktywowany raz dla początkowego stanu danych i ponownie przy każdej zmianie danych.

(Opcjonalnie) Prototypowanie i testowanie za pomocą Firebase Local Emulator Suite

Zanim omówimy, jak aplikacja odczytuje i zapisze dane w Realtime Database, zapoznamy Cię z zestawem narzędzi, które możesz wykorzystać do tworzenia prototypów i testowania funkcji Realtime Database: Firebase Local Emulator Suite. Jeśli testujesz różne modele danych, optymalizujesz reguły bezpieczeństwa lub szukasz najbardziej opłacalnego sposobu interakcji z back-endem, warto pracować lokalnie bez wdrażania usług na żywo.

Emulator Realtime Database jest częścią Local Emulator Suite, która umożliwia aplikacji interakcję z emulowaną zawartością i konfiguracją bazy danych, a także opcjonalnie z emulowanymi zasobami projektu (funkcjami, innymi bazami danych i regułami zabezpieczeń).

Aby użyć emulatora Realtime Database, wystarczy wykonać kilka czynności:

  1. Dodajesz wiersz kodu do konfiguracji testowej aplikacji, aby połączyć się z emulatorem.
  2. W katalogu głównym lokalnego katalogu projektu uruchom firebase emulators:start.
  3. Wywoływanie z prototypowego kodu aplikacji za pomocą pakietu SDK platformy Realtime Database lub interfejsu API REST Realtime Database.

Dostępny jest szczegółowy samouczek dotyczący funkcji Realtime DatabaseCloud Functions. Zapoznaj się też z Local Emulator Suitewprowadzeniem.

Pobieranie obiektu DatabaseReference

Aby móc odczytywać lub zapisywać dane z bazy danych, potrzebujesz instancji DatabaseReference:

Kotlin+KTX

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

Java

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

Zapisywanie danych

Podstawowe operacje zapisu

W przypadku podstawowych operacji zapisu możesz użyć polecenia setValue(), aby zapisać dane w określonym odwołaniu, zastępując wszystkie dotychczasowe dane w tej ścieżce. Dzięki tej metodzie możesz:

  • Typy przepustki odpowiadające dostępnym typom danych JSON:
    • String
    • Long
    • Double
    • Boolean
    • Map<String, Object>
    • List<Object>
  • Przekaż niestandardowy obiekt Java, jeśli klasa, która go definiuje, ma domyślny konstruktor, który nie przyjmuje żadnych argumentów i ma publiczne metody gettera dla właściwości, które mają zostać przypisane.

Jeśli używasz obiektu Java, zawartość obiektu jest automatycznie mapowana do podrzędnych lokalizacji w postaci zagnieżdżonej. Użycie obiektu Java zazwyczaj zwiększa też czytelność kodu i utrzymanie w jego utrzymaniu. Jeśli na przykład masz aplikację z podstawowym profilem użytkownika, obiekt User może wyglądać tak:

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

}

Aby dodać użytkownika z uprawnieniami setValue(), wykonaj te czynności:

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

Użycie setValue() w ten sposób powoduje zastąpienie danych w wybranej lokalizacji, w tym wszystkich węzłów podrzędnych. Nadal jednak możesz zaktualizować element podrzędny bez przepisywania całego obiektu. Jeśli chcesz zezwolić użytkownikom na aktualizowanie ich profili, możesz zaktualizować nazwę użytkownika w ten sposób:

Kotlin+KTX

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

Java

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

Odczytywanie danych

Odczytywanie danych za pomocą trwałych odbiorców

Aby odczytywać dane na ścieżce i monitorować zmiany, użyj metody addValueEventListener(), aby dodać element ValueEventListener do elementu DatabaseReference.

Listener Połączenie zwrotne zdarzenia Typowe zastosowanie
ValueEventListener onDataChange() czytać i słuchać zmian w całej zawartości ścieżki;

Metoda onDataChange() umożliwia odczyt statycznego migawka zawartości w danej ścieżce w stanie, w jakim znajdowała się ona w momencie zdarzenia. Ta metoda jest wywoływana raz, gdy słuchacz jest dołączony, i ponownie za każdym razem, gdy dane, w tym dzieci, ulegną zmianie. Funkcja wywołania z okazji zdarzenia otrzymuje podany przez Ciebie snapshot zawierający wszystkie dane w tej lokalizacji, w tym dane podrzędne. Jeśli nie ma żadnych danych, snapshot zwróci wartość false, gdy wywołasz exists(), i null, gdy wywołasz getValue().

Ten przykład pokazuje aplikację do blogowania społecznościowego, która pobiera szczegóły wpisu z bazy danych:

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

Listener otrzymuje DataSnapshot, który zawiera dane z określonej lokalizacji w bazie danych w momencie zdarzenia. Wywołanie metody getValue() w przypadku snapshot zwraca obiekt Java reprezentujący dane. Jeśli w danej lokalizacji nie ma żadnych danych, wywołanie funkcji getValue() zwraca null.

W tym przykładzie ValueEventListener definiuje też metodę onCancelled(), która jest wywoływana, gdy odczyt zostanie anulowany. Odczyt może zostać anulowany, jeśli klient nie ma uprawnień do odczytu z lokalizacji bazy danych Firebase. Do tej metody jest przekazywany obiekt DatabaseError, który wskazuje, dlaczego wystąpił błąd.

odczytywanie danych raz;

Jednokrotny odczyt za pomocą get()

Pakiet SDK służy do zarządzania interakcjami z serwerami baz danych niezależnie od tego, czy aplikacja działa online, czy offline.

Ogólnie zalecamy stosowanie opisanych powyżej technik ValueEventListener do odczytywania danych, aby otrzymywać powiadomienia o aktualizacjach danych z back-endu. Techniki związane z odtwarzaniem dźwięku zmniejszają wykorzystanie i obciążenia oraz są optymalizowane pod kątem zapewnienia użytkownikom jak najlepszych wrażeń w sieci i poza nią.

Jeśli dane są potrzebne tylko raz, możesz użyć funkcji get(), aby uzyskać podsumowanie danych z bazy danych. Jeśli z jakiegokolwiek powodu get() nie może zwrócić wartości serwera, klient sprawdzi pamięć podręczną lokalnego magazynu i zwróci błąd, jeśli wartość nadal nie zostanie znaleziona.

Niepotrzebne używanie funkcji get() może zwiększyć wykorzystanie przepustowości i spowodować spadek wydajności. Można temu zapobiec, używając odbiorcy w czasie rzeczywistym, jak pokazano powyżej.

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

Czytanie raz za pomocą listenera

W niektórych przypadkach możesz chcieć, aby wartość z lokalnej pamięci podręcznej była zwracana natychmiast, zamiast sprawdzania aktualizacji na serwerze. W takich przypadkach możesz użyć polecenia addListenerForSingleValueEvent, aby natychmiast pobrać dane z lokalnego dysku podręcznego.

Jest to przydatne w przypadku danych, które trzeba wczytać tylko raz i które nie powinny się często zmieniać ani wymagać aktywnego słuchania. Na przykład aplikacja do blogowania w poprzednich przykładach używa tej metody do wczytania profilu użytkownika, gdy ten zacznie pisać nowy post.

Aktualizowanie i usuwanie danych

Zaktualizuj określone pola

Aby jednocześnie zapisywać dane w określonych węzłach podrzędnych węzła bez zastępowania innych węzłów podrzędnych, użyj metody updateChildren().

Podczas wywoływania funkcji updateChildren() możesz aktualizować wartości podrzędne niższego poziomu, podając ścieżkę klucza. Jeśli dane są przechowywane w wielu lokalizacjach w celu zwiększenia ich skalowania, możesz zaktualizować wszystkie ich wystąpienia, używając przekazywania danych na zewnątrz. Na przykład aplikacja do blogowania społecznościowego może mieć klasę Post, która wygląda tak:

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

Aby utworzyć post i jednocześnie zaktualizować go w strumieniach aktywności użytkownika, aplikacja do blogowania używa kodu podobnego do tego:

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

W tym przykładzie funkcja push() służy do tworzenia postu w węźle zawierającym posty wszystkich użytkowników w węźle /posts/$postid i jednoczesnego pobierania klucza za pomocą funkcji getKey(). Klucz można następnie wykorzystać do utworzenia drugiego wpisu w postach użytkownika na stronie /user-posts/$userid/$postid.

Dzięki tym ścieżkom możesz przeprowadzać jednoczesne aktualizacje wielu lokalizacji w drzewie JSON za pomocą jednego wywołania funkcji updateChildren(), jak w tym przykładzie, w którym tworzy nowy post w obu lokalizacjach. Jednoczesne aktualizacje w ten sposób są atomowe: albo wszystkie się powiodą, albo wszystkie się nie powiodą.

Dodaj zakończone wywołanie zwrotne

Jeśli chcesz się dowiedzieć, kiedy dane zostały zatwierdzone, możesz dodać detektor uzupełnienia. Zarówno setValue(), jak i updateChildren() przyjmują opcjonalny detektor ukończenia, który jest wywoływany po pomyślnym zatwierdzeniu zapisu w bazie danych. Jeśli wywołanie zakończyło się niepowodzeniem, odbiorca otrzyma obiekt błędu wskazujący, dlaczego wystąpił błąd.

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

Usuń dane

Najprostszym sposobem usunięcia danych jest wywołanie metody removeValue() w odniesieniu do lokalizacji tych danych.

Możesz też usunąć plik, określając null jako wartość innej operacji zapisu, np. setValue() lub updateChildren(). Możesz użyć tej metody z updateChildren(), aby usunąć wiele elementów podrzędnych w jednym wywołaniu interfejsu API.

Odłącz detektory

Callbacki są usuwane przez wywołanie metody removeEventListener() w przypadku odwołania do bazy danych Firebase.

Jeśli detektor został dodany do lokalizacji danych wiele razy, jest wywoływany wiele razy w przypadku każdego zdarzenia. Aby całkowicie usunąć odbiornik, musisz go odłączyć tyle samo razy.

Wywołanie metody removeEventListener() w słuchaczu nadrzędnym nie powoduje automatycznego usuwania słuchaczy zarejestrowanych w podrzędnych węzłach. Aby usunąć wywołanie zwrotne, musisz też wywołać metodę removeEventListener() w słuchaczach podrzędnych.

Zapisywanie danych jako transakcji

Podczas pracy z danymi, które mogą zostać uszkodzone przez równoległe modyfikacje, np. z licznikami przyrostowymi, możesz użyć operacji transakcji. Ta operacja wymaga podania 2 argumentów: funkcji aktualizacji i opcjonalnego wywołania zwrotnego po zakończeniu. Funkcja aktualizacji przyjmuje bieżący stan danych jako argument i zwraca nowy pożądany stan do zapisania. Jeśli inny klient zapisze dane w lokalizacji, zanim Twoja nowa wartość zostanie zapisana, funkcja aktualizacji zostanie wywołana ponownie z nową bieżącą wartością, a zapis jest ponawiany.

Na przykład w przypadku aplikacji do prowadzenia bloga społecznościowego możesz zezwolić użytkownikom na oznaczanie postów gwiazdką i usuwanie tej oceny oraz śledzenie, ile gwiazdek otrzymał dany post. W tym celu:

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

Użycie transakcji zapobiega nieprawidłowej liczbie gwiazdek, jeśli wielu użytkowników oznaczyło gwiazdką ten sam post w tym samym czasie lub klient miał nieaktualne dane. W przypadku odrzucenia transakcji serwer zwraca bieżącą wartość klientowi, który uruchamia transakcję ponownie ze zaktualizowaną wartością. Ta procedura jest powtarzana, dopóki transakcja nie zostanie zaakceptowana lub nie zostanie podjęta zbyt duża liczba prób.

Atomowe zwiększenia po stronie serwera

W tym przypadku do bazy danych zapisujemy 2 wartości: identyfikator użytkownika, który oznaczył post gwiazdką lub odznaczył go gwiazdką, oraz zwiększoną liczbę gwiazdek. Jeśli wiemy, że użytkownik oznaczył post jako ulubiony, możemy użyć operacji atomowej zwiększania zamiast transakcji.

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

Ten kod nie korzysta z operacji transakcji, więc nie jest automatycznie ponownie uruchamiany, jeśli wystąpiła aktualizacja powodująca konflikt. Operacja zwiększania wartości występuje jednak bezpośrednio na serwerze bazy danych, więc nie ma możliwości wystąpienia konfliktu.

Jeśli chcesz wykrywać i odrzucać konflikty dotyczące aplikacji (np. gdy użytkownik oznacza posta gwiazdką już wcześniej), musisz utworzyć niestandardowe reguły zabezpieczeń odpowiednie do tego zastosowania.

Praca z danymi offline

Jeśli klient utraci połączenie z siecią, Twoja aplikacja będzie nadal działać prawidłowo.

Każdy klient połączony z bazą danych Firebase utrzymuje własną wersję wewnętrzną wszystkich danych, w przypadku których są używane odsłuchy lub które są oznaczone jako dane, które mają być synchronizowane z serwerem. Podczas odczytu lub zapisu danych używana jest najpierw lokalna wersja danych. Klient Firebase synchronizuje te dane z serwerami zdalnych baz danych oraz z innymi klientami w miarę swoich możliwości.

W rezultacie wszystkie zapisy w bazie danych wywołują zdarzenia lokalne natychmiast, jeszcze przed jakąkolwiek interakcją z serwerem. Oznacza to, że aplikacja pozostaje responsywna niezależnie od opóźnienia sieci lub połączenia.

Po ponownym nawiązaniu połączenia aplikacja otrzymuje odpowiedni zestaw zdarzeń, dzięki czemu klient synchronizuje się z bieżącym stanem serwera, bez konieczności pisania kodu niestandardowego.

Więcej informacji o działaniu offline znajdziesz w artykule Więcej informacji o możliwościach online i offline.

Dalsze kroki