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

18. Reading Data from Cloud Firestore
Written by Dean Djermanović

In the previous chapter, you learned how to write data to the Firestore, and how to update or delete data from Firestore by implementing that functionality in the WhatsUp app. You also became familiar with the Firebase console and learned how to use it for managing data.

In this chapter, you’ll continue working on your app. Since now you still don’t have any data on the home screen when you run the app, you’ll focus on implementing reading logic. In the process, you’ll learn how to read data from the Firestore, how to listen for updates in real-time, and how queries work.

Setting up Firebase

If you skipped previous chapters, you need to setup 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.”

Be sure to use the starter project from this chapter, by opening the reading-data-from-cloud-firestore folder and its starter project from the projects folder, rather than continuing with the final project you previously worked on. It has a few things added to it, including placeholders for the code to add in this chapter.

Reading data

Like the Realtime Database, Firestore allows to read data once, or to listen for data changes in real-time.

To get the data once, you need to call get() on the collection reference from which you want to read the data or you can also use get() on the document reference, if you need to read data from a specific document. For example, this is how you’d read the data from the posts collection:

database.collection("posts")
    .get()
    .addOnSuccessListener { result ->
      ...
    }
    .addOnFailureListener { exception ->
      ...
    }

Since getting the data is asynchronous, you need to attach a listener that will notify you when the data fetching is complete. It returns the data as a QuerySnapshot. QuerySnapshot is a class that contains the results of a query and can contain QueryDocumentSnapshot objects if they are available. A QueryDocumentSnapshot contains data read from a document in your Firestore database. You’ll see an example of this shortly.

Since you want that your app always has the latest data you won’t fetch the data only once. Instead, you’ll implement a listener, so you can receive events when the data changes.

Listening for data changes

If you don’t have the project open by now, make sure to open it, and head over to CloudFirestoreManager.kt. Add a postsRegistration field like this:

private lateinit var postsRegistration: ListenerRegistration

ListenerRegistration represents a subscription of sorts, for a database reference, when you attach a listener to the reference. You’ll use it to assign a posts listener to it and to remove it when needed, to clean up your code, and to stop receiving changes.

Next, navigate to listenForPostsValueChanges(). Replace the TODO inside the fuction with the following implementation:

private fun listenForPostsValueChanges() {
  // 1
  postsRegistration = database.collection(POSTS_COLLECTION) // 2
    // 3
    .addSnapshotListener(EventListener<QuerySnapshot> { value, error ->
      // 4
      if (error != null || value == null) {
        return@EventListener
      }

      // 5
      if (value.isEmpty) {
        // 6
        postsValues.postValue(emptyList())
      } else {
        // 7
        val posts = ArrayList<Post>()
        // 8
        for (doc in value) {
          // 9
          val post = doc.toObject(Post::class.java)
          posts.add(post)
        }
        // 10
        postsValues.postValue(posts)
      }
    })
}
  1. You assign the listener to postsRegistration, by attaching an EventListener to the database collection.
  2. You receive the database collection by calling database.collection(POSTS_COLLECTION).
  3. You call addSnapshotListener() on the collection reference which starts listening for the data changes at its location.
  4. When onEvent() is called you first check if an error occurred by checking if the error argument is not null, or if the value is null. If it is you’ll just return from the function, since you cannot consume the event.
  5. Check if the received data actually contains documents - it isn’t empty.
  6. If the value is empty, it means no posts are available, and you simply communicate it by updating the postsValues with an emptyList().
  7. If there are values, instantiate an ArrayList to store them.
  8. Iterate through each document in the value snapshot.
  9. Parse each document as a Post, and add it to posts.
  10. Update the postsValues with parsed posts.

To read the data, you pass in the listener of type EventListener that will be called whenever data changes or if an error occurs. EventListener is a generic interface that is used for any type of event listening, and contains only the onEvent(T value, FirebaseFirestoreException error) function that you’re required to implement. onEvent() will be called with the new value or the error if an error occurred. You get the result data as a QuerySnapshot object. This is the easiest way to communicate data changes or errors, and since it’s generic, it can work for any collection.

Open HomeActivity and navigate to the listenForPostsUpdates function. Replace the TODO inside the function with the following:

private fun listenForPostsUpdates() {
  cloudFirestoreManager.onPostsValuesChange()
      .observe(this, Observer(::onPostsUpdate))
}

When new data is received you’ll reflect that, by displaying it on the screen.

You also need to remove the listener when you no longer want to receive data change events. To do that, open CloudFirestoreManager.kt and navigate to stopListeningForPostChanges() and replace the function code with the following:

fun stopListeningForPostChanges() = postsRegistration.remove()

By calling remove method on a ListenerRegistration object you remove the listener from the locatin that this listener is assigned to.

Now go back to the HomeActivity and override onStop method and call stopListeningForPostChanges from there, like this:

override fun onStop() {
    super.onStop()
    cloudFirestoreManager.stopListeningForPostChanges()
}

Build and run your app. You should see the posts now on the home screen:

