Getting Started With Cloud Firestore for Android

Cloud Firestore is a recent addition to the Firebase family of products. Although still in beta, it’s already being presented by Google as a more flexible and feature-rich alternative to the Firebase Realtime Database.

If you’ve ever used the Realtime Database, you’re probably aware that it’s essentially a large JSON document best suited for storing simple key-value pairs only. Storing hierarchical data on it efficiently and securely, although possible, is quite cumbersome and requires a well-thought-out strategy, which usually involves flattening the data as much as possible or denormalizing it. Without such a strategy, queries on the Realtime Database are likely to consume unnecessarily large amounts of bandwidth.

Cloud Firestore, being more akin to document-oriented databases such as MongoDB and CouchDB, has no such problems. Moreover, it comes with a large number of very handy features, such as support for batch operations, atomic writes, and indexed queries.

In this tutorial, I’ll help you get started with using Cloud Firestore on the Android platform.

Prerequisites

To be able to follow this tutorial, you’ll need:

  • the latest version of Android Studio
  • a Firebase account
  • and a device or emulator running Android 4.4 or higher

1. Creating a Firebase Project

Before you use Firebase products in your Android app, you must create a new project for it in the Firebase console. To do so, log in to the console and press the Add project button in the welcome screen.

Welcome screen of Firebase console

In the dialog that pops up, give a meaningful name to the project, optionally give a meaningful ID to it, and press the Create Project button.

Add a project dialog

Once the project has been created, you can set Firestore as its database by navigating to Develop > Database and pressing the Try Firestore Beta button.

Database selection page

In the next screen, make sure you choose the Start in test mode option and press the Enable button.

Firestore security configuration page

At this point, you’ll have an empty Firestore database all ready to be used in your app.

Empty Firestore database

2. Configuring the Android Project

Your Android Studio project still knows nothing about the Firebase project you created in the previous step. The easiest way to establish a connection between the two is to use Android Studio’s Firebase Assistant.

Go to Tools > Firebase to open the Assistant.

Firebase Assistant window

Because Firestore is still in beta, the Assistant doesn’t support it yet. Nevertheless, by adding Firebase Analytics to your app, you’ll be able to automate most of the required configuration steps.

Start by clicking on the Log an Analytics Event link under the Analytics section and pressing the Connect to Firebase button. A new browser window should now pop up asking you if you want to allow Android Studio to, among other things, manage Firebase data.

Android Studio requesting permissions

Press the Allow button to continue.

Back in Android Studio, in the dialog that pops up, select the Choose an existing Firebase or Google project option, pick the Firebase project you created earlier, and press the Connect to Firebase button.

Connect to Firebase dialog

Next, press the Add Analytics to your app button to add the core Firebase dependencies to your project.

Finally, to add Firestore as an implementation dependency, add the following line in the app module’s build.gradle file:

implementation 'com.google.firebase:firebase-firestore:11.8.0'

Don’t forget to press the Sync Now button to complete the configuration. If you encounter any version conflict errors during the sync process, make sure that the versions of the Firestore dependency and the Firebase Core dependency are identical and try again.

3. Understanding Documents and Collections

Firestore is a NoSQL database that allows you to store data in the form of JSON-like documents. However, a document stored on it cannot exist independently. It must always belong to a collection. As its name suggests, a collection is nothing but a bunch of documents. 

Documents within a collection are obviously siblings. If you want to establish parent-child relationships between them, though, you must use subcollections. A subcollection is just a collection that belongs to a document. By default, a document automatically becomes the parent of all the documents that belong to its subcollections.

It is also worth noting that Firestore manages the creation and deletion of both collections and subcollections by itself. Whenever you try to add a document to a non-existent collection, it creates the collection. Similarly, once you delete all the documents from a collection, it deletes it.

4. Creating Documents

To be able to write to the Firestore database from your Android app, you must first get a reference to it by calling the getInstance() method of the FirebaseFirestore class.

val myDB = FirebaseFirestore.getInstance()

Next, you must either create a new collection or get a reference to an existing collection, by calling the collection() method. For example, on an empty database, the following code creates a new collection named solar_system:

val solarSystem = myDB.collection("solar_system")

Once you have a reference to a collection, you can start adding documents to it by calling its add() method, which expects a map as its argument.

// Add a document
solarSystem.add(mapOf(
        "name" to "Mercury",
        "number" to 1,
        "gravity" to 3.7
))

// Add another document
solarSystem.add(mapOf(
        "name" to "Venus",
        "number" to 2,
        "gravity" to 8.87
))

The add() method automatically generates and assigns a unique alphanumeric identifier to every document it creates. If you want your documents to have your own custom IDs instead, you must first manually create those documents by calling the document() method, which takes a unique ID string as its input. You can then populate the documents by calling the set() method, which, like the add method, expects a map as its only argument.

For example, the following code creates and populates a new document called PLANET_EARTH:

solarSystem.document("PLANET_EARTH")
        .set(mapOf(
                "name" to "Earth",
                "number" to 3,
                "gravity" to 9.807
        ))

If you go to the Firebase console and take a look at the contents of the database, you’ll be able to spot the custom ID easily.

New entries in the Firestore database

Beware that if the custom ID you pass to the document() method already exists in the database, the set() method will overwrite the contents of the associated document.

5. Creating Subcollections

