Chapters

Hide chapters

Saving Data on Android

Second Edition · Android 11 · Kotlin 1.5 · Android Studio 4.2

Using Firebase

Section 3: 11 chapters
Show chapters Hide chapters

19. Reading Data from Cloud Firestore
Written by Harun Wangereka

In the previous chapter, you learned how to write data to 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 Firestore, how to listen for updates in real-time, and how queries work.

Setting up Firebase

If you skipped previous chapters, you need to set up Firebase 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 12: “Firebase Overview” and Chapter 13: “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 your app to always have the latest data, you won’t fetch the data only once. Instead, you’ll put in place 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 this field below commentsValues variable:

private lateinit var postsRegistration: ListenerRegistration

Add the IDE import when the IDE prompts you. 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.

Still inside CloudFirestoreManager.kt , navigate to listenForPostsValueChanges(). Replace TODO inside the function with the following implementation:

// 1
postsRegistration = database.collection(POSTS_COLLECTION)
    // 2
    .addSnapshotListener(EventListener { value, error ->
        // 3
        if (error != null || value == null) {
            return@EventListener
        }
        // 4
        if (value.isEmpty) {
            // 5
            postsValues.postValue(emptyList())
        } else {
            // 6
            val posts = ArrayList<Post>()
            // 7
            for (doc in value) {
                // 8
                val post = doc.toObject(Post::class.java)
                posts.add(post)
            }
            // 9
            postsValues.postValue(posts)
        }
    })

Here’s a breakdown of the code above:

  1. You assign the listener to postsRegistration variable that you’ve declared, by attaching an EventListener to the database collection. You receive the database collection by calling database.collection(POSTS_COLLECTION).

  2. You call addSnapshotListener() on the collection reference which starts listening for the data changes at its location.

  3. 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 return from the function since you cannot consume the event.

  4. You check if the received data actually contains documents - it isn’t empty.

  5. You update the postsValues with emptyList() when the value is empty. Which means no posts are available.

  6. If there are values, instantiate an ArrayList to store them.

  7. Iterate through each document in the value snapshot.

  8. Parse each document as a Post, and add it to posts.

  9. Update the postsValues with parsed posts.

To resolve the import error add this import for EventListener class in your imports

import com.google.firebase.firestore.EventListener

To read the data, you pass in the listener of type EventListener that Firestore calls whenever data changes or if an error occurs. EventListener is a generic interface for any type of event listening, and contains only onEvent(T value, FirebaseFirestoreException error) 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. Since it’s generic, it can work for any collection.

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

cloudFirestoreManager.onPostsValuesChange()
  .observe(this, Observer(::onPostsUpdate))

Here, you register an observer for values on the posts collection. When you receive new data, 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() . Replace the function code with the following:

fun stopListeningForPostChanges() = postsRegistration.remove()

Here, you call remove() on a ListenerRegistration object. This removes the listener from the location that this listener is assigned to.

Now go back to HomeActivity.kt and add this code:

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

Here, you call your stopListeningForPostChanges() method. It stops the listener when the activity stops.

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

Listening to Firetore Collection.
Listening to Firetore Collection.

You can test the real-time updates by deleting the posts from the console. You”ll see the app will instantly reflect the changes.

Performing Queries

Sometimes, you don’t want to read only 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. Currently, you don’t have any nested documents in the posts collection. Up next, you’ll add comments to it to make it a bit more complex.

Adding Comments

Open CloudFirestoreManager.kt, and navigate to addComment(). Replace TODO 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

To sum it up, here’s what happens:

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

Open PostDetailsActivity.kt and find initializeClickListener(). Replace TODO inside the addCommentButton click listener with:

cloudFirestoreManager.addComment(
            post.id,
            comment,
            ::onCommentSuccessfullyAdded,
            ::onCommentAddFailed
          )

In this code, you call addComment() with the comment from EditText. You’re also passing the id of the post and methods to handle success and failure. This will save the comment to the database when you tap ADD COMMENT.

Build and run. Tap on any post in the list. Enter some text into the comments EditText and tap ADD COMMENT:

The screen for creating a comment.
The screen for creating a comment.

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

Comments Collection in Firestore Database.
Comments Collection in Firestore Database.

Listening for comments

You can add comments to the database now, but you still can’t read them. Since you store comments 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.

Open CloudFirestoreManager.kt. Add a commentsRegistration field below the postsRegistration variable:

private lateinit var commentsRegistration: ListenerRegistration

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

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

// 1
commentsRegistration = database.collection(COMMENTS_COLLECTION)
    // 2
    .whereEqualTo(POST_ID, postId) // 3
    // 4
    .addSnapshotListener(EventListener { 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)
        }
    })

Here’s what you’re doing:

  1. As before, you assign a listener to the commentsRegistration reference.

  2. whereEqualTo() creates a query that filters the documents in the collection that contain the specified field and value in that field.

  3. You pass in two parameters. One is POST_ID, which represents the post_id property in your collection. The second parameter, postId represents the value for comparison. This query will only return the documents that belong to the specified post. whereEqualTo() returns Query that you can read or listen to.

  4. Once again, you attach EventListener and parse the comments list, if there are any, updating the UI when you’re done.

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

 post?.id?.let {postId ->
      cloudFirestoreManager.onCommentsValuesChange(postId)
        .observe(this, Observer(::onCommentsUpdate))
    }

Add necessary import from the lifecycle package. With these lines, you are listening for the comments changes for that particular post and when the data changes you update the UI.

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

fun stopListeningForCommentsChanges() = commentsRegistration.remove()

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

Open PostDetailsActivity.kt and add the following code below onStart():

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

In the code above, you call stopListeningForCommentsChanges() which stops the listener when the activity stops.

Build and run. Open a post that has at least one comment. You’ll see all comments appear in the post details section of the app.

Adding Comments in Realtime.
Adding Comments in Realtime.

Adding more comments to the post will appear instantly since you’ve attached a listener. Firestore notifies the listener of any changes in the comments collection.

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 you delete that post.

Open CloudFirestoreManager.kt and navigate to deletePostComments(). Replace 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() } }

In the code above, you:

  1. Create 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 you load the comments, you delete them from the database, one by one.

Using continueWith() on a Task instance returns a new Task. The Task completes with the result of applying the specified Continuation to this Task. A Continuation function once called, continues execution after completion of a Task. You fetch the comments for the specific posts and delete them by traversing the result documents and calling delete() on each document reference.

Finally, in CloudFirestoreManager.kt, at the end of deletePost() add:

  deletePostComments(key)

This will delete the comments tied to that post, when you delete a post.

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

Listening to Comments Updates.
Listening to Comments Updates.

Now, delete the posts that you added comments to and observe the database in the console. 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!

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

Here are the steps:

  • Add some posts if you don’t have them already. You can add some comments to that post as well if you like.
  • 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.
  • Add another post. Tap the floating action button on the home screen and enter some content for the post and tap Post. 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. The app displays the post that you added while offline on the home screen. This is because the app saved it to the local cache. If you open the console and look into the database you won’t see that post in the database.
  • Connect your device back to the network. You’ll get a Toast message on the device that the post is saved and 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 (https://firebase.google.com/docs/firestore/manage-data/enable-offline) 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 whereEqualTo():

  • You can use orderBy() on the collection reference to sort the data by the specified field. By default, Firestore sorts the documents 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 where() for filtering with limit() and orderBy().

Pagination

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

Firestore provides you with the pagination feature, which works in a way where you don’t need to execute one large query. Instead, you use multiple smaller queries sequentially. Firestore 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 (https://firebase.google.com/docs/firestore/query-data/query-cursors) 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 (https://firebase.google.com/docs/firestore/query-data/indexing) 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() on the collection reference.

  • The 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 (https://firebase.google.com/docs/firestore/query-data/get-data).

WhatsUp is now complete, but it has one big flaw. Anyone can read and write the data to the database. In “Chapter 20: Securing Data in Cloud Firestore”, you’ll learn how to secure the data in the database and 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.