Membaca dan Menulis Data di Android

Dokumen ini membahas dasar-dasar membaca dan menulis data Firebase.

Data Firebase dituliskan ke referensi FirebaseDatabase dan diambil dengan menambahkan pemroses asinkron ke referensi tersebut. Pemroses dipicu satu kali untuk status awal data, dan dipicu lagi setiap kali data berubah.

(Opsional) Membuat prototipe dan melakukan pengujian dengan Firebase Local Emulator Suite

Sebelum membahas cara aplikasi Anda membaca dari dan menulis ke Realtime Database, kami akan memperkenalkan serangkaian alat yang dapat digunakan untuk membuat prototipe dan menguji fungsionalitas Realtime Database: Firebase Local Emulator Suite. Jika Anda sedang mencoba berbagai model data, mengoptimalkan aturan keamanan, atau berupaya menemukan cara yang paling hemat untuk berinteraksi dengan backend, kemampuan untuk bekerja secara lokal tanpa men-deploy layanan langsung dapat sangat bermanfaat.

Emulator Realtime Database adalah bagian dari Local Emulator Suite yang memungkinkan aplikasi berinteraksi dengan konfigurasi dan konten database yang diemulasi, serta secara opsional dengan resource project yang diemulasi (fungsi, database lain, dan aturan keamanan).

Anda hanya perlu beberapa langkah untuk menggunakan emulator Realtime Database:

  1. Menambahkan satu baris kode ke konfigurasi pengujian aplikasi untuk terhubung ke emulator.
  2. Menjalankan firebase emulators:start dari root direktori project lokal Anda.
  3. Melakukan panggilan dari kode prototipe aplikasi Anda menggunakan SDK platform Realtime Database seperti biasa, atau menggunakan Realtime Database REST API.

Panduan mendetail yang mencakup Realtime Database dan Cloud Functions telah tersedia. Sebaiknya baca juga Pengantar Local Emulator Suite.

Mendapatkan DatabaseReference

Untuk membaca atau menulis data dari database, Anda memerlukan instance DatabaseReference:

Kotlin+KTX

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

Java

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

Menulis data

Operasi tulis dasar

Untuk operasi tulis dasar, Anda dapat menggunakan setValue() untuk menyimpan data ke referensi yang ditentukan, sehingga menggantikan data yang ada di jalur tersebut. Anda bisa menggunakan metode ini untuk:

  • Meneruskan jenis yang cocok dengan jenis JSON yang tersedia berikut ini:
    • String
    • Long
    • Double
    • Boolean
    • Map<String, Object>
    • List<Object>
  • Meneruskan objek Java kustom, jika class yang menentukannya memiliki konstruktor default yang tidak membutuhkan argumen, dan memiliki pengambil publik untuk properti yang akan ditetapkan.

Jika Anda menggunakan objek Java, konten objek akan otomatis dipetakan ke lokasi turunan secara bertingkat. Penggunaan objek Java biasanya juga membuat kode Anda lebih mudah dibaca dan dipelihara. Misalnya, jika Anda memiliki aplikasi dengan profil pengguna dasar, objek User mungkin akan terlihat sebagai berikut:

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

}

Anda dapat menambahkan pengguna dengan setValue() sebagai berikut:

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

Penggunaan setValue() seperti ini akan menimpa data di jalur yang ditentukan, termasuk semua node turunan. Namun, Anda masih dapat memperbarui turunan tanpa menulis ulang seluruh objek. Jika ingin mengizinkan pengguna memperbarui profilnya, Anda dapat memperbarui nama pengguna seperti berikut:

Kotlin+KTX

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

Java

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

Membaca data

Membaca data dengan pemroses persisten

Untuk membaca data di suatu jalur dan memproses perubahan, gunakan metode addValueEventListener() untuk menambahkan ValueEventListener ke DatabaseReference.

Pemroses Callback peristiwa Penggunaan standar
ValueEventListener onDataChange() Membaca dan memproses perubahan pada seluruh konten di sebuah jalur.

Anda dapat menggunakan metode onDataChange() untuk membaca snapshot statis konten di jalur tertentu, sebagaimana adanya konten tersebut ketika peristiwa terjadi. Metode ini terpicu satu kali ketika pemroses ditambahkan dan terpicu lagi setiap kali terjadi perubahan pada data, termasuk pada setiap turunannya. Callback peristiwa mendapatkan snapshot yang berisi semua data di lokasi tersebut, termasuk data turunan. Jika tidak ada data, snapshot akan menampilkan false ketika exists() dipanggil, serta menampilkan null ketika getValue() dipanggil pada snapshot tersebut.