Support for subcollections is one of the most powerful features of Firestore and is what makes it markedly different from the Firebase Realtime Database. Using subcollections, you can not only easily add nested structures to your data but also be sure that your queries will consume minimal amounts of bandwidth.

Creating a subcollection is much like creating a collection. All you need to do is call the collection() method on a DocumentReference object and pass a string to it, which will be used as the name of the subcollection.

For example, the following code creates a subcollection called satellites and associates it with the PLANET_EARTH document:

val satellitesOfEarth = solarSystem.document("PLANET_EARTH")
                                   .collection("satellites")

Once you have a reference to a subcollection, you are free to call the add() or set() methods to add documents to it.

satellitesOfEarth.add(mapOf(
        "name" to "The Moon",
        "gravity" to 1.62,
        "radius" to 1738
))

After you run the above code, the PLANET_EARTH document will look like this in the Firebase console:

Subcollection of a document

6. Running Queries

Performing a read operation on your Firestore database is very easy if you know the ID of the document you want to read. Why? Because you can directly get a reference to the document by calling the collection() and document() methods. For instance, here’s how you can get a reference to the PLANET_EARTH document that belongs to the solar_system collection:

val planetEarthDoc = myDB.collection("solar_system")
                         .document("PLANET_EARTH")

To actually read the contents of the document, you must call the asynchronous get() method, which returns a Task. By adding an OnSuccessListener to it, you can be notified when the read operation completes successfully.

The result of a read operation is a DocumentSnapshot object, which contains the key-value pairs present in the document. By using its get() method, you can get the value of any valid key. The following example shows you how:

planetEarthDoc.get().addOnSuccessListener {
    println(
       "Gravity of ${it.get("name")} is ${it.get("gravity")} m/s/s"
    )
}

// OUTPUT:
// Gravity of Earth is 9.807 m/s/s

If you don’t know the ID of the document you want to read, you will have to run a traditional query on an entire collection. The Firestore API provides intuitively named filter methods such as whereEqualTo()whereLessThan(), and whereGreaterThan(). Because the filter methods can return multiple documents as their results, you’ll need a loop inside your OnSuccessListener to handle each result.

For example, to get the contents of the document for planet Venus, which we added in an earlier step, you could use the following code:

myDB.collection("solar_system")
  .whereEqualTo("name", "Venus") 
  .get().addOnSuccessListener {
      it.forEach {
        println(
          "Gravity of ${it.get("name")} is ${it.get("gravity")} m/s/s"
        )
      }
  }

// OUTPUT:
// Gravity of Venus is 8.87 m/s/s

Lastly, if you are interested in reading all the documents that belong to a collection, you can directly call the get() method on the collection. For instance, here’s how you can list all the planets present in the solar_system collection:

myDB.collection("solar_system")
        .get().addOnSuccessListener {
            it.forEach {
                println(it.get("name"))
            }
        }

// OUTPUT:
// Earth
// Venus
// Mercury

Note that, by default, there is no definite order in which the results are returned. If you want to order them based on a key that’s present in all the results, you can make use of the orderBy() method. The following code orders the results based on the value of the number key:

myDB.collection("solar_system")
                .orderBy("number")
                .get().addOnSuccessListener {
            it.forEach {
                println(it.get("name"))
            }
        }

// OUTPUT:
// Mercury
// Venus
// Earth

7. Deleting Data

To delete a document with a known ID, all you need to do is get a reference to it and then call the delete() method.

myDB.collection("solar_system")
    .document("PLANET_EARTH")
    .delete()

Deleting multiple documents—documents you get as the result of a query—is slightly more complicated because there’s no built-in method for doing so. There are two different approaches you can follow.

The easiest and most intuitive approach—though one that’s suitable only for a very small number of documents—is to loop through the results, get a reference to each document, and then call the delete() method. Here’s how you can use the approach to delete all the documents in the solar_system collection:

myDB.collection("solar_system")
        .get().addOnSuccessListener {
            it.forEach {
                it.reference.delete()
            }
        }

A more efficient and scalable approach is to use a batch operation. Batch operations can not only delete multiple documents atomically but also significantly reduce the number of network connections required.

To create a new batch, you must call the batch() method of your database, which returns an instance of the WriteBatch class. Then, you can loop through all the results of the query and mark them for deletion by passing them to the delete() method of the WriteBatch object. Finally, to actually start the deletion process, you can call the commit() method. The following code shows you how:

myDB.collection("solar_system")
        .get().addOnSuccessListener {

    // Create batch
    val myBatch = myDB.batch()

    // Add documents to batch
    it.forEach {
        myBatch.delete(it.reference)
    }

    // Run batch
    myBatch.commit()
}

Note that trying to add too many documents to a single batch operation can lead to out-of-memory errors. Therefore, if your query is likely to return a large number of documents, you must make sure you split them into multiple batches

Conclusion

In this introductory tutorial, you learned how to perform read and write operations on the Google Cloud Firestore. I suggest you start using it in your Android projects right away. There’s a good chance that it will replace the Realtime Database in the future. In fact, Google already says that by the time it comes out of beta, it will be much more reliable and scalable than the Realtime Database.

To learn more about Cloud Firestore, you can refer to its official documentation.

And while you’re here, check out some of our other tutorials on Firebase and Android app development!

Leave a Reply

Your email address will not be published. Required fields are marked *