13.
Reading to & Writing from Realtime Database
Written by Dean Djermanović
In the last chapter, you integrated Realtime Database into your app. You added Firebase SDK to your app and connected the app with Firebase project. You also learned about database rules and you set them up to allow only authenticated users the access to the database. You even wrote your first data to the database which was just a sneak peek of what you’ll do in this chapter.
This chapter will teach you how to work with Realtime Database data. You’ll learn how to read and write data as well as how to do basic manipulation with that data. First, you’ll learn about performing CRUD operations on to the Realtime Database. CRUD is just an acronym for the four basic types of SQL commands: Create, Read, Update, Delete. You’ll combine all these concepts together in order to build a fully functional app with Realtime Database as the backend.
Setting up Firebase
You need to set up Firebase in order to follow along. Do the following steps:
- Create a project in the Firebase console.
- Enable Google sign-in.
- Set security rules to the test mode to allow everyone read and write access.
- Add google-service.json to both starter and final projects.
To see how to do this, go back to Chapter 11: “Firebase Overview” and Chapter 12: “Introduction to Firebase Realtime Database.”
Reading and writing data
Open starter project, and build and run your app. If this is the first time you’re running this app you’ll need to sign in first in order to use it. To sign in, tap the Sign in with Google button and follow the steps on the screen. Next, click on the floating action button. A new screen opens where you can write your post. Write something and click on the Post button:
As you can see nothing happens yet. You’ll add the logic for saving the post to the database.
Saving data to the database
Open the RealtimeDatabaseManager class. Add the database field to the class:
private val database = FirebaseDatabase.getInstance()
FirebaseDatabase object is the main entry point to the database. The getInstance() method gets you the default FirebaseDatabase instance. There are overloads of getInstance() methods if you want to get the database for the specific URL or specific app.
Add POSTS_REFERENCE constant above the class declaration:
private const val POSTS_REFERENCE = "posts"
Next, add the function for creation of the Post object to the RealtimeDatabaseManager class:
private fun createPost(key: String, content: String): Post {
val user = authenticationManager.getCurrentUser()
val timestamp = getCurrentTime()
return Post(key, content, user, timestamp)
}
This function uses the AuthenticationManager class to get the current logged in user name, gets the current time and then returns newly created Post instance.
Next, add the function for saving the post to the database:
fun addPost(content: String, onSuccessAction: () -> Unit, onFailureAction: () -> Unit) {
//1
val postsReference = database.getReference(POSTS_REFERENCE)
//2
val key = postsReference.push().key ?: ""
val post = createPost(key, content)
//3
postsReference.child(key)
.setValue(post)
.addOnSuccessListener { onSuccessAction() }
.addOnFailureListener { onFailureAction() }
}
Here’s what happens here:
-
DatabaseReferenceclass represents a particular location in the database and it’s used to refer to the location in the database to which you want to write to or read from.getReference()method returns a reference to the database root node. You won’t save posts to the root node. Instead, you’ll create a new node for the posts, which is why you addedPOSTS_REFERENCEconstant earlier. You pass that constant to thegetReference()and this returns a reference for the provided path. Now you can usepostsReferenceto read or write data to this location. - Posts will be added as a child of the
postsnode. To add a post as a child it needs to have a unique key which will be used as a path to the specific post. The key needs to be unique because setting a value to the existing path would overwrite previous value on that path. You don’t want that. You can use thepush()method to create an empty node with an auto-generated key. Thepush()method returns a database reference to the newly created node. You can callgetKey()on the database reference to get the key to that reference. Next, you create aPostinstance that you’ll save to the database and you store the key of that post so you can refer to it later. - To access the newly created location you can use the
child()method that returns a reference to the location relative to the calling reference. Finally, you use thesetValue(post)method to save the post to this location. The Realtime Database accepts multiple data types to store the data:String,Long,Double,Boolean,Map<String, Object>, andList<Object>. You can also use custom Kotlin or Java objects to store the data model class directly to the database as you’re doing here. Finally, you attachOnSuccessListenerthat gets called if the post is saved to the database successfully, andOnFailureListenerthat gets called if the post saving failed.
Now open the AddPostActivity class and replace the TODO in the addPostIfNotEmpty() with the following:
val postMessage = postText.text.toString().trim()
if (postMessage.isNotEmpty()) {
realtimeDatabaseManager.addPost(postMessage,
{ showToast(getString(R.string.posted_successfully)) },
{ showToast(getString(R.string.posting_failed)) } )
finish()
} else {
showToast(getString(R.string.empty_post_message))
}
Here you get the text from EditText and if it’s not empty you save the text to the database and you close the current activity.
Build and run your app. Click on the floating action button on the home screen, add some text and tap the Post button. Current activity gets closed and the home screen is shown. Posts should be displayed on the home screen. You’ll add logic for that in a bit.
Open the database in the Firebase console and confirm that data is saved into the database. You should see your post here along with addiotional data that you added:
Good job! You’ll add the logic for displaying posts on the home screen next.
Fetching data from the database
When it comes to reading the data from the database you have two options. You can read the data once or you can be notified whenever data changes. Since you want to see every new post from other users instantly you’ll implement the second option.
To get all posts from the database and listen for value changes you need to use ValueEventListener. You need to attach this listener to the specific location in the database that you want to listen for changes from.
Open RealtimeDatabaseManager class and add postsValues field:
private val postsValues = MutableLiveData<List<Post>>()
You’ll use LiveData to notify the observers about post changes.
Next, add postsValueEventListener field:
private lateinit var postsValueEventListener: ValueEventListener
This where you’ll store your event listener.
Next, add the following function:
private fun listenForPostsValueChanges() {
//1
postsValueEventListener = object : ValueEventListener {
//2
override fun onCancelled(databaseError: DatabaseError) {
/* No op */
}
//3
override fun onDataChange(dataSnapshot: DataSnapshot) {
//4
if (dataSnapshot.exists()) {
val posts = dataSnapshot.children.mapNotNull { it.getValue(Post::class.java) }.toList()
postsValues.postValue(posts)
} else {
//5
postsValues.postValue(emptyList())
}
}
}
//6
database.getReference(POSTS_REFERENCE)
.addValueEventListener(postsValueEventListener)
}
- You add
ValueEventListeneras an anonymous inner class and you assign it topostsValueEventListenerfield. There are two methods that you need to implement. - The
onCancelled(databaseError: DatabaseError)method gets triggered if reading from the database is cancelled. Reading can be canceled in case if there are server issues or if you don’t have access to the location you’re trying to read from due to database rules.databaseErrorparameter contains more information about an error that occurred. In this case, you won’t do anything if reading gets canceled. -
onDataChange(dataSnapshot: DataSnapshot)gets triggered whenever data under the reference you attached the listener to gets changed; either new data is added or existing data is updated or deleted. This is the method where you perform desired operations on the new data. You get the data back asDataSnapshot.DataSnapshotcontains all the data from a specific location in the database.DataSnapshotis just an immutable copy of your database data so can’t use it to modify the data in the database. - By calling the
exists()method onDataSnapshotyou check if the snapshot contains a non-null value. If there is data in the snapshot you get all of the direct children of the snapshot and you map each one to thePostobject by calling thegetValue(Post::class.java)method on a child.getValue(Post::class.java)wraps the data to the specifiedPostclass and returns an instance of the passed in class or null if there is no data in this location. Then you addPostinstances to the list and you set this list to theLiveDatafield created earlier which will notify all active observers about new data. - If data doesn’t exist you set empty list as the new value of
LiveData. This is needed in the case where all posts get deleted and the database is empty. In that case,dataSnapshot.exists()will returnfalseand by setting empty list as the new value you’ll reflect that. - You attach the listener to the
POSTS_REFERENCEbecause that is the location from where you want to listen for changes.
Now add onPostsValuesChange() function which just calls the function that attaches the listener and returns LiveData field:
fun onPostsValuesChange(): LiveData<List<Post>> {
listenForPostsValueChanges()
return postsValues
}
You only want to listen for posts updates when you’re on the home screen. Once you navigate away from home screen you don’t care about posts updates anymore. To achieve that you need to remove event listener when you’re no longer interested in the events. Add removePostsValuesChangesListener() function:
fun removePostsValuesChangesListener() {
database.getReference(POSTS_REFERENCE).removeEventListener(postsValueEventListener)
}
This method removes the passed in event listener, by calling removeEventListener function, from the specified location.
Open HomeActivity and add onPostsUpdate(posts: List<Post>) function which will get called every time posts update and it will set new data to the recycler view adapter:
private fun onPostsUpdate(posts: List<Post>) {
feedAdapter.onFeedUpdate(posts)
}
Now implement listenForPostsUpdates() function which will listen for the changes in the posts and will call onPostsUpdate() on every update. Replace the //TODO comment with the following :
realtimeDatabaseManager.onPostsValuesChange()
.observe(this, Observer(::onPostsUpdate))
Finally, override onStop() method and call the realtimeDatabaseManager.removePostsValuesChangesListener() to stop listening for the posts updates:
override fun onStop() {
super.onStop()
realtimeDatabaseManager.removePostsValuesChangesListener()
}
Build and run your app. You should see the post that you previously added on the home screen:
Build and run your app once again on a different device and log in with a different account. Add a new post and observe on your first device how the data is updated in the realtime.
Updating and deleting data
Tap on a post on the home screen to open another screen which shows post details:
On this screen, you can edit your post by taping on it. When you’re done you can tap an Update button to update the post content. By tapping Delete button you can delete the post if you’re the author of the post. There’s also a Comments section here. The app also has the feature of adding a comment to the post which will be displayed here. If you try to tap to any of these buttons you’ll see that nothing happens. You’ll implement those functionalities next.
Updating
Updating data in Realtime Database is almost the same as writing. You use the same setValue() method for updating. Add POST_CONTENT_PATH constant above RealtimeDatabaseManager class declaration:
private const val POST_CONTENT_PATH = "content"
You’ll use this constant to indicate which field in the database you want to update.
Now add the updatePostContent function:
fun updatePostContent(key: String, content: String) {
//1
database.getReference(POSTS_REFERENCE)
//2
.child(key)
//3
.child(POST_CONTENT_PATH)
//4
.setValue(content)
}
- First, you get a reference to the location of the posts in the database.
- Here you use the key to access the location of the post you wan’t to update.
- You can create a new object and write the entire object to this location but that is not needed. You can update a specific field in the post by specifying the path to that field. Here you update only the content of the post.
- Finally, you call
setValuemethod with new content to update the content of the post.
Open PostDetailsActivity class and replace TODO in updatePostButton.setOnClickListener method in the initializeClickListener with this:
realtimeDatabaseManager.updatePostContent(post.id, postText.text.toString().trim())
finish()
When the user taps on the Update button the post content will update and the current activity will close.
Build and run your app. Open any post in the list that was written by you, update the post content, tap the Update button and verify both on the home screen and firebase console that post content is updated.
Deleting
Deleting data in Realtime Database is very simple. You have two options. You can delete data by using setValue method and specify null as an argument or you can use removeValue() method which will set the value at the specified location to null. You’ll use the latter approach. Open RealtimeDatabaseManager class and add the deletePost function:
fun deletePost(key: String) {
database.getReference(POSTS_REFERENCE)
.child(key)
.removeValue()
}
Here you get a reference to the location of the posts, then you get the reference to the desired posts and call removeValue() to delete it.
Open PostDetailsActivity and navigate to the initializeClickListener() function and replace TODO in on click listener of the deletePostButton with this:
realtimeDatabaseManager.deletePost(post.id)
finish()
Build and run your app. Open any post in the list that was written by you and click the delete button. Verify on the home screen and firebase console that the post is deleted.
Querying and filtering data
To show how to query data you’ll add another feature to the app. You’ll enable users to add comments to the post.
Open RealtimeDatabaseManager class and add two more constants above class declaration:
private const val COMMENTS_REFERENCE = "comments"
private const val COMMENT_POST_ID_PATH = "postId"
COMMENTS_REFERENCE is used to refer to the location of comments and COMMENT_POST_ID_PATH is used when building a query. You’ll do that in a bit.
Next, add commentsValues and commentsValueEventListener fields to the RealtimeDatabaseManager class:
private val commentsValues = MutableLiveData<List<Comment>>()
private lateinit var commentsValueEventListener: ValueEventListener
Add the createComment function which is just a helper function for building a Comment instance:
private fun createComment(postId: String, content: String): Comment {
val user = authenticationManager.getCurrentUser()
val timestamp = getCurrentTime()
return Comment(postId, user, timestamp, content)
}
Now add the addComment function:
fun addComment(postId: String, content: String) {
val commentsReference = database.getReference(COMMENTS_REFERENCE)
val key = commentsReference.push().key ?: ""
val comment = createComment(postId, content)
commentsReference.child(key).setValue(comment)
}
This function saves the comment to the database. It uses the same logic as the post saving function so you should be familiar with this by now.
Now open the PostDetailsActivity class, navigate to the initializeClickListener function and replace the TODO inside the addCommentButton click listener with the following:
val comment = commentEditText.text.toString().trim()
if (comment.isNotEmpty()) {
realtimeDatabaseManager.addComment(post.id, comment)
commentEditText.text.clear()
} else {
showToast(getString(R.string.empty_comment_message))
}
Here you get the text from the edit text and if it is not empty you save the comment to the database. Build and run your app. Tap on any post from the list, add a comment in the edit text and click Add comment button.
Edit text gets cleared but nothing happens on the UI. Go to the Firebase console. You will see there that comment is saved to the database.
You can see there that comment has a postId child. This is how you’ll know to which post comment belongs.
Now you can add logic for reading the comments from the database.
Open RealtimeDatabaseManager class again and add the listenForPostCommentsValueChanges function:
private fun listenForPostCommentsValueChanges(postId: String) {
commentsValueEventListener = object : ValueEventListener {
override fun onCancelled(databaseError: DatabaseError) {
/* No op */
}
override fun onDataChange(dataSnapshot: DataSnapshot) {
if (dataSnapshot.exists()) {
val comments = dataSnapshot.children.mapNotNull { it.getValue(Comment::class.java) }.toList()
commentsValues.postValue(comments)
} else {
commentsValues.postValue(emptyList())
}
}
}
database.getReference(COMMENTS_REFERENCE)
//1
.orderByChild(COMMENT_POST_ID_PATH)
//2
.equalTo(postId)
.addValueEventListener(commentsValueEventListener)
}
This function listens for comments value updates and it is very similar to the listenForPostsValueChanges, but there are two differences:
- The
orderByChildmethod returns aQueryinstance where children are ordered by thepostIdvalue. A query is a request for data or information from a database.Queryclass is used for reading data and it has many useful methods that allow you to fetch the data in a way you want. You can filter data by some criteria, sort data, limit, etc. Check the official documentation (https://firebase.google.com/docs/reference/android/com/google/firebase/database/Query) for theQueryclass to see what it offers. - The
equalTomethod returns aQueryinstance which contains child nodes only where the node value is equal to the specified function argument. In this case, it will return a query with the comments for the specific post.
The rest of the code is the same as the code for listening for post updates.
Next, add a deletePostComments function, which will delete all of the comments for the specific post:
private fun deletePostComments(postId: String) {
database.getReference(COMMENTS_REFERENCE)
.orderByChild(COMMENT_POST_ID_PATH)
.equalTo(postId)
.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onCancelled(databaseError: DatabaseError) {
/* No op */
}
override fun onDataChange(dataSnapshot: DataSnapshot) {
dataSnapshot.children.forEach { it.ref.removeValue() }
}
})
}
It uses exactly the same logic for fetching the comments for the specific post as the listenForPostCommentsValueChanges function and you’re already familiar with how to delete data from the database. Call this function from the deletePost function passing in the key of the post. This makes sure that when a post gets deleted, its comments get deleted as well.
Next, add an onCommentsValuesChange function which starts listening for comments updates and returns a LiveData object:
fun onCommentsValuesChange(postId: String): LiveData<List<Comment>> {
listenForPostCommentsValueChanges(postId)
return commentsValues
}
Now, open the PostDetailsActivity class again, navigate to the listenForComments function and replace its TODO comment with the following:
realtimeDatabaseManager.onCommentsValuesChange(post.id)
.observe(this, Observer(::onCommentsUpdate))
This just starts listening for the comments update. Note the Observer in this case requires you to import androidx.lifecycle.Observer.
Next, back in the RealtimeDatabaseManager class, add a function for removing the comments listener:
fun removeCommentsValuesChangesListener() {
database.getReference(COMMENTS_REFERENCE).removeEventListener(commentsValueEventListener)
}
In PostDetailsActivity, override onStop and call realtimeDatabaseManager.removeCommentsValuesChangesListener() to remove the comments listener when you no longer want to listen for comment updates.
override fun onStop() {
super.onStop()
realtimeDatabaseManager.removeCommentsValuesChangesListener()
}
Build and run your app. Navigate to the same post you added a comment to, earlier.
Now you can see your comment on the UI as well. Add more comments from the same and from the different account to see how comments are updated in real time.
Other features
Transactions
Realtime Database also allows you to write data to the database using a transaction. A database transaction is a unit of work that is independently executed and it must be atomic, consistent, isolated and durable. If the WhatsUp app had a feature to allow you to “like” a post you could use transactions to keep track how many likes a given post had. Since there is a use case where multiple users could “like” the post at the same time, the transaction would allow you to always have fresh and correct data about likes.
Check the official documentation https://firebase.google.com/docs/database/android/read-and-write#save_data_as_transactions to see how to work with transactions.
Listening for child events
Previously you saw how to use value event listener. Often you’ll also need to know about changes in children of a specific node. In that case, you’ll need to use a child event listener. Child event listeners notifiy the app when child nodes are added, deleted or moved within a parent node. To add a child event listener you’ll need to call addChildEventListener() on a database reference instance, and there are four methods that you’ll need to implement. Check the official documentation https://firebase.google.com/docs/database/android/lists-of-data#child-events to learn more about child events.
Indexing
There can be a performance issue if your app frequently queries the database. To improve query performance you should consider defining indexing rules. A database index is a data structure which is used to quickly locate and access the data in a database.
You can learn more about indexing data in the official documentation https://firebase.google.com/docs/database/security/indexing-data.
Key points
-
FirebaseDatabaseobject is the main entry point to the database -
DatabaseReferenceclass represents a particular location in the database and it is used to refer to the location in the database to which you want to write to or read from. -
push()method is used to create an empty node with an auto-generated key. - Firebase Realtime Database has several types of listeners, and each listener type has a different kind of callback.
-
ValueEventListenerlistens for data changes to a specific database reference. -
ChildEventListenerlistens for changes to the children of a specific database reference. - You need to decide how to handle listeners when the user is not actively interacting with the app. In most cases, you want to stop listening for updates. To do that you need to remove the listener.
- For updating data in Realtime Database, the
setValue()method is used. - You can delete data by using the
setValuemethod and specifynullas an argument or you can useremoveValue()method which will set the value at the specified location tonull. - A query is a request for data or information from a database.
Queryclass is used for reading data and it has many useful methods that allow you to fetch the data in a way you want. - A database transaction is a unit of work that is independently executed and it must be atomic, consistent, isolated and durable.
- To improve query performance you should consider defining indexing rules.
Where to go from here?
You covered a lot in this chapter. You have seen how to write data to the Realtime Database, how to listen for changes in the database and how to update and delete data. It takes a little bit of practice to get used to working with Realtime Database so feel free to play a bit with the current app. To see specifics about each method, what it does and how it does it, you can visit the official Firebase documentation to find out.
WhatsUp app works great for now, but what if you were using it in a place where the Internet connection is bad? What if you started uploading data and you lost internet connection in the process? Can you write data to the database if you’re offline? The good news is that Realtime Database provides great offline support. In Chapter 14, “Realtime Database offline capabilities” you’ll learn how Firebase handles all of the mentioned cases. You’ll make your WhatsUp app to work seamlessly offline and you’ll learn what happens under the hood that makes that possible.