Cloud Firestore Android Codelab


In this codelab you will build a restaurant recommendation app on Android backed by Cloud Firestore. You will learn how to:

  • Read and write data to Firestore from an Android app
  • Listen to changes in Firestore data in realtime
  • Use Firebase Authentication and security rules to secure Firestore data
  • Write complex Firestore queries


Before starting this codelab make sure you have:

  • Android Studio 4.0 or higher
  • An Android device or emulator
  1. Sign into the Firebase console with your Google account.
  2. In the Firebase console, click Add project. 14da8a378fd40320.png
  3. As shown in the screen capture below, enter a name for your Firebase project (for example, "Friendly Eats"), and click Continue.


  1. Choose whether to enable Google Analytics for the project and click Continue.


  1. Lastly, choose your analytics location, choose what settings to apply to your project, read and accept the terms, and click Create project.


  1. After a minute or so, your Firebase project will be ready. Click Continue.


Download the code

Run the following command to clone the sample code for this codelab. This will create a folder called friendlyeats-android on your machine:

$ git clone

If you don't have git on your machine, you can also download the code directly from GitHub.

Import the project into Android Studio. You will probably see some compilation errors or maybe a warning about a missing google-services.json file. We'll correct this in the next section.

Set up Firebase

  1. In the Firebase console, select Project Overview in the left nav. Click the Android button to select the platform. When prompted for a package name use


  1. Click Register App and follow the instructions to download the google-services.json file, and move it into the app/ folder of the sample code. Then click Next.
  2. Follow the instructions to add the Firebase SDK dependencies to your gradle files, then run the app.

Configure Firebase

In the Sign-in Providers tab of the Firebase console, enable Email Authentication:


Next, follow the steps to enable Cloud Firestore for your project.

Access to data in Cloud Firestore is controlled by Security Rules. We'll talk more about rules later in this codelab but first we need to set some basic rules on our data to get started. In the Rules tab of the Firebase console add the following rules and then click Publish.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      // WARNING: These rules are insecure! We will replace them with
      // more secure rules later in the codelab
      allow read, write: if request.auth != null;

The rules above restrict data access to users who are signed in, which prevents unauthenticated users from reading or writing. This is better than allowing public access but is still far from secure, we will improve these rules later in the codelab.

Run the app

If you have set up your app correctly, the project should now compile. In Android Studio click Build > Rebuild Project and ensure that there are no remaining errors.

Now run the app on your Android device. At first you will be presented with a "Sign in" screen. You can use an email and password to sign into the app. Once you have completed the sign in process you should see the app home screen:


In the next section we will add some data to populate the home screen.

In this section we will write some data to Firestore so that we can populate the home screen. You can enter data manually in the Firebase console, but we'll do it in the app itself to demonstrate how to write data to Firestore using the Android SDK.