Contoh berikut menampilkan aplikasi blogging sosial yang mengambil detail suatu postingan dari 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);

Pemroses akan menerima DataSnapshot yang berisi data di lokasi yang ditentukan dalam database saat peristiwa terjadi. Pemanggilan getValue() pada snapshot akan menampilkan perwakilan objek Java data tersebut. Jika tidak ada data di lokasi, null akan ditampilkan saat getValue() dipanggil.

Dalam contoh ini, ValueEventListener juga menentukan metode onCancelled() yang dipanggil jika operasi baca dibatalkan. Misalnya, proses baca dapat dibatalkan jika klien tidak memiliki izin untuk membaca dari lokasi database Firebase. Metode ini mendapatkan objek DatabaseError yang menunjukkan alasan terjadinya kegagalan.

Membaca data sekali

Membaca sekali menggunakan get()

SDK dirancang untuk mengelola interaksi dengan server database, baik saat aplikasi Anda online maupun offline.

Biasanya, Anda harus menggunakan teknik ValueEventListener yang dijelaskan di atas untuk membaca data agar mendapatkan notifikasi terkait perubahan data dari backend. Teknik pemroses mengurangi penggunaan dan penagihan Anda, serta dioptimalkan untuk memberikan pengalaman terbaik kepada pengguna saat mereka online dan offline.

Jika hanya memerlukan data satu kali, Anda dapat menggunakan get() untuk mendapatkan snapshot data dari database. Jika karena alasan apa pun get() tidak dapat menampilkan nilai server, klien akan menyelidiki cache penyimpanan lokal dan menampilkan error jika nilainya masih belum ditemukan.

Penggunaan get() yang tidak perlu dapat meningkatkan penggunaan bandwidth dan menyebabkan penurunan performa. Ini dapat dicegah menggunakan pemroses realtime seperti yang ditunjukkan di atas.

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

Membaca sekali menggunakan pemroses

Dalam beberapa kasus, Anda mungkin menginginkan nilai dari cache lokal langsung ditampilkan, daripada memeriksa nilai yang diperbarui di server. Dalam kasus tersebut, Anda dapat menggunakan addListenerForSingleValueEvent untuk langsung mendapatkan data dari cache disk lokal.

Cara ini berguna untuk data yang hanya perlu dimuat sekali, dan tidak diharapkan sering berubah atau memerlukan pemroses aktif. Misalnya, aplikasi blogging pada contoh sebelumnya menggunakan metode ini untuk memuat profil pengguna ketika pengguna mulai membuat postingan baru.

Memperbarui atau menghapus data

Memperbarui kolom tertentu

Untuk menulis secara simultan ke turunan tertentu sebuah node tanpa menimpa node turunan yang lain, gunakan metode updateChildren().

Saat memanggil updateChildren(), Anda dapat memperbarui nilai turunan di level yang lebih rendah dengan menentukan jalur untuk kunci. Jika data disimpan dalam beberapa lokasi agar dapat melakukan penskalaan yang lebih baik, Anda dapat memperbarui semua instance data tersebut menggunakan fan-out data. Misalnya, sebuah aplikasi blogging sosial mungkin memiliki class Post seperti ini:

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

Untuk membuat postingan dan memperbaruinya ke feed aktivitas terbaru sekaligus ke feed aktivitas pengguna yang memposting, aplikasi blogging tersebut menggunakan kode seperti ini:

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

Contoh ini menggunakan push() untuk membuat postingan dalam node yang berisi postingan bagi semua pengguna di /posts/$postid, sekaligus mengambil kunci dengan getKey(). Selanjutnya, kunci tersebut dapat digunakan untuk membuat entri kedua di postingan pengguna pada /user-posts/$userid/$postid.

Dengan menggunakan jalur tersebut, Anda dapat menjalankan pembaruan simultan ke beberapa lokasi di hierarki JSON dengan satu panggilan ke updateChildren(), seperti yang digunakan pada contoh ini untuk membuat postingan baru di kedua lokasi. Pembaruan simultan yang dilakukan dengan cara ini bersifat atomik: semuanya akan berhasil atau semuanya akan gagal.

Menambahkan Callback Penyelesaian

Jika ingin tahu kapan data telah di-commit, Anda bisa menambahkan pemroses penyelesaian. setValue() dan updateChildren() menerima pemroses penyelesaian opsional yang dipanggil ketika operasi tulis telah berhasil di-commit ke database. Jika panggilan tidak berhasil, pemroses akan diberi objek error yang menunjukkan penyebab kegagalannya.

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

