Firebase Android Series: Firestore

Francisco García Sierra
ProAndroidDev
Published in
9 min readJun 18, 2018

--

Welcome to another article of Firebase Android Series. Here, we are going to learn how to build an Android app using Firebase from scratch.

This is the second article of Firebase Android Series. A series of articles focused on learn how to use Firebase in any aspect that you would need to implement it as part of your published applications, or your start implementing it in your new ones.

The module that we are going to deal with this time is Cloud Firestore.

Cloud Firestore is a flexible, scalable database for mobile, web, and server development. Like Firebase Realtime Database, it keeps your data in sync across client apps through realtime listeners and offers offline support for mobile and web so you can build responsive apps that work regardless of network latency or Internet connectivity.

Working with a backend is not always friendly. We usually lose a lot of time picking a server, creating an instance of SQL and implementing our whole database model. Together with thinking on flowcharts and expose an API Rest to expose our server in a safe way…

Firestore allow us to create a database in just a few minutes. Also after some practice you will create non-relational database models like a pro. Continuing with this articles series, we are going to use Firestore to model all the database of our chat application. Let’s do this!

Configuring the project

To start configuring our Firestore database we just need to go to the Database tab in our Firebase console and click on Cloud Firestore.

After clicking on Cloud Firestore we will find the Firestore console. There we are going to be able to start creating our database model.

Building a database model

The Cloud Firestore data model supports flexible, hierarchical data structures. Store your data in documents, organized into collections. Documents can contain complex nested objects in addition to subcollections.

For now, we just want to allow our application to register an user, login and send/receive messages. For this few features we are going to divide our data in 3 different collections:

  • PrivateData : Contains the documents related with the private data of an user. We will take care of this data making it accesible just for the owner.
  • PublicProfile: Contains the data of the user that is going to be public for the rest of users: Name, profile picture, number of messages sent and the last login date.
  • Messages: Contains the data of each message sent in the application.

Now that we have clarified how we are going to build the database model, let’s move to the code.

Note: If you are totally new in non-relational database model. I suggest you to give a look to the official Firestore documentation regarding the database model. Also take some time and read this another article that teachs how to build an easy database model for Firestore:

Firestore operations

The Firestore API is quite simple and it just have a few methods to work with. They include all the possible cases that we could need to build our application. These methods are: get, set, add, update and delete.

Firebase APIs uses Google Play Tasks API to manage all their calls. If you are not familiar with the Tasks API you can give it a look here.

Database Models

Firestore allow us to upload and modify data though models representations in our codes. This representations are plain classes that contains all the values of our model. They are usually called POJO (plain old java object).

In Kotlin we can simply create a new dataclass for our model like the next one:

Model example for Firestore model class

The POJOs classes should have an empty constructor in java to be used for Firestore. In Kotlin we just need to add a default value to all our parameters.

The names given to the variables inside our POJO will be the same ones that Firestore will create on the database. If we are using camel case annotation in our code and we want to set a different name in Firestore we can make use of the annotation @PropertyName.

Note: To create timestamp or fields that holds dates on Firebase we should use the class Timestamp . To make Firestore automatically generate Timestamp values when we upload data to the database, we must use the annotation @ServerTimestamp and set the value to null.

Add Data

Every document in Cloud Firestore is uniquely identified by its location within the database. To refer documents and collections in Firestore it’s needed to use references.

Example of documents and collections

A reference is a lightweight object that just points to a location in your database. You can create a reference whether or not data exists there, and creating a reference does not perform any network operations.

To add data to Firestore we just need to create a new reference and use the method add . This operation like any operation in the Firebase API returns a Task . We are using Tasks.await method to make our code synchronous and easy to read without callbacks.

Uploading data to Firestore using add method

Set Data

Set data in Firestore is a different concept than add. The difference between both is that add must be use over a CollectionReference that generates a new unique ID for our document and upload the given data to Firestore. In the other hand, set is a method used over a DocumentReference and needs the ID of the document to work. We can use our own IDs or the ones generated by Firestore.

Uploading data to Firestore using set method

There would be cases where we will want Firestore to generate a new unique ID and store it on memory for our own purposes. To do this we need to call the document() method without pass it any ID.

Uploading data to Firestore using and ID generated by us

Update Data

Firestore allow us to update specific parts of our documents through the method update . It can receive by parameter a single String and an Any object to update an unique specific value or a Map<String,Any> to update multiple values of the given reference.

Updating a single value of the public profile document of our user

Delete Data

To delete documents on Firestore we just need to call delete over the reference.

Deleting the public profile of our user

Warning: Deleting a document does not delete its subcollections. When you delete a document that has associated subcollections, the subcollections are not deleted. They are still accessible by reference.