The main model object in our app is a restaurant (see model/ Firestore data is split into documents, collections, and subcollections. We will store each restaurant as a document in a top-level collection called "restaurants". To learn more about the Firestore data model, read about documents and collections in the documentation.

First, let's get an instance of FirebaseFirestore to work with. Edit the initFirestore() method in MainActivity:

    private void initFirestore() {
        mFirestore = FirebaseFirestore.getInstance();

For demonstration purposes, we will add functionality in the app to create ten random restaurants when we click the "Add Random Items" button in the overflow menu. Fill in the onAddItemsClicked():

    private void onAddItemsClicked() {
        // Get a reference to the restaurants collection
        CollectionReference restaurants = mFirestore.collection("restaurants");

        for (int i = 0; i < 10; i++) {
            // Get a random Restaurant POJO
            Restaurant restaurant = RestaurantUtil.getRandom(this);

            // Add a new document to the restaurants collection

There are a few important things to note about the code above:

  • We started by getting a reference to the "restaurants" collection. Collections are created implicitly when documents are added, so there was no need to create the collection before writing data.
  • Documents can be created using POJOs, which we use to create each Restaurant doc.
  • The add() method adds a document to a collection with an auto-generated ID, so we did not need to specify a unique ID for each Restaurant.

Now run the app again and click the "Add Random Items" button in the overflow menu to invoke the code you just wrote:


If you navigate to the Firebase console, you should see the newly added data:


Congratulations, you just wrote data to Firestore! In the next step we'll learn how to display this data in the app.

In this step we will learn how to retrieve data from Firestore and display it in our app. The first step to reading data from Firestore is to create a Query. Modify the initFirestore() method:

    private void initFirestore() {
        mFirestore = FirebaseFirestore.getInstance();

        // Get the 50 highest rated restaurants
        mQuery = mFirestore.collection("restaurants")
                .orderBy("avgRating", Query.Direction.DESCENDING)

Now we want to listen to the query, so that we get all matching documents and are notified of future updates in real time. Because our eventual goal is to bind this data to a RecyclerView, we need to create a RecyclerView.Adapter class to listen to the data.

Open the FirestoreAdapter class, which has been partially implemented already. First, let's make the adapter implement EventListener and define the onEvent function so that it can receive updates to a Firestore query:

public abstract class FirestoreAdapter<VH extends RecyclerView.ViewHolder>
        extends RecyclerView.Adapter<VH>
        implements EventListener<QuerySnapshot> {

    // ...

    public void onEvent(QuerySnapshot documentSnapshots, 
                        FirebaseFirestoreException e) {
        // Handle errors
        if (e != null) {
            Log.w(TAG, "onEvent:error", e);

        // Dispatch the event
        for (DocumentChange change : documentSnapshots.getDocumentChanges()) {
            // Snapshot of the changed document
            DocumentSnapshot snapshot = change.getDocument();
            switch (change.getType()) {
                case ADDED:
                    // TODO: handle document added
                case MODIFIED:
                    // TODO: handle document modified
                case REMOVED:
                    // TODO: handle document removed


  // ...

On initial load the listener will receive one ADDED event for each new document. As the result set of the query changes over time the listener will receive more events containing the changes. Now let's finish implementing the listener. First add three new methods: onDocumentAdded, onDocumentModified, and on onDocumentRemoved:

    protected void onDocumentAdded(DocumentChange change) {
        mSnapshots.add(change.getNewIndex(), change.getDocument());

    protected void onDocumentModified(DocumentChange change) {
        if (change.getOldIndex() == change.getNewIndex()) {
            // Item changed but remained in same position
            mSnapshots.set(change.getOldIndex(), change.getDocument());
        } else {
            // Item changed and changed position
            mSnapshots.add(change.getNewIndex(), change.getDocument());
            notifyItemMoved(change.getOldIndex(), change.getNewIndex());

    protected void onDocumentRemoved(DocumentChange change) {

Then call these new methods from onEvent:

    public void onEvent(QuerySnapshot documentSnapshots,
                        FirebaseFirestoreException e) {

        // ...

        // Dispatch the event
        for (DocumentChange change : documentSnapshots.getDocumentChanges()) {
            // Snapshot of the changed document
            DocumentSnapshot snapshot = change.getDocument();

            switch (change.getType()) {
                case ADDED:
                case MODIFIED:
                case REMOVED:

Finally implement the startListening() method to attach the listener:

    public void startListening() {
        if (mQuery != null && mRegistration == null) {
            mRegistration = mQuery.addSnapshotListener(this);

Now the app is fully configured to read data from Firestore. Run the app again and you should see the restaurants you added in the previous step:


Now go back to the Firebase console and edit one of the restaurant names. You should see it change in the app almost instantly!

The app currently displays the top-rated restaurants across the entire collection, but in a real restaurant app the user would want to sort and filter the data. For example the app should be able to show "Top seafood restaurants in Philadelphia" or "Least expensive pizza".

Clicking white bar at the top of the app brings up a filters dialog. In this section we'll use Firestore queries to make this dialog work:


Let's edit the onFilter() method of This method accepts a Filters object which is a helper object we created to capture the output of the filters dialog. We will change this method to construct a query from the filters:

    public void onFilter(Filters filters) {
        // Construct query basic query
        Query query = mFirestore.collection("restaurants");

        // Category (equality filter)
        if (filters.hasCategory()) {
            query = query.whereEqualTo("category", filters.getCategory());

        // City (equality filter)
        if (filters.hasCity()) {
            query = query.whereEqualTo("city", filters.getCity());

        // Price (equality filter)
        if (filters.hasPrice()) {
            query = query.whereEqualTo("price", filters.getPrice());

        // Sort by (orderBy with direction)
        if (filters.hasSortBy()) {
            query = query.orderBy(filters.getSortBy(), filters.getSortDirection());

        // Limit items
        query = query.limit(LIMIT);

        // Update the query
        mQuery = query;

        // Set header

        // Save filters

In the snippet above we build a Query object by attaching where and orderBy clauses to match the given filters.

Run the app again and select the following filter to show the most popular low-price restaurants:


If you view the app logs using adb logcat or the Logcat panel in Android Studio, you will notice some warnings:

W/Firestore Adapter: onEvent:error FAILED_PRECONDITION: The query requires an index. You can create it here:
    at Source)
    // ...

The query could not be completed on the backend because it requires an index. Most Firestore queries involving multiple fields (in this case price and rating) require a custom index. Clicking the link in the error message will open the Firebase console and automatically prompt you to create the correct index:


Clicking Create Index will begin creating the required index. When the index is complete your query should succeed:


Now that the proper index has been created, run the app again and execute the same query. You should now see a filtered list of restaurants containing only low-price options:


If you've made it this far, you have now built a fully functioning restaurant recommendation viewing app on Firestore! You can now sort and filter restaurants in real time. In the next few sections we posting reviews and security to the app.

In this section we'll add ratings to the app so users can review their favorite (or least favorite) restaurants.

Collections and subcollections

So far we have stored all restaurant data in a top-level collection called "restaurants". When a user rates a restaurant we want to add a new Rating object to the restaurants. For this task we will use a subcollection. You can think of a subcollection as a collection that is attached to a document. So each restaurant document will have a ratings subcollection full of rating documents. Subcollections help organize data without bloating our documents or requiring complex queries.

To access a subcollection, call .collection() on the parent document:

CollectionReference subRef = mFirestore.collection("restaurants")

You can access and query a subcollection just like with a top-level collection, there are no size limitations or performance changes. You can read more about the Firestore data model here.

Writing data in a transaction

Adding a Rating to the proper subcollection only requires calling .add(), but we also need to update the Restaurant object's average rating and number of ratings to reflect the new data. If we use separate operations to make these two changes there are a number of race conditions that could result in stale or incorrect data.

To ensure that ratings are added properly, we will use a transaction to add ratings to a restaurant. This transaction will perform a few actions:

  • Read the restaurant's current rating and calculate the new one
  • Add the rating to the subcollection
  • Update the restaurant's average rating and number of ratings

Open and implement the addRating function:

    private Task<Void> addRating(final DocumentReference restaurantRef, 
                                 final Rating rating) {
        // Create reference for new rating, for use inside the transaction
        final DocumentReference ratingRef = restaurantRef.collection("ratings")

        // In a transaction, add the new rating and update the aggregate totals
        return mFirestore.runTransaction(new Transaction.Function<Void>() {
            public Void apply(Transaction transaction) 
                    throws FirebaseFirestoreException {

                Restaurant restaurant = transaction.get(restaurantRef)

                // Compute new number of ratings
                int newNumRatings = restaurant.getNumRatings() + 1;

                // Compute new average rating
                double oldRatingTotal = restaurant.getAvgRating() * 
                double newAvgRating = (oldRatingTotal + rating.getRating()) /

                // Set new restaurant info

                // Commit to Firestore
                transaction.set(restaurantRef, restaurant);
                transaction.set(ratingRef, rating);

                return null;

The addRating() function returns a Task representing the entire transaction. In the onRating() function listeners are added to the task to respond to the result of the transaction.

Now Run the app again and click on one of the restaurants, which should bring up the restaurant detail screen. Click the + button to start adding a review. Add a review by picking a number of stars and entering some text.


Hitting Submit will kick off the transaction. When the transaction completes, you will see your review displayed below and an update to the restaurant's review count:


Congrats! You now have a social, local, mobile restaurant review app built on Cloud Firestore. I hear those are very popular these days.

At the beginning of this codelab we set our app's security rules to prevent unauthenticated access. In a real application we'd want to set much more fine-grained rules to prevent undesirable data access or modification. Open the rules tab of the console and change the security rules to the following:

rules_version = '2';
service cloud.firestore {

  // Determine if the value of the field "key" is the same
  // before and after the request.
  function unchanged(key) {
    return (key in 
      && (key in 
      && ([key] ==[key]);

  match /databases/{database}/documents {
    // Restaurants:
    //   - Authenticated user can read
    //   - Authenticated user can create/update (for demo purposes only)
    //   - Updates are allowed if no fields are added and name is unchanged
    //   - Deletes are not allowed (default)
    match /restaurants/{restaurantId} {
      allow read: if request.auth != null;
      allow create: if request.auth != null;
      allow update: if request.auth != null
                    && ( == 
                    && unchanged("name");
      // Ratings:
      //   - Authenticated user can read
      //   - Authenticated user can create if userId matches
      //   - Deletes and updates are not allowed (default)
      match /ratings/{ratingId} {
        allow read: if request.auth != null;
        allow create: if request.auth != null
                      && == request.auth.uid;

These rules restrict access to ensure that clients only make safe changes. For example updates to a restaurant document can only change the ratings, not the name or any other immutable data. Ratings can only be created if the user ID matches the signed-in user, which prevents spoofing.

To read more about Security Rules, visit the documentation.

You have now created a fully-featured app on top of Firestore. You learned about the most important Firestore features including:

  • Documents and collections
  • Reading and writing data
  • Sorting and filtering with queries
  • Subcollections
  • Transactions

The restaurant app in this codelab was based on the "Friendly Eats" example application. You can browse the source code for that app here.

If you want to keep learning about Firestore, here are some good places to get started: