This document covers the basics of reading and writing Firebase data.
Firebase data is written to a FirebaseDatabase
reference and retrieved by
attaching an asynchronous listener to the reference. The listener is triggered
once for the initial state of the data and again anytime the data changes.
Get a DatabaseReference
To read or write data from the database, you need an instance of
DatabaseReference
:
private DatabaseReference mDatabase; // ... mDatabase = FirebaseDatabase.getInstance().getReference();
Read and write data
Basic write operations
For basic write operations, you can use setValue()
to save data to a specified
reference, replacing any existing data at that path. You can use this method to:
- Pass types that correspond to the available JSON types as follows:
String
Long
Double
Boolean
Map<String, Object>
List<Object>
- Pass a custom Java object, if the class that defines it has a default constructor that takes no arguments and has public getters for the properties to be assigned.
If you use a Java object, the contents of your object are automatically mapped
to child locations in a nested fashion. Using a Java object also typically makes
your code more readable and easier to maintain. For example, if you have an
app with a basic user profile, your User
object might look as follows:
@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; } }
You can add a user with setValue()
as follows:
private void writeNewUser(String userId, String name, String email) { User user = new User(name, email); mDatabase.child("users").child(userId).setValue(user); }
Using setValue()
in this way overwrites data at the specified location,
including any child nodes. However, you can still update a child without
rewriting the entire object. If you want to allow users to update their profiles
you could update the username as follows:
mDatabase.child("users").child(userId).child("username").setValue(name);
Listen for value events
To read data at a path and listen for changes, use the addValueEventListener()
oraddListenerForSingleValueEvent()
method to add a ValueEventListener
to a
DatabaseReference
.
Listener | Event callback | Typical usage |
---|---|---|
ValueEventListener |
onDataChange() |
Read and listen for changes to the entire contents of a path. |
You can use the onDataChange()
method to read a static snapshot of the
contents at a given path, as they existed at the time of the event. This method
is triggered once when the listener is attached and again every time the data,
including children, changes. The event callback is passed a snapshot containing
all data at that location, including child data. If there is no data, the
snapshot returned is null
.
The following example demonstrates a social blogging application retrieving the details of a post from the database:
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);
The listener receives a DataSnapshot
that contains the data at the specified
location in the database at the time of the event. Calling getValue()
on a
snapshot returns the Java object representation of the data. If no data exists
at the location, calling getValue()
returns null
.
In this example, ValueEventListener
also defines the onCancelled()
method that
is called if the read is canceled. For example, a read can be canceled if the
client doesn't have permission to read from a Firebase database location. This
method is passed a DatabaseError
object indicating why the failure occurred.
Read data once
In some cases you may want a callback to be called once and then immediately
removed, such as when initializing a UI element that you don't expect to change.
You can use the addListenerForSingleValueEvent()
method
to simplify this scenario: it triggers once and then does not trigger again.
This is useful for data that only needs to be loaded once and isn't expected to change frequently or require active listening. For instance, the blogging app in the previous examples uses this method to load a user's profile when they begin authoring a new post:
Updating or deleting data
Update specific fields
To simultaneously write to specific children of a node without overwriting other
child nodes, use the updateChildren()
method.
When calling updateChildren()
, you can update lower-level child values by
specifying a path for the key. If data is stored in multiple locations to scale
better, you can update all instances of that data using
data fan-out. For example, a
social blogging app might have a Post
class like this:
@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; } }
To create a post and simultaneously update it to the recent activity feed and the posting user's activity feed, the blogging application uses code like this:
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); }
This example uses push()
to create a post in the node containing posts for
all users at /posts/$postid
and simultaneously retrieve the key with
getKey()
. The key can then be used to create a second entry in the user's
posts at /user-posts/$userid/$postid
.
Using these paths, you can perform simultaneous updates to multiple locations in
the JSON tree with a single call to updateChildren()
, such as how this example
creates the new post in both locations. Simultaneous updates made this way
are atomic: either all updates succeed or all updates fail.
Delete data
The simplest way to delete data is to call removeValue()
on a reference to the
location of that data.
You can also delete by specifying null
as the value for another write
operation such as setValue()
or updateChildren()
. You can use this technique
with updateChildren()
to delete multiple children in a single API call.
Detach listeners
Callbacks are removed by calling the removeEventListener()
method on your
Firebase database reference.
If a listener has been added multiple times to a data location, it is called multiple times for each event, and you must detach it the same number of times to remove it completely.
Calling removeEventListener()
on a parent listener does not
automatically remove listeners registered on its child nodes;
removeEventListener()
must also be called on any child listeners
to remove the callback.
Save data as transactions
When working with data that could be corrupted by concurrent modifications, such as incremental counters, you can use a transaction operation. You give this operation two arguments: an update function and an optional completion callback. The update function takes the current state of the data as an argument and returns the new desired state you would like to write. If another client writes to the location before your new value is successfully written, your update function is called again with the new current value, and the write is retried.
For instance, in the example social blogging app, you could allow users to star and unstar posts and keep track of how many stars a post has received as follows:
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 b, DataSnapshot dataSnapshot) { // Transaction completed Log.d(TAG, "postTransaction:onComplete:" + databaseError); } }); }
Using a transaction prevents star counts from being incorrect if multiple users star the same post at the same time or the client had stale data. If the transaction is rejected, the server returns the current value to the client, which runs the transaction again with the updated value. This repeats until the transaction is accepted or too many attempts have been made.
Write data offline
If a client loses its network connection, your app will continue functioning correctly.
Every client connected to a Firebase database maintains its own internal version of any active data. When data is written, it's written to this local version first. The Firebase client then synchronizes that data with the remote database servers and with other clients on a "best-effort" basis.
As a result, all writes to the database trigger local events immediately, before any data is written to the server. This means your app remains responsive regardless of network latency or connectivity.
Once connectivity is reestablished, your app receives the appropriate set of events so that the client syncs with the current server state, without having to write any custom code.