Storing and Retrieving Data Using Cloud Firestore
Course: Android App Development
Topic: Storing and Retrieving Data Using Cloud Firestore
Course Link: Android App Development Course
Demo Link: Register for a Course Demo
Website: JustAcademy Official Website
1. Introduction to Cloud Firestore
Cloud Firestore is a flexible, scalable, cloud-hosted NoSQL document database provided by Firebase. It enables Android applications to store, retrieve, update, and delete application data over the internet. It also supports queries, realtime listeners, and offline capabilities for supported client operations.
In Android development, Cloud Firestore is useful for applications that need to share information between users or synchronize data across devices. Examples include student management systems, chat applications, e-commerce applications, task managers, attendance systems, and course management platforms.
Unlike a traditional relational database that organizes data into tables and rows, Firestore stores information in collections and documents. Each document contains fields represented by supported data types such as strings, numbers, Boolean values, arrays, maps, timestamps, and references.
2. Learning Objectives
- Understand Cloud Firestore and its architecture.
- Learn the difference between collections and documents.
- Configure Firestore in an Android application.
- Store data using Kotlin and the Firebase Android SDK.
- Retrieve individual documents and collections.
- Update and delete stored information.
- Perform filtering, sorting, and limited queries.
- Retrieve realtime updates using snapshot listeners.
- Understand offline access, error handling, and asynchronous operations.
- Secure Firestore data using Firebase Authentication and Security Rules.
- Build a practical Android application using Firestore CRUD operations.
3. Understanding the Firestore Data Model
Firestore organizes information into collections containing documents. Documents store fields and values, and they can also contain subcollections.
3.1 Collections
A collection is a group of related documents. For example, a student management application may contain a students collection, while an online shopping application may contain products and orders collections.
3.2 Documents
A document represents an individual record within a collection. It has a unique document ID and contains fields describing that record.
Example structure
students (collection)
|
|-- student_101 (document)
| |-- name: "Aarav Sharma"
| |-- age: 21
| |-- course: "Android Development"
| |-- active: true
|
|-- student_102 (document)
|-- name: "Priya Patel"
|-- age: 22
|-- course: "Data Analytics"
|-- active: true
In this example, students is the collection name, student_101 is a document ID, and name, age, course, and active are fields.
4. Cloud Firestore vs. Realtime Database
| Feature |
Cloud Firestore |
Realtime Database |
| Data model |
Collections and documents |
JSON tree |
| Queries |
Supports flexible filtering, sorting, and compound queries subject to index requirements |
Supports queries suited to its hierarchical data model |
| Realtime updates |
Supported through snapshot listeners |
Supported through database listeners |
| Offline support |
Supported by Android SDK with persistence enabled by default in typical configurations |
Supported by its client SDK |
| Data structure |
Document-oriented |
Hierarchical JSON |
| Common use cases |
Structured application records and flexible queries |
Low-latency synchronized data trees |
The correct choice depends on the application's data model, querying needs, latency requirements, and pricing. Both services require appropriate access controls.
5. Prerequisites for Using Cloud Firestore
- Android Studio installed and configured.
- A working Android project using Kotlin.
- A Firebase project created in the Firebase Console.
- The Android application registered with the correct application ID.
- The google-services.json configuration file placed in the app module.
- The Google Services Gradle plugin configured.
- The Firebase Firestore SDK added to the project.
- An internet connection for cloud operations.
If Firebase is not yet connected to your Android project, complete the initial setup described in the official Firebase Android setup guide.
6. Step 1: Create a Cloud Firestore Database
- Open the Firebase Console.
- Select the Firebase project associated with your Android application.
- Open Firestore Database from the database section.
- Choose Create database.
- Select the required database edition and location as prompted.
- Configure the initial security rules or access mode.
- Complete database creation.
Choose the database location carefully because location can affect latency, data residency, and service configuration. The database must exist before your application can successfully perform Firestore operations.
Security note: Do not leave a database publicly readable and writable for production use. Configure and test suitable Security Rules before releasing the application.
7. Step 2: Add the Firestore Dependency
In the app module's build.gradle.kts file, add the Firebase Android BoM and the Firestore dependency. The BoM manages compatible versions of Firebase libraries.
dependencies {
implementation(
platform("com.google.firebase:firebase-bom:35.1.0")
)
implementation(
"com.google.firebase:firebase-firestore"
)
}
Use a current compatible Firebase BoM version when creating a project. The version shown is an example; consult the official Firebase setup instructions for the current recommended configuration.
After updating the Gradle file, synchronize the project and verify that the build succeeds.
8. Step 3: Initialize the Firestore Instance
Once Firebase is configured, use FirebaseFirestore.getInstance() to obtain the Firestore client.
import com.google.firebase.firestore.FirebaseFirestore
class StudentRepository {
private val db: FirebaseFirestore =
FirebaseFirestore.getInstance()
}
The Firestore instance is used to access collections and documents. In a maintainable application, database operations are commonly placed in a repository rather than directly inside every Activity or composable.
9. Storing Data in Cloud Firestore
Firestore supports several ways to write data. The main methods are add(), set(), and update(). The method you choose determines how document IDs and existing fields are handled.
9.1 Using add() to Create a Document
The add() method creates a new document in a collection and automatically generates its document ID.
val db = FirebaseFirestore.getInstance()
val student = hashMapOf(
"name" to "Aarav Sharma",
"age" to 21,
"course" to "Android Development",
"active" to true
)
db.collection("students")
.add(student)
.addOnSuccessListener { documentReference ->
println(
"Student saved with ID: ${documentReference.id}"
)
}
.addOnFailureListener { exception ->
println("Error saving student: ${exception.message}")
}
Explanation
- FirebaseFirestore.getInstance() obtains the Firestore client.
- collection("students") selects the students collection.
- hashMapOf() creates the field-value map to store.
- add(student) creates a document with an automatically generated ID.
- addOnSuccessListener() handles successful completion.
- addOnFailureListener() handles a failed write.
This approach is useful when every new record should receive a unique document ID.
9.2 Using set() with a Custom Document ID
The set() method writes data to a document whose ID you choose. If the document already exists, set() without merge options replaces its existing fields with the supplied document data.
val student = hashMapOf(
"name" to "Priya Patel",
"age" to 22,
"course" to "Data Analytics"
)
db.collection("students")
.document("student_102")
.set(student)
.addOnSuccessListener {
println("Student document saved")
}
.addOnFailureListener { exception ->
println("Write failed: ${exception.message}")
}
Custom IDs are useful when a record has a stable identifier, such as a user UID or a known product code. Avoid using predictable IDs as a substitute for security rules.
9.3 Using set() with merge
If you want to add or update supplied fields without replacing unrelated existing fields, use SetOptions.merge().
import com.google.firebase.firestore.SetOptions
val updates = mapOf(
"course" to "Advanced Android Development",
"active" to true
)
db.collection("students")
.document("student_102")
.set(updates, SetOptions.merge())
.addOnSuccessListener {
println("Selected fields saved")
}
Merge behavior is useful when a document contains fields that should remain unchanged. Review nested map behavior carefully when updating complex data.
9.4 Using update() to Modify Existing Fields
The update() method modifies specified fields in an existing document. Unlike set(), update() fails if the target document does not exist.
db.collection("students")
.document("student_102")
.update(
"course", "Kotlin Development",
"active", false
)
.addOnSuccessListener {
println("Student updated successfully")
}
.addOnFailureListener { exception ->
println("Update failed: ${exception.message}")
}
Use update() when you expect the document to exist and need to modify selected fields without replacing the whole document.
10. Retrieving a Single Document
Use document() with the document ID and call get() to retrieve a document once.
db.collection("students")
.document("student_102")
.get()
.addOnSuccessListener { document ->
if (document.exists()) {
val name = document.getString("name")
val course = document.getString("course")
val age = document.getLong("age")
println("Name: $name")
println("Course: $course")
println("Age: $age")
} else {
println("Student document not found")
}
}
.addOnFailureListener { exception ->
println("Read failed: ${exception.message}")
}
How it works
- The application selects the students collection.
- It identifies the document using student_102.
- The get() method requests a document snapshot.
- exists() checks whether the document is present in the returned snapshot.
- Field getter methods retrieve values using the expected data types.
- Success and failure listeners handle the result.
Firestore operations are asynchronous. Avoid assuming the document is available immediately after calling get(). Use the result inside the appropriate callback or convert the operation into a coroutine-based flow where suitable.
11. Retrieving All Documents from a Collection
To retrieve documents from a collection, call get() on the collection reference.
db.collection("students")
.get()
.addOnSuccessListener { querySnapshot ->
for (document in querySnapshot.documents) {
val name = document.getString("name")
val course = document.getString("course")
println("ID: ${document.id}")
println("Name: $name")
println("Course: $course")
}
}
.addOnFailureListener { exception ->
println("Unable to retrieve students: ${exception.message}")
}
This code retrieves the documents matching the collection query at that time. For large collections, avoid downloading every record at once. Use filters, limits, and pagination to control the amount of data returned.
12. Converting Documents into Kotlin Data Classes
Data classes help organize Firestore fields into typed Kotlin objects. They make application code easier to read and maintain.
Define a Student data class
data class Student(
val name: String = "",
val age: Long = 0,
val course: String = "",
val active: Boolean = true
)
Convert a document to an object
db.collection("students")
.document("student_102")
.get()
.addOnSuccessListener { document ->
if (document.exists()) {
val student = document.toObject(Student::class.java)
if (student != null) {
println(student.name)
println(student.course)
}
}
}
Default values can help with object construction, but they do not guarantee that malformed or missing data will be interpreted correctly. Validate important fields and account for inconsistent document schemas.
13. Querying Firestore Data
Queries allow the application to retrieve only the documents that meet specific conditions.
13.1 Filtering with whereEqualTo()
Retrieve students enrolled in Android Development:
db.collection("students")
.whereEqualTo("course", "Android Development")
.get()
.addOnSuccessListener { result ->
for (document in result) {
println(document.getString("name"))
}
}
13.2 Filtering with Numeric Comparisons
Retrieve students whose age is greater than or equal to 21:
db.collection("students")
.whereGreaterThanOrEqualTo("age", 21)
.get()
.addOnSuccessListener { result ->
for (document in result) {
println(document.getString("name"))
}
}
13.3 Sorting Results
Use orderBy() to sort documents by a field:
db.collection("students")
.orderBy("name")
.get()
.addOnSuccessListener { result ->
for (document in result) {
println(document.getString("name"))
}
}
13.4 Limiting Results
Use limit() to retrieve a smaller number of documents:
db.collection("students")
.orderBy("name")
.limit(10)
.get()
.addOnSuccessListener { result ->
println("Retrieved ${result.size()} students")
}
13.5 Combining Filters and Sorting
db.collection("students")
.whereEqualTo("active", true)
.whereGreaterThanOrEqualTo("age", 18)
.orderBy("age")
.limit(20)
.get()
.addOnSuccessListener { result ->
for (document in result) {
println(document.getString("name"))
}
}
.addOnFailureListener { exception ->
println("Query failed: ${exception.message}")
}
Some compound queries require a composite index. If Firestore reports a missing index, follow the generated Firebase Console link to create the required index. Query behavior must also comply with the database's Security Rules.
14. Receiving Realtime Updates
Firestore supports snapshot listeners that notify the application when the observed document or query changes. This is useful for chat screens, task lists, order status, and collaborative applications.
Listen to a collection
val registration = db.collection("students")
.addSnapshotListener { snapshot, error ->
if (error != null) {
println("Listener error: ${error.message}")
return@addSnapshotListener
}
if (snapshot != null) {
for (document in snapshot.documents) {
println(document.getString("name"))
}
}
}
The listener may run initially and again when relevant data changes. Keep the ListenerRegistration returned by the listener and remove it when it is no longer needed.
Remove the listener
registration.remove()
In an Activity or Fragment, manage listener registration according to the relevant lifecycle. In ViewModel-based applications, expose appropriate observable state and clean up listeners when the owning component is cleared or the observation is no longer required.
15. Deleting Data from Firestore
Use delete() to remove a document from a collection.
db.collection("students")
.document("student_102")
.delete()
.addOnSuccessListener {
println("Student deleted successfully")
}
.addOnFailureListener { exception ->
println("Delete failed: ${exception.message}")
}
Deleting a document does not automatically delete its subcollections. If the data model uses nested collections, plan and implement the necessary cleanup explicitly.
16. Using Coroutines with Firestore
Android applications written in Kotlin can use coroutines to make asynchronous database code easier to read. Firebase Task await() support is available through the appropriate kotlinx-coroutines-play-services dependency.
Example repository method
import com.google.firebase.firestore.FirebaseFirestore
import kotlinx.coroutines.tasks.await
class StudentRepository {
private val db = FirebaseFirestore.getInstance()
suspend fun getStudent(
studentId: String
): Student? {
val snapshot = db.collection("students")
.document(studentId)
.get()
.await()
return if (snapshot.exists()) {
snapshot.toObject(Student::class.java)
} else {
null
}
}
}
Add the compatible kotlinx-coroutines-play-services dependency before using await(). Call suspend functions from a suitable coroutine scope, such as viewModelScope, and handle exceptions. Do not perform blocking network calls on the Android main thread.
17. Understanding Offline Data Access
The Firestore Android SDK supports offline persistence in typical configurations. The SDK can cache documents and allow supported reads and writes to work while the device is temporarily offline. Pending writes can synchronize when connectivity returns.
- Offline reads may use cached data rather than the latest server version.
- Offline writes may remain pending until synchronization is possible.
- The application should handle stale data and failed operations gracefully.
- Do not assume a locally visible update has already been committed to the server.
- Design conflict handling for data that may be changed from multiple devices.
Offline behavior depends on the operation, cache state, and SDK configuration. For explicit control over whether a read should use the server, cache, or either source, review the Firestore SDK's source options.
18. Firestore Security Rules
Security Rules are essential for protecting Firestore data. They determine whether a client request can read or modify a document. A working Firebase connection does not automatically grant permission to every database operation.
Example: Restrict access to user-owned documents
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write:
if request.auth != null
&& request.auth.uid == userId;
}
}
}
This example allows a signed-in user to access only the document under users whose ID matches the user's Firebase Authentication UID. It is appropriate only for a data model that follows this ownership structure. Add rules for other collections separately.
Important security practices
- Use Firebase Authentication when data access depends on user identity.
- Validate document fields and allowed value types.
- Restrict writes to authorized users.
- Do not rely on UI checks alone to protect database operations.
- Do not use permanently open read/write rules in production.
- Test rules using the Firebase Emulator Suite or Rules Playground.
Security Rules are not filters. A query must be structured so that all potential returned documents satisfy the rule conditions; Firestore will not simply remove unauthorized documents from a broad query.
19. Error Handling and Troubleshooting
| Problem |
Possible Cause |
Solution |
| PERMISSION_DENIED |
Security Rules reject the operation |
Verify authentication, document ownership, and rules |
| Document does not exist |
Incorrect document ID or deleted record |
Check exists() and verify the document path |
| Data is not appearing |
Wrong project, collection, or field name |
Verify Firebase configuration and document structure |
| Missing index error |
Query needs a composite index |
Create the suggested index in the Firebase Console |
| Network failure |
No internet or temporary connectivity problem |
Handle errors and provide retry options where appropriate |
| Wrong field type |
Stored value does not match the expected type |
Validate data and use the correct field getter |
| Duplicate records |
Repeated calls to add() |
Use a stable document ID or prevent duplicate submissions |
| Unexpected data replacement |
set() used without merge options |
Use merge or update where appropriate |
| Realtime listener keeps running |
Listener not removed when no longer needed |
Manage ListenerRegistration lifecycle correctly |
20. Best Practices for Firestore in Android
- Use a repository: Keep Firestore operations out of UI components where possible.
- Use Kotlin data classes: Model records with clear field types and sensible defaults.
- Choose stable document IDs: Prevent accidental duplicates where the data model requires uniqueness.
- Handle asynchronous results: Show loading, success, empty, and error states.
- Limit large queries: Retrieve data in manageable batches and paginate when necessary.
- Use indexes thoughtfully: Review query requirements and index configuration.
- Protect database access: Apply and test Security Rules.
- Avoid unnecessary reads and writes: Understand document-operation costs and listener behavior.
- Validate data: Check required fields and expected value ranges before saving.
- Manage listeners: Remove observers when they are no longer needed.
- Separate UI and data state: Use ViewModel and lifecycle-aware state collection where appropriate.
- Test offline scenarios: Verify how the application behaves when the network becomes unavailable.
21. Practical Exercises
Exercise 1: Add Student Records
Create an Android screen with fields for student name, age, and course. Save the entered information to the students collection using add(). Show a success message or a useful error.
Exercise 2: Retrieve and Display Records
Retrieve the student collection from Firestore and display the records in a RecyclerView or Jetpack Compose LazyColumn. Handle empty collections and loading states.
Exercise 3: Update a Student
Add an edit screen that loads a student's existing data and updates selected fields using update(). Verify that unrelated fields remain unchanged.
Exercise 4: Filter and Sort Records
Retrieve active students, filter by course, and sort the results by name or age. Add a limit and handle missing-index errors where applicable.
Exercise 5: Realtime Student List
Use addSnapshotListener() to update the list when records change. Ensure the listener is removed when the screen no longer needs it.
22. Mini Project: Student Management Application
Build a small Android application that uses Cloud Firestore for student records.
Project requirements
- Create a form to enter student details.
- Save new student records in Firestore.
- Display saved records in a list.
- Open a record and retrieve its complete details.
- Update student information.
- Delete records with user confirmation.
- Filter students by course and active status.
- Show loading indicators, empty states, and error messages.
- Secure student data using suitable Firebase Authentication and Security Rules.
- Test behavior with slow or unavailable network connectivity.
Suggested architecture
Android UI
|
v
ViewModel
|
v
StudentRepository
|
v
Cloud Firestore
|
v
Firebase Security Rules
This separation makes the application easier to maintain, test, and extend.
23. Interview Questions and Answers
Q1. What is Cloud Firestore?
Answer: Cloud Firestore is a cloud-hosted NoSQL document database that stores application information in collections and documents.
Q2. What is the difference between a collection and a document?
Answer: A collection groups related documents, while a document represents one record containing fields and values.
Q3. What is the difference between add() and set()?
Answer: add() creates a document with an automatically generated ID. set() writes to a specified document ID and can replace existing fields unless merge options are used.
Q4. What is the difference between set() and update()?
Answer: set() creates or writes a document and can replace its data. update() modifies specified fields on an existing document and fails if the document does not exist.
Q5. How do you retrieve a single document?
Answer: Select the collection and document ID, call get(), and inspect the resulting document snapshot.
Q6. What is a snapshot listener?
Answer: It observes a document or query and receives an initial snapshot and subsequent updates when relevant data changes.
Q7. What are Firestore Security Rules?
Answer: They are server-enforced rules that control database access and can validate authentication, authorization, and document fields.
Q8. Does Firestore support offline access?
Answer: Yes. The Android SDK supports offline caching and synchronization for supported operations, although cached data may not reflect the latest server state.
Q9. Why might a Firestore query require an index?
Answer: Some compound queries require indexes to execute efficiently. Firestore reports a missing-index error and provides a link to create the required index.
Q10. How can you prevent duplicate documents?
Answer: Use stable document IDs where uniqueness is required, validate input, and prevent repeated submissions through appropriate UI and application logic.
24. Quick Revision Checklist
- Understand the collection-document data model.
- Create and configure a Firestore database.
- Add the Firebase Firestore dependency.
- Initialize Firestore using FirebaseFirestore.getInstance().
- Store records using add() or set().
- Update existing records using update().
- Retrieve individual documents and collection queries.
- Use filters, sorting, limits, and indexes appropriately.
- Observe realtime changes and remove listeners when finished.
- Handle asynchronous operations and failures.
- Apply appropriate Security Rules and test authorization.
- Consider offline behavior, data costs, and query performance.
25. Conclusion
Cloud Firestore is a powerful option for storing and retrieving structured data in Android applications. Its document-based data model, flexible queries, realtime listeners, and offline support make it suitable for a wide range of mobile projects.
Android developers should understand the differences between add(), set(), update(), get(), queries, and delete(). They should also organize database operations into maintainable repositories, handle asynchronous results, and secure every collection with suitable Security Rules.
By applying these concepts, developers can build reliable applications that create, read, update, and delete cloud data while providing a smooth experience across devices and network conditions.
Additional Learning Resources
Explore Android App Development with JustAcademy
Continue learning Android development and practice building cloud-connected applications.
Explore the Android App Development Course
Register for a Course Demo
Visit JustAcademy Official Website