Menghapus data

Cara termudah untuk menghapus data adalah dengan memanggil removeValue() pada referensi ke lokasi data tersebut.

Penghapusan juga dapat dilakukan dengan menentukan null sebagai nilai untuk operasi tulis lainnya, seperti setValue() atau updateChildren(). Teknik ini dapat digunakan dengan updateChildren() untuk menghapus beberapa turunan dengan satu panggilan API.

Melepas pemroses

Callback dihapus dengan memanggil metode removeEventListener() pada referensi database Firebase.

Jika telah ditambahkan beberapa kali ke lokasi data, pemroses akan dipanggil beberapa kali untuk setiap peristiwa, dan Anda harus melepasnya dalam jumlah yang sama seperti saat menambahkannya agar terhapus semuanya.

Memanggil removeEventListener() pada pemroses induk tidak akan otomatis menghapus pemroses yang terdaftar pada node turunannya. removeEventListener() juga harus dipanggil pada pemroses turunan mana pun untuk menghapus callback.

Menyimpan data sebagai transaksi

Ketika menangani data yang bisa rusak karena perubahan serentak, seperti penghitung pertambahan inkremental, Anda dapat menggunakan operasi transaksi. Operasi ini menggunakan dua argumen: fungsi pembaruan dan callback penyelesaian opsional. Fungsi pembaruan mengambil status data saat ini sebagai argumen, dan akan menampilkan status baru yang ingin Anda tulis. Jika klien lain melakukan operasi tulis ke lokasi ini sebelum nilai baru Anda berhasil ditulis, fungsi pembaruan Anda akan dipanggil lagi dengan nilai saat ini yang baru, dan operasi tulis akan dicoba ulang.

Misalnya, pada contoh aplikasi blogging sosial, Anda dapat mengizinkan pengguna memberi bintang atau menghapus bintang pada postingan, serta mengetahui berapa banyak bintang yang telah diterima suatu postingan dengan cara berikut ini:

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

Menggunakan transaksi akan mencegah kesalahan penghitungan jumlah bintang jika beberapa pengguna memberi bintang pada postingan yang sama secara bersamaan, atau klien memiliki data yang sudah usang. Jika transaksi ditolak, server akan menampilkan nilai saat ini ke klien yang akan menjalankan lagi transaksi tersebut dengan nilai yang diupdate. Proses ini akan berulang hingga transaksi diterima atau ada terlalu banyak percobaan yang dilakukan.

Pertambahan inkremental atomik sisi server

Dalam kasus penggunaan di atas, kita menulis dua nilai ke database: ID pengguna yang memberi/menghapus bintang pada postingan, dan pertambahan inkremental jumlah bintang. Jika sudah mengetahui bahwa pengguna memberi bintang pada postingan, kita dapat menggunakan operasi pertambahan inkremental atomik, bukan transaksi.

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

Kode ini tidak menggunakan operasi transaksi, sehingga tidak otomatis dijalankan ulang jika ada pembaruan yang bertentangan. Namun, karena operasi pertambahan inkremental terjadi langsung di server database, tidak ada kemungkinan konflik.

Jika ingin mendeteksi dan menolak konflik khusus aplikasi, misalnya pengguna memberi bintang pada postingan yang sebelumnya telah dibintanginya, Anda harus menulis aturan keamanan khusus untuk kasus penggunaan tersebut.

Menangani data secara offline

Jika koneksi jaringan klien terputus, aplikasi Anda akan tetap berfungsi dengan baik.

Setiap klien yang terhubung ke database Firebase mempertahankan setiap data versi internalnya sendiri yang menggunakan pemroses atau yang ditandai agar tetap sinkron dengan server. Saat data dibaca atau ditulis, versi lokal data ini digunakan terlebih dahulu. Selanjutnya, klien Firebase menyinkronkan data tersebut dengan server database di tempat lain, dan dengan klien lain berdasarkan "upaya terbaik".

Akibatnya, semua operasi tulis ke database akan segera memicu peristiwa lokal, sebelum ada interaksi dengan server. Artinya, aplikasi Anda akan tetap responsif, apa pun kondisi latensi atau konektivitas jaringannya.

Setelah terhubung kembali ke jaringan, aplikasi Anda akan menerima kumpulan peristiwa yang sesuai agar klien melakukan sinkronisasi dengan kondisi server saat ini, tanpa harus menulis kode khusus.

Kita akan membahas lebih lanjut perilaku offline dalam artikel Mempelajari lebih lanjut kemampuan online dan offline.

Langkah berikutnya