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.

Mendapatkan DatabaseReference

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

Java

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

Kotlin+KTX

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

Membaca dan 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 getter publik untuk properti yang akan ditetapkan.

Jika Anda menggunakan objek Java, konten objek Anda akan otomatis dipetakan ke lokasi turunan secara bertingkat. Jika objek Java digunakan, kode Anda biasanya jadi lebih mudah dibaca dan dipertahankan. Misalnya, jika Anda memiliki aplikasi dengan profil pengguna dasar, objek User Anda mungkin akan terlihat seperti berikut:

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

}

Kotlin+KTX

@IgnoreExtraProperties
data class User(
    var username: String? = "",
    var email: String? = ""
)

Anda dapat menambahkan pengguna dengan setValue() seperti berikut:

Java

private void writeNewUser(String userId, String name, String email) {
    User user = new User(name, email);

    mDatabase.child("users").child(userId).setValue(user);
}

Kotlin+KTX

private fun writeNewUser(userId: String, name: String, email: String?) {
    val user = User(name, email)
    database.child("users").child(userId).setValue(user)
}

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

Java

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

Kotlin+KTX

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

Memproses peristiwa nilai

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

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

Anda dapat menggunakan metode onDataChange() untuk membaca snapshot statis konten di jalur tertentu, saat konten tersebut ada selama peristiwa terjadi. Metode ini terpicu satu kali ketika pemroses terpasang 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 Anda memanggil exists(), dan null ketika Anda memanggil getValue() pada snapshot tersebut.

Contoh berikut menunjukkan aplikasi blogging sosial yang mengambil detail suatu postingan dari database:

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

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)

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 dari data. 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 meneruskan objek DatabaseError yang menunjukkan alasan terjadinya kegagalan.

Membaca data sekali

Dalam beberapa kasus, Anda mungkin ingin agar callback dipanggil satu kali kemudian segera dihapus, seperti ketika melakukan inisialisasi elemen UI yang tidak diharapkan berubah. Anda dapat menggunakan metode addListenerForSingleValueEvent() untuk menyederhanakan skenario ini: metode ini dipicu satu kali dan tidak dipicu lagi.

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

Mengupdate atau menghapus data

Mengupdate kolom tertentu

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

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

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

}

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

Untuk membuat postingan dan sekaligus mengupdatenya ke feed aktivitas terbaru serta memposting feed aktivitas pengguna, aplikasi blog menggunakan kode seperti ini:

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

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 = HashMap<String, Any>()
    childUpdates["/posts/$key"] = postValues
    childUpdates["/user-posts/$userId/$key"] = postValues

    database.updateChildren(childUpdates)
}

Contoh ini menggunakan push() guna membuat postingan dalam node yang berisi postingan untuk semua pengguna di /posts/$postid, dan 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 update simultan ke beberapa lokasi di pohon JSON dengan sekali panggilan ke updateChildren(), seperti yang digunakan pada contoh ini untuk membuat postingan baru di kedua lokasi. Melalui update simultan, proses ini berjalan menyeluruh: entah semua update berhasil atau semua update gagal.

Menambahkan Callback Penyelesaian

Jika Anda ingin tahu kapan data menerapkan commit, Anda bisa menambahkan pemroses penyelesaian. setValue()dan updateChildren() membawa pemroses penyelesaian opsional yang dipanggil ketika operasi tulis telah berhasil menerapkan commit pada database. Jika panggilan tidak berhasil, pemroses akan diberikan objek error yang menunjukkan penyebab terjadinya kegagalan.

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

Kotlin+KTX

database.child("users").child(userId).setValue(user)
        .addOnSuccessListener {
            // Write was successful!
            // ...
        }
        .addOnFailureListener {
            // 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 sebuah panggilan API.

Melepas pemroses

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

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

Jika removeEventListener() dipanggil pada pemroses induk, pemroses yang terdaftar pada node turunannya tidak akan terhapus secara otomatis; removeEventListener() juga harus dipanggil pada pemroses turunan mana pun untuk menghapus callback.

Menyimpan data sebagai transaksi

Ketika bekerja dengan data yang bisa rusak karena perubahan serentak, seperti penghitung tambahan, Anda dapat menggunakan operasi transaksi. Operasi ini menggunakan dua argumen: fungsi update dan callback penyelesaian opsional. Fungsi update mengambil kondisi data saat ini sebagai argumen, dan akan menampilkan kondisi baru yang diinginkan untuk Anda tulis. Jika klien lainnya melakukan penulisan ke lokasi sebelum nilai baru Anda berhasil ditulis, fungsi update Anda akan dipanggil lagi dengan nilai baru saat ini, dan proses tulis akan dicoba ulang.

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

Java

private void onStarClicked(DatabaseReference postRef) {
    postRef.runTransaction(new Transaction.Handler() {
        @Override
        public Transaction.Result doTransaction(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);
        }
    });
}

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

Penggunaan transaksi dapat mencegah kesalahan penghitungan jumlah bintang jika beberapa pengguna memberi bintang pada postingan yang sama secara bersamaan atau jika klien memiliki data yang sudah tidak digunakan lagi. 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.

Menulis data offline

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

Setiap klien yang terhubung ke database Firebase menyimpan versi internalnya sendiri dari setiap data aktif. Ketika ditulis, data akan dituliskan ke versi lokal ini 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 data yang dituliskan ke server. Ini berarti 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, sehingga klien dapat menyinkronkannya dengan kondisi server saat ini, tanpa harus menulis kode khusus.

Langkah berikutnya