14.
Reading to & Writing from Realtime Database
Written by Fuad Kamal
In the last chapter, you integrated Realtime Database into your app. You added Firebase SDK to your app and connected the app with the Firebase project. You also learned about database rules and you set them up to allow only authenticated users 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 and 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 12: “Firebase Overview” and Chapter 13: “Introduction to Firebase Realtime Database.”
Reading and writing data
Open the 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 Sign in with Google 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 POST.
As you can see nothing happens yet. Next, you’ll add the logic for saving the post to the database.
Saving data to the database
Open RealtimeDatabaseManager.kt. Add the following line to the class:
private val database = FirebaseDatabase.getInstance()
The database object is the main entry point to the database. getInstance() gets you the default FirebaseDatabase instance. There are overloads of getInstance() if you want to get the database for the specific URL or specific app.
Next, you need to create an actual object which will contain data. Inside the class, add the code for creation of the Post object:
private fun createPost(key: String, content: String): Post {
val user = authenticationManager.getCurrentUser()
val timestamp = getCurrentTime()
return Post(key, content, user, timestamp)
}
This function uses AuthenticationManager to get the current logged in user name, gets the current time and then returns the newly created Post instance.
Now that you have a post object, you need to store it. But first, add a constant above the class declaration:
private const val POSTS_REFERENCE = "posts"
You’ll use this constant in retrieving reference from the database. Now, 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:
-
The
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()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 added thePOSTS_REFERENCEconstant earlier. You pass that constant togetReference()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 that 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 the previous value on that path. You don’t want that. You can usepush()to create an empty node with an auto-generated key. This 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
child()that returns a reference to the location relative to the calling reference. Finally, you usesetValue()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 AddPostActivity.kt and replace the TODO in 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. Otherwise, you show an error message.
Build and run your app. Click on the floating action button on the home screen, add some text and tap Post. Current activity gets closed and the home screen is shown. You’ll fix that in a bit. After you reopen the app, the added post will be displayed on the home screen.
To make sure your post is saved to the database, open the database in the Firebase console and you’ll see your post here along with additional 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.kt and at the top of the class add:
private val postsValues = MutableLiveData<List<Post>>()
You’ll use LiveData to notify the observers about post changes.
Next, below the previous line add:
private lateinit var postsValueEventListener: ValueEventListener
This is 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. -
onCancelled(databaseError: DatabaseError)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.databaseErrorcontains 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
exists()on theDataSnapshotobject you 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 callinggetValue(Post::class.java)on a child.getValue(Post::class.java)wraps the data to the specifiedPostclass and returns an instance of the passed in class ornullif 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 an 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
POSTS_REFERENCEbecause that is the location from where you want to listen for changes.
Now inside the class, add onPostsValuesChange():
fun onPostsValuesChange(): LiveData<List<Post>> {
listenForPostsValueChanges()
return postsValues
}
This method just calls the function that attaches the listener and returns LiveData.
You only want to listen for posts updates when you’re on the home screen. Once you navigate away from the 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 this code inside the slass:
fun removePostsValuesChangesListener() {
database.getReference(POSTS_REFERENCE).removeEventListener(postsValueEventListener)
}
This method removes the passed in event listener, by calling removeEventListener(), from the specified location.
Open HomeActivity.kt and add the following method:
private fun onPostsUpdate(posts: List<Post>) {
feedAdapter.onFeedUpdate(posts)
}
This method will get called every time posts update and it will set new data to the RecyclerView adapter.
Now, find listenForPostsUpdates() and replace the TODO with the following:
realtimeDatabaseManager.onPostsValuesChange()
.observe(this, Observer(::onPostsUpdate))
This code enables listening for the changes in the posts. On every update, the system will call onPostsUpdate().
Finally, override onStop():
override fun onStop() {
super.onStop()
realtimeDatabaseManager.removePostsValuesChangesListener()
}
Once the activity stops, it also stops listening for the posts updates.
Build and run. You’ll see the post that you previously added on the home screen.
Build and run your app 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 to open another screen that shows post details.
On this screen, you can edit your post by taping on its text. When you’re done you can tap UPDATE the post content. By tapping Delete 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 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 method for updating - setValue() . Above the RealtimeDatabaseManager class declaration, add next constant:
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 following to the class:
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 want to update.
- You can create a new object and write the entire object to this location but that isn’t 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
setValue()with new content to update the content of the post.
Now, use this method in PostDetailsActivity.kt. Replace the TODO in updatePostButton.setOnClickListener() in the initializeClickListener() with this:
realtimeDatabaseManager.updatePostContent(post.id, postText.text.toString().trim())
finish()
When the user taps UPDATE the post content will change and the current activity will close.
Build and run. Open any post in the list that was written by you, update the post content, tap UPDATE 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() and specify null as an argument or you can use removeValue() which will set the value at the specified location to null. You’ll use the latter approach.
Open RealtimeDatabaseManager.kt and add the deleting post 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 post and call removeValue() to delete it.
Open PostDetailsActivity.kt, navigate to initializeClickListener() and replace the TODO in deletePostButton.setOnClickListener() 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.kt and add two more constants above the 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 class:
private val commentsValues = MutableLiveData<List<Comment>>()
private lateinit var commentsValueEventListener: ValueEventListener
Now, add 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)
}
When adding Comment import, use com.raywenderlich.android.whatsup.model.Comment. With this method you create a Comment object in the same way as you created your first Post ` object.
Now, add the following 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.
Next, in PostDetailsActivity.kt, navigate to initializeClickListener() and replace the TODO inside addCommentButton.setOnClickListener() 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 EditText and, if it’s not empty, you save the comment to the database.
Build and run. Open details of any post from the list, add a comment and click ADD COMMENT.
EditText gets cleared but nothing happens on the UI. Go to the Firebase console. You’ll 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.
Finally, you can add logic for reading the comments from the database.
Open RealtimeDatabaseManager.kt again and add this code:
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’s very similar to listenForPostsValueChanges(), but there are two differences:
-
orderByChild()returns aQueryinstance where children are ordered by thepostIdvalue. A query is a request for data or information from a database. TheQueryclass 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) to see what it offers. -
equalTo()returns aQueryinstance that 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 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 listenForPostCommentsValueChanges() and you’re already familiar with how to delete data from the database.
Call this function from the bottom of deletePost() passing in the key of the post:
deletePostComments(key)
This makes sure that when a post gets deleted, its comments get deleted as well.
Next, add onCommentsValuesChange() which starts listening for comments updates and returns a LiveData object:
fun onCommentsValuesChange(postId: String): LiveData<List<Comment>> {
listenForPostCommentsValueChanges(postId)
return commentsValues
}
Now, open PostDetailsActivity.kt again, navigate to listenForComments() and replace the TODO with the following:
realtimeDatabaseManager.onCommentsValuesChange(post.id)
.observe(this, Observer(::onCommentsUpdate))
Observer in this case requires you to import androidx.lifecycle.Observer. This just starts listening for the comments update.
Next, back in RealtimeDatabaseManager.kt, add a function for removing the comments listener:
fun removeCommentsValuesChangesListener() {
database.getReference(COMMENTS_REFERENCE).removeEventListener(commentsValueEventListener)
}
Finally, in PostDetailsActivity.kt, 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. 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 of 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 notify 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 that 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
- The
FirebaseDatabaseobject is the main entry point to the database -
DatabaseReferencerepresents 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()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, use
setValue(). - You can delete data by using
setValue()and specifynullas an argument or you can useremoveValue()which will set the value at the specified location tonull. - A query is a request for data or information from a database. The
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 15, “Realtime Database Offline Capabilities”, you’ll learn how Firebase handles all of the mentioned cases. You’ll make your WhatsUp app work seamlessly offline and you’ll learn what happens under the hood that makes that possible.