Chapters

Hide chapters

Saving Data on Android

First Edition · Android 10 · Kotlin 1.3 · AS 3.5

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Using Firebase

Section 3: 11 chapters
Show chapters Hide chapters

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:

  1. Create a project in the Firebase console.
  2. Enable Google sign-in.
  3. Set security rules to the test mode to allow everyone read and write access.
  4. 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:

  1. DatabaseReference class 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 added POSTS_REFERENCE constant earlier. You pass that constant to the getReference() and this returns a reference for the provided path. Now you can use postsReference to read or write data to this location.
  2. Posts will be added as a child of the posts node. 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 the push() method to create an empty node with an auto-generated key. The push() method returns a database reference to the newly created node. You can call getKey() on the database reference to get the key to that reference. Next, you create a Post instance that you’ll save to the database and you store the key of that post so you can refer to it later.
  3. 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 the setValue(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>, and List<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 attach OnSuccessListener that gets called if the post is saved to the database successfully, and OnFailureListener that 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)
}
  1. You add ValueEventListener as an anonymous inner class and you assign it to postsValueEventListener field. There are two methods that you need to implement.
  2. 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. databaseError parameter contains more information about an error that occurred. In this case, you won’t do anything if reading gets canceled.
  3. 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 as DataSnapshot . DataSnapshot contains all the data from a specific location in the database. DataSnapshot is just an immutable copy of your database data so can’t use it to modify the data in the database.
  4. By calling the exists() method on DataSnapshot 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 the Post object by calling the getValue(Post::class.java) method on a child. getValue(Post::class.java) wraps the data to the specified Post class and returns an instance of the passed in class or null if there is no data in this location. Then you add Post instances to the list and you set this list to the LiveData field created earlier which will notify all active observers about new data.
  5. 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 return false and by setting empty list as the new value you’ll reflect that.
  6. You attach the listener to the POSTS_REFERENCE because 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)
}
  1. First, you get a reference to the location of the posts in the database.
  2. Here you use the key to access the location of the post you wan’t to update.
  3. 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.
  4. Finally, you call setValue method 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:

  1. The orderByChild method returns a Query instance where children are ordered by the postId value. A query is a request for data or information from a database. Query class 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 the Query class to see what it offers.
  2. The equalTo method returns a Query instance 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

  • FirebaseDatabase object is the main entry point to the database
  • DatabaseReference class 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.
  • ValueEventListener listens for data changes to a specific database reference.
  • ChildEventListener listens 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 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.
  • A query is a request for data or information from a database. Query class 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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.