Cloud Firestore is a NoSQL database similar to the Realtime Database. It stores data in a structure that looks like a tree, but where data is stored as documents. It’s designed to overcome all of the drawbacks of the Realtime database.
Firestore uses collections and documents to store data. A document is an entry that contains any fields. Documents are combined into collections.
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.
The Firebase console is where you set up and manage your apps. From there, you can view all of your projects as well as create new ones.
To add the Cloud Firestore to your app, you only need to add one more dependency. Open up build.gradle file, if you haven't got it open and add the following dependency, making sure you're using the latest version:
com.google.firebase:firebase-firestore-ktx:21.4.3
Model
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:
data class User( val first: String = "", val last: String = "", val birth: Int = 0 )
Get data
Let's create a reference to users collection
val db = Firebase.firestore val users = db.collection("users")
The following code shows how to get a document
val user1 = users.document("uid1") user1.get().addOnSuccessListener { snapshot -> if (snapshot != null) { Log.d(TAG, "DocumentSnapshot data: ${snapshot.data}") } else { Log.d(TAG, "No such document") } }.addOnFailureListener { exception -> Log.d(TAG, "get failed with ", exception) }
Get custom object
user1.get().addOnSuccessListener { snapshot -> val usr = snapshot.toObject<User>() }
In order to get all the documents of a collection, the following code is enough
users.get() .addOnSuccessListener { snapshot -> for (document in snapshot) { Log.d(TAG, "${document.id} => ${document.data}") } } .addOnFailureListener { exception -> Log.w(TAG, "Error getting documents: ", exception) }
The following code shows how to get documents from the collection by condition
users.whereEqualTo("birth", 1985) .get() .addOnSuccessListener { snapshot -> } .addOnFailureListener { exception -> }
While getting the documents, they can be immediately converted into our data classes
users.get() .addOnSuccessListener { snapshot -> val items: List<User> = snapshot.toObjects(User::class.java) } .addOnFailureListener { exception -> }
Get Multple Documents by Ids
fun getItems(ids: List<String>): List<Deferred<DocumentSnapshot>> { val firestore = FirebaseFirestore.getInstance() return ids.map { id -> firestore.collection("users").document(id).get().asDeferred() } } launch(Dispatchers.Default) { val ids = ... val docs = getItems(ids).awaitAll() }
Read more about queries in Cloud Firestore.
Write data
For writing, you have to create a Hashmap with data (where the name of the field acts as a key, and the value of this field as a value) and transfer it to the library. You can see that in the following code
val usr = hashMapOf( "first" to "Joe", "last" to "Black", "birth" to 1985 ) users.add(usr) .addOnSuccessListener {} .addOnFailureListener { e -> Log.w(TAG, "Error writing document", e) }
To set your own ID you need to do the following
users.document("uid1") .set(usr) .addOnSuccessListener { } .addOnFailureListener { e -> Log.w(TAG, "Error writing document", e) }
Write custom object
val usr2 = User("Joe", "Black", 1985) users.document("uid2").set(usr2)
Update a document
users.document("uid2").update("last", "Green")
Delete data
users.document("uid2").delete()
Subscribe to changes
Firestore allows you to subscribe to data changes. You can subscribe to changes in the collection as well as changes to a specific document
users.document("usr1").addSnapshotListener { snapshot, e -> if (e != null) { Log.w(TAG, "Listen failed.", e) return@addSnapshotListener } if (snapshot != null && snapshot.exists()) { Log.d(TAG, "Current data: ${snapshot.data}") } else { Log.d(TAG, "Current data: null") } } users.whereEqualTo("birth", 1985) .addSnapshotListener { snapshot, e -> if (e != null) { Log.w(TAG, "Listen failed.", e) return@addSnapshotListener } val names = ArrayList<String>() for (doc in value!!) { doc.getString("first")?.let { names.add(it) } } Log.d(TAG, "Current cites in CA: $cities") }
snapshot
contains two useful fields
ADDED
— documents added, MODIFIED
— documents updated, REMOVED
— documents deleted. It is often useful to see the actual changes to query results between query snapshots, instead of simply using the entire query snapshot.
users.whereEqualTo("birth", 1985) .addSnapshotListener { snapshots, e -> if (e != null) { Log.w(TAG, "listen:error", e) return@addSnapshotListener } for (dc in snapshots!!.documentChanges) { when (dc.type) { DocumentChange.Type.ADDED -> Log.d(TAG, "New user: ${dc.document.data}") DocumentChange.Type.MODIFIED -> Log.d(TAG, "Modified user: ${dc.document.data}") DocumentChange.Type.REMOVED -> Log.d(TAG, "Removed user: ${dc.document.data}") } } }
Detach a listener
When you are no longer interested in listening to your data, you must detach your listener so that your event callbacks stop getting called. This allows the client to stop using bandwidth to receive updates. For example:
val registration = users.addSnapshotListener { snapshots, e -> } // Stop listening to changes registration.remove()
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 ddSnapshotListener()
, 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. To paginate our data, Firestore give us the startAt
, startAfter
and endAt
, endBefore
methods, which receives a DocumentSnapshot
and uses it for paginate data. The startAt
method includes the start point, while the startAfter
method excludes it. 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.
val query = users.orderBy("birth").limit(5) query.get().addOnSuccessListener { snapshots -> // Get the last visible document val lastVisible = snapshots.documents[snapshots.size() - 1] // Construct a new query starting at this document, // get the next 5 items. val next = users.orderBy("birth") .startAfter(lastVisible) .limit(5) // Use the query for pagination // ... }
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 post.
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:
val user1 = users.document("uid1") val user2 = users.document("uid2") val user3 = users.document("uid3") db.runBatch { batch -> batch.set(user1, User("Al", "Pacino", 1940)) batch.update(user2, "birth", 1990) batch.delete(user3) }.addOnCompleteListener { // ... }
Example with LiveData
You need a way to talk to the Cloud Firestore. Let's create CloudFirestoreManager
class that helps us to manage connection and value fetching.
private const val POSTS_COLLECTION = "posts" class CloudFirestoreManager { private val database = FirebaseFirestore.getInstance() private val postsValues = MutableLiveData<List<Post>>() private lateinit var postsRegistration: ListenerRegistration fun addPost(content: String, onSuccessAction: () -> Unit, onFailureAction: () -> Unit) { val documentReference = database.collection(POSTS_COLLECTION).document() val post = HashMap<String, Any>() post[AUTHOR_KEY] = authenticationManager.getCurrentUser() post[CONTENT_KEY] = content post[TIMESTAMP_KEY] = System.currentTimeMillis() post[ID_KEY] = documentReference.id documentReference .set(post) .addOnSuccessListener { onSuccessAction() } .addOnFailureListener { onFailureAction() } } fun onPostsValuesChange(): LiveData<List<Post>> { listenForPostsValueChanges() return postsValues } private fun listenForPostsValueChanges() { postsRegistration = database.collection(POSTS_COLLECTION) .addSnapshotListener(EventListener<QuerySnapshot> { value, error -> if (error != null || value == null) { return@EventListener } if (value.isEmpty) { postsValues.postValue(emptyList()) } else { val posts = ArrayList<Post>() for (doc in value) { val post = doc.toObject(Post::class.java) posts.add(post) } postsValues.postValue(posts) } }) } fun stopListeningForPostChanges() = postsRegistration.remove() fun updatePostContent(key: String, content: String, onSuccessAction: () -> Unit, onFailureAction: () -> Unit) { val updatedPost = HashMap<String, Any>() updatedPost[CONTENT_KEY] = content database.collection(POSTS_COLLECTION) .document(key) .update(updatedPost) .addOnSuccessListener { onSuccessAction() } .addOnFailureListener { onFailureAction() } } fun deletePost(key: String, onSuccessAction: () -> Unit, onFailureAction: () -> Unit) { database.collection(POSTS_COLLECTION) .document(key) .delete() .addOnSuccessListener { onSuccessAction() } .addOnFailureListener { onFailureAction() } } }
We can use this help class like so
class MainActivity : AppCompatActivity() { private val cfm by lazy { CloudFirestoreManager() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } override fun onStart() { super.onStart() listenForPostsUpdates() } override fun onStop() { super.onStop() cfm.stopListeningForPostChanges() } private fun listenForPostsUpdates() { cfm.onPostsValuesChange() .observe(this, Observer(::onPostsUpdate)) } private fun onPostsUpdate(posts: List<Post>) { Log.d("TAG", posts) } }
Using FirebaseUI to populate a RecyclerView
FirebaseUI offers two types of RecyclerView adapters for Cloud Firestore:
FirestoreRecyclerAdapter
— binds a Query
to a RecyclerView
and responds to all real-time events included items being added, removed, moved, or changed. Best used with small result sets since all results are loaded at once.FirestorePagingAdapter
— binds a Query
to a RecyclerView
by loading data in pages. Best used with large, static data sets. Real-time events are not respected by this adapter, so it will not detect new/removed items or changes to items already loaded.Example of FirestoreRecyclerAdapter
Import the Dependency
implementation 'com.firebaseui:firebase-ui-firestore:4.3.1'
Example of Adapter
class UserAdapter(options: FirestoreRecyclerOptions<User>) : FirestoreRecyclerAdapter<User, UserAdapter.ViewHolder>(options) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.user_item, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int, item: User) { holder.apply { tvName.text = item.name } } inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { var tvName: TextView = view.findViewById(R.id.tvName) } }
Example of Activity
class UsersActivity : AppCompatActivity() { var adapter: UserAdapter? = null var userListener: ListenerRegistration? = null var items = mutableListOf<User>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_users) val db = FirebaseFirestore.getInstance() val query = db.collection("users").orderBy("name") val options = FirestoreRecyclerOptions.Builder<User>() .setQuery(query, User::class.java) .build() adapter = UserAdapter(options) val layoutManager = LinearLayoutManager(applicationContext) rvItems.layoutManager = layoutManager rvItems.itemAnimator = DefaultItemAnimator() rvItems.adapter = adapter } override fun onStart() { super.onStart() adapter?.startListening() } override fun onStop() { super.onStop() adapter?.stopListening() } override fun onDestroy() { super.onDestroy() userListener?.remove() } fun setupUserListener() { val db = FirebaseFirestore.getInstance() userListener = db.collection("users") .addSnapshotListener(EventListener { documentSnapshots, e -> if (e != null) { Log.e(TAG, "Listen failed!", e) return@EventListener } items = mutableListOf() for (doc in documentSnapshots) { val item = doc.toObject(User::class.java) items.add(item) } rvItems.adapter = adapter adapter?.notifyDataSetChanged() }) } }
The FirestoreRecyclerAdapter
uses a snapshot listener to monitor changes to the Firestore query. To begin listening for data, call the startListening()
method. Similarly, the stopListening()
call removes the snapshot listener and all data in the adapter.
Example of FirestorePagingAdapter
The FirestorePagingAdapter
binds a Query
to a RecyclerView
by loading documents in pages. This results in a time and memory efficient binding, however it gives up the real-time events afforted by the FirestoreRecyclerAdapter
.
The FirestorePagingAdapter
is built on top of the Android Paging Support Library. Before using the adapter in your application, you must add a dependency on the support library:
implementation 'android.arch.paging:runtime:2.1.2'
First, configure the adapter by building FirestorePagingOptions
.
val query = FirebaseFirestore.getInstance() .collection("users") .orderBy("name", Query.Direction.ASCENDING) val config = PagedList.Config.Builder() .setEnablePlaceholders(false) .setPrefetchDistance(5) .setPageSize(10) .build() val options = FirestorePagingOptions.Builder<User>() .setLifecycleOwner(this) .setQuery(query, config, User::class.java) .build()
Next, create the FirestorePagingAdapter
class. You should already have a ViewHolder
subclass for displaying each item.
class UserPageAdapter(options: FirestorePagingOptions<User>) : FirestorePagingAdapter<User, UserPageAdapter.ViewHolder>(options) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.user_item, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int, item: User) { holder.apply { tvName.text = item.name } } override fun onLoadingStateChanged(state: LoadingState) { when(state) { LoadingState.LOADING_INITIAL -> { Log.d(Constants.TAG, "LOADING_INITIAL: "); } LoadingState.LOADING_MORE -> { Log.d(Constants.TAG, "LOADING_MORE: "); } } } inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { var tvName: TextView = view.findViewById(R.id.tvName) } }
Finally attach the adapter to your RecyclerView
with the setAdapter()
method.
class UsersActivity : AppCompatActivity() { val TAG = Constants.TAG; var adapter: UserPageAdapter? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_users) val query = FirestoreUtils.getCollection(Constants.FIREBASE_USERS_DB).orderBy("firstName", Query.Direction.ASCENDING) val config = PagedList.Config.Builder() .setEnablePlaceholders(false) .setPrefetchDistance(5) .setPageSize(10) .build() val options = FirestorePagingOptions.Builder<User>() .setLifecycleOwner(this) .setQuery(query, config, User::class.java) .build() adapter = UserPageAdapter(options) val layoutManager = LinearLayoutManager(applicationContext) rvItems.layoutManager = layoutManager rvItems.itemAnimator = DefaultItemAnimator() rvItems.adapter = adapter } override fun onStart() { super.onStart() adapter?.startListening() } override fun onStop() { super.onStop() adapter?.stopListening() } }
Useful links