You can test the real-time updates by deleting the post directly from the console. You should see how the change is reflected in the app almost instantly.

Performing queries

Sometimes, you don’t want to read just the documents of certain collections. Sometimes you need to filter out, match by values, or simply skip a certain amount of documents. To do this, you use database queries. Since you don’t have any nested documents in the posts collection, let’s add something to make it a bit more complex.

Adding comments

There’s one more feature that you have in the Realtime Database version of the WhatsUp app that’s you didn’t add, and that is the ability to add a comment to the post. You’ll add that now and you’ll use that feature to see how the queries are performed.

Open CloudFirestoreManager.kt, once again, and navigate to addComment(). Replace the TODO, inside the function, with the following code:

// 1
val commentReference = database.collection(COMMENTS_COLLECTION).document()

// 2
val comment = HashMap<String, Any>()

// 3
comment[AUTHOR_KEY] = authenticationManager.getCurrentUser()
comment[CONTENT_KEY] = content
comment[POST_ID] = postId
comment[TIMESTAMP_KEY] = getCurrentTime()

// 4
commentReference
    .set(comment) // 5
    .addOnSuccessListener { onSuccessAction() } // 6
    .addOnFailureListener { onFailureAction() } // 7

The logic for adding the comment to the database is exactly the same as with adding posts so you should understand what happens in the code above by now, but to sum it up, here’s what happens:

  1. Create a new document in Firestore, and store its reference.
  2. Create a HashMap<String, Any>, to store the comment data.
  3. Store the data in comment.
  4. Use the commentReference to communicate the save operation.
  5. Set the reference value to the new comment.
  6. In case of a Success, call the onSuccessAction.
  7. In case something goes wrong - a Failure occured, call the onFailureAction.

Open PostDetailsActivity.kt, navigate to initializeClickListener(), and replace the TODO inside addCommentButton click listener with a call to addComment():

...
addCommentButton.setOnClickListener {
  val comment = commentEditText.text.toString().trim()
  if (comment.isNotEmpty()) {
    cloudFirestoreManager.addComment(
      post.id,
      comment, 
      ::onCommentSuccessfullyAdded, 
      ::onCommentAddFailed
      )
  } else {
    showToast(getString(R.string.empty_comment_message))
  }
}

This will save the comment to the database on Add Comment button click.

Build and run your app. Tap on any post in the list. Enter some text into the comments EditText and tap the Add Comment button:

Your comment is now saved to the database. Open the database in the console to confirm that. You should see your comment there:

Listening for comments

You can add comments to the database now, but you still can’t read them. Since comments are stored in a separate collection from posts, to read them you’ll need to write a query that returns comments for the specific post. Every comment document has a post_id property that indicates to which post the comment belongs to.

Open CloudFirestoreManager.kt. Add a commentsRegistration field:

private lateinit var commentsRegistration: ListenerRegistration

You’ll use this field to assign the comments listener to it and to remove the listener when needed, just like before.

Next, navigate to listenForPostCommentsValueChanges(). Replace the TODO inside the function with the following:

// 1
commentsRegistration = database.collection(COMMENTS_COLLECTION)
    // 2
    .whereEqualTo(POST_ID, postId) // 3
    // 4
    .addSnapshotListener(EventListener<QuerySnapshot> { value, error ->
      if (error != null || value == null) {
        return@EventListener
      }

      if (value.isEmpty) {
        postsValues.postValue(emptyList())
      } else {
        val comments = ArrayList<Comment>()
        for (doc in value) {
          val comment = doc.toObject(Comment::class.java)
          comments.add(comment)
        }
        commentsValues.postValue(comments)
      }
    })
  1. As before, you assing a listener to the comments reference.
  2. You use whereEqualTo method to create the query that filters the documents in the collection that contain the specified field and value in that field.
  3. Pass in the POST_ID that represents the post_id property and the postId that represents the value for comparison. This query will only return the documents that belong to the specified post. whereEqualTo method returns a Query that you can read or listen to.
  4. Once again, attach an EventListener and parse the Comments, if there are any, updating the UI when you’re done.

Next, open PostDetailsActivity.kt and navigate to listenForComments(). Replace the TODO inside the function with the following:

cloudFirestoreManager.onCommentsValuesChange(post.id)
    .observe(this, Observer(::onCommentsUpdate))

Here, you start listening for the comments changes for that particular post and when the data changes you update the UI.

Go back to the CloudFirestoreManager.kt class and navigate to stopListeningForCommentsChanges(). Replace the function with the following:

fun stopListeningForCommentsChanges() = commentsRegistration.remove()

Here, you remove the listener from the location that you assigned to the commentsRegistration.

Open PostDetailsActivity.kt class and override onStop(). Remove the comments listener from this method because this is the point where you’re no longer interested in the comments changes:

override fun onStop() {
    super.onStop()
    cloudFirestoreManager.stopListeningForCommentsChanges()
}

If you build and run the code now, you should see the comment appear, in the post details section of the app.

Deleting comments