Get data

All the data in Firestore is retrieved through DocumentSnapshots and QuerySnapshots . The first one represents a single document while the second one represents the result of a query over a collection, being able to contain multiple DocumentSnapshots.

We can retrieve data from Firestore using the method get to retrive data just once or addSnapshotListener to add a listener to listen any changes inside a document or collection. Let’s give a look to both cases:

Getting data from a document reference
Listening data changes from the messages collection

In the case of the SnapshotListener we will need to call remove() over the snapshot instance to make the listener stop working.

Note: Firestore will always return a snapshot value, even if there is no data in the given reference. To check if our snapshot contains or not data we must use the method exists() over our DocumentSnapshot references.

Atomic operations

Cloud Firestore supports atomic operations for reading and writing data. In a set of atomic operations, either all of the operations succeed, or none of them are applied. There are two types of atomic operations in Cloud Firestore:

  • Transactions: a transaction is a set of read and write operations on one or more documents.
  • Batched Writes: a batched write is a set of write operations on one or more documents.

Each transaction or batch of writes can write to a maximum of 500 documents. For additional limits related to writes, see Quotas and Limits in the official documentation.

Transaction

A transaction consists of any number of get() operations followed by any number of write operations such as set(),update(), or delete().

Transactions never partially apply writes. All writes execute at the end of a successful transaction. Let’s see an example:

Using a transaction to increase the followers counter of an user

You can read more about transactions in the official documentation.

Note: When running transactions the read operations must come before write operations. Also a transaction will fail if the client is offline.

Batches writes

If you do not need to read any documents in your operation set, you can execute multiple write operations as a single batch that contains any combination of set(), update(), or delete() operations. A batch of writes completes atomically and can write to multiple documents.

Batched writes are also useful for migrating large data sets to Cloud Firestore. Write batches can contain up to 500 operations and they reduces connection overhead resulting in faster data migration. Let’s see an example:

Executing multiple operations in a single batch

Query data and pagination

Cloud Firestore provides powerful query functionality for specifying which documents you want to retrieve from a collection. These queries can also be used with either get() or addSnapshotListener(), as described above.

We can make use of the next methods over our database references to query our data:

  • whereEqualTo() does an == comparation.
  • whereLessThan() does an < comparation.
  • whereLessThanOrEqualTo() does an <= comparation.
  • whereGreaterThan() does an > comparation.
  • whereGreaterThanOrEqualTo() does an >= comparation.
  • orderBy() allow us to order by a given field.
  • limit() limit the number of DocumentSnapshot received.

All this methods can be combined between them to form our desired query. For compounded queries Firestore will force us to generate a custom index on our database console. Without an index our compound queries will fail.

Pagination

To paginate our data, Firestore give us the startAt and endAt methods, which receives a DocumentSnapshot and uses it for paginate our data. This means that we will need to keep in the memory the last DocumentSnapshot given by each query done when we will want to implement pagination over it.

Note: Firestore also allow us to save geographical coordinates in our Database. Using the class GeoPoint we can query by distance. If you want to read more about it you can check the next post on Stackoverflow.

Good practices

We are about to finish. ’m going to share a few good practices to have into account when working with Firestore in your application.

Separate your database models

Keep your Firestore data models separated from your application models. As you will see in the code sample. For each POJO of the database, there is always a FirebaseModel for the same class.

This allow us to keep our Firestore model with their own names in the variables and make easier complex casts, serving as a gateway in the client side. It also allow us to keep the id of each document out of the original model(because it’s already contained in the DocumentSnapshot) without use annotations.

Keep your Firestore models in a single package

When using Firestore in your app along with ProGuard you need to consider how your model objects will be serialized and deserialized after obfuscation. If you use DocumentSnapshot.toObject(Class)to read data you will need to add rules to the proguard-rules.pro keeping the model classes.

Proguard code to keep firebase models from obfuscation

Build your references as a tree

Try to keep all your Firestore references together and build them as a tree. This will make it easier to read them when they start to become more nested. You can also use Kotlin extensions over Firestore to make them more verbose.

Use reactive model in your repository

I suggest you to wrap Firebase in your own way on your repository. The Task API made by Google is really nice, but it generates a lot of boilerplate and make a bit tedious concatenate operations if you are not friendly with it.

Kotlin coroutines or RxJava are good choices to use on your repository together with Firebase. Both will allow you to work more comfy on your application. There is already a RxJava 2.0 wrapper over Firebase, you can give it a look in one of my old articles:

Github Sample

This series will always work over the same project. Building a chat application using Firebase and Kotlin. You will find each one of the different articles code in individual branches of the project.

--

--