One last thing that you need to add is the ability to delete the comments. You’ll delete the comments for the particular posts when that post is deleted.

Open CloudFirestoreManager.kt and navigate to deletePostComments(). Replace the TODO inside the function with the following:

// 1
database.collection(COMMENTS_COLLECTION)
    .whereEqualTo(POST_ID, postId)
    //2
    .get()
    //3
    .continueWith { task -> task.result?.documents?.forEach { it.reference.delete() } }
  1. First, get a reference to the comments collection and filter the comments that belong to the specific post.
  2. Call get() to retrieve the filtered-by-post comments, asynchronously.
  3. After comments are loaded, delete them from the database, one by one.

continueWith() on a Task instance returns a new Task that will be completed with the result of applying the specified Continuation to this Task. A Continuation is a function that is called to continue execution after completion of a Task. When the comments for the specific posts are fetched, you delete them by traversing the result documents and calling delete method on each document reference. // TODO FPE - Should we try to separate and clarify this more? And how?

Finally, call deletePostComments() from deletePost(), to delete the comments tied to that post, when thte post is deleted:

fun deletePost(key: String, onSuccessAction: () -> Unit, onFailureAction: () -> Unit) {
    ...
    deletePostComments(key)
}

Build and run your app. Open the post that you added a comment to before. You’ll now see that your comment is displayed, like before. Add another comment and you’ll see that it’s displayed on the screen immediately.

Open the database console. Now, delete the posts that you added comments to and observe the database in the console. You’ll notice that comments for that post are deleted, as well.

Working offline

Like Realtime Database, Firestore can also work offline. Cloud Firestore stores a copy of data that your app is using, locally, so that you can have access to the data if the device goes offline. You can perform operations like reading, writing and querying on the local copy. When your device goes back online, Firestore automatically syncs the data with the data that is stored remotely!

Firestores offline persistence is enabled by default for mobile clients. You can test this in your WhatsUp app.

Build and run your app. Add some posts if you don’t have already. You can add some comment to that post as well if you like. Now disconnect the device from the network and kill the process of your app. Start your app again and you’ll notice that your data is still displayed on the screen.

Now add another post. Tap on the floating action button on the home screen and enter some content for the post and tap the Post button. Nothing happens because you only consider post saved when it is saved to the remote database.

Go back to the home screen by tapping the system back button. You’ll see your post that you added while offline is displayed on the home screen. This is because it was saved to the local cache. If you open the console and look into the database you won’t see that post in the database.

Now connect your device back to the network. You’ll get a toast message on the device that the post is saved an now you can see your post in the remote database.

If you don’t want to have the offline feature enabled, you can disable it when initializing Cloud Firestore.

Check the official documentation to learn more about offline support.

Other features

Cloud Firestore has many other features. You’ll go through some of them next.

Ordering and limiting

You’ve already seen how you can specify which documents you want to fetch from the collection by using whereEqualTo(). But there’s much more you can do, on top of the whereEqualTo():

  • You can use orderBy() on the collection reference to sort the data by the specified field. By default, the documents are sorted in ascending order by document ID.
  • You can use limit() on the collection reference to only return up to the specified number of documents.
  • You can also combine all of the where methods for filtering with limit() and orderBy().

Pagination

You can have a lot of data stored in your database, but you probably don’t need all of the data all the time. Pagination allows you to split your database data into chunks so that you don’t need to fetch all of it at once.

Firestore provides you with the pagination feature, that works in a way where you don’t need to execute one large query, but instead multiple smaller queries sequentially. Firestores library has some useful methods that you can use to divide your query into smaller queries, like startAt(), startAfter(), endAt() or endBefore().

Check the official documentation to learn more about pagination.

Indexing

To ensure good performance for every query, Firestore requires an index. Firestore automatically creates indices for the basic queries for you.

Check the official documentation to learn more about how to add indexing manually and how it works.

Key points

  • Firestore allows to read data once or to listen for data changes in real-time.

  • To get the data once, you would need to use get method on the collection reference.

  • ListenerRegistration interface represents a Firestore subscription listener.

  • You can call addSnapshotListener() on a collection reference to start listening for data changes at a specific location.

  • Queries are used to get only a subset of the documents within a collection.

  • Cloud Firestore stores a copy of data that your app is using, locally, so that you can access the data, if the device goes offline.

  • You can also use orderBy() and limit(), on the collection reference, to get only specific documents from a collection.

  • Pagination allows you to split your database data into chunks so that you don’t need to fetch all your data at once.

  • To ensure good performance for every query, Firestore requires an index, when creating them.

Where to go from here?

You covered a lot in this chapter. You learned how to read data from Firestore and listen for data changes in real-time. You also learned what queries are and how to use them only to fetch specific documents from a collection.

To learn more about those features, you can check out the official documentation.

WhatsUp app is now complete, but it has one big flaw. Anyone can read and write the data to the database. In “Chapter 19: Securing data in Cloud Firestore” you’ll learn how to secure the data in the database and to restrict access to the data.

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.