Fire Up Your App With Firebase Firestore in MVVM Architecture With Jetpack Compose (Part II)
Firestore CRUD Operations Add Firestore storage to the app
CRUD in an acronym representing the data operations
- Create
- Retrieve
- Update
- Delete
CRUD represents the primary data operations. We house the code for CRUD operations on Firebase Firestore in the package “remote” in the Data layer. Firebase provides a Software Development Kit (SDK) to make querying from inside the Android app relatively easy and with only a few lines of code. But queries are only a small part of what we need to integrate Firebase storage into our app. In this article, we will look at the other components following Model-View-View Model (MVVM) architecture.
CRUD Interface Define interface for CRUD operations
First, we define an interface with an abstract function for each query. It is always a good practice to program to an interface. That means that even if implementation details change later, we have a contract for the data operations that specifies the input and the output of each function.
The contractual outputs are described in data models that align with the structure of information in Firestore. These are Kotlin data classes which, together with the interface and implementation, are all part of the package “remote”.
Here is an example of the interface:
package com.example.androidally.play.data.remote
import com.example.androidally.play.domain.model.ChapterInfo
import com.example.androidally.play.domain.model.Quiz
interface FireStoreCRUD {
suspend fun fetchModuleNames(): List<ChapterInfo>
suspend fun fetchModuleContents(name: String): List<Quiz>
}
This code defines an interface named FireStoreCRUD
in the com.example.androidally.play.data.remote
package. The interface declares two abstract functions for interacting with a Firestore database, specifically for fetching data related to our quiz app:
suspend fun fetchModuleNames(): List<ChapterInfo>
: This function is responsible for fetching a list ofChapterInfo
objects.ChapterInfo
is a data model representing information about different chapters or modules within the quiz app. EachChapterInfo
object has attributes describing a chapter such as the chapter title and description. The function is marked assuspend
for asynchronous I/O and it returns aList<ChapterInfo>
, retrieving multiple chapter info objects.suspend fun fetchModuleContents(name: String): List<Quiz>
: This function is responsible for fetching a list ofQuiz
objects based on a specified chaptername
.Quiz
is another data model representing quiz-related information and has attributes including the question, answer, an optional explanatory image, and id of a YouTube video with start time of the track. The function retrieves quizzes associated with the supplied chaptername
. Like the previous function, this one is also marked assuspend
for asynchronous I/O operations. It returns aList<Quiz>
, retrieving multiple quiz objects.
Overall, this interface defines a contract for fetching data related to quiz modules from a Firestore database. The contract is currently limited in scope since we are in the early stages of the project. More functions will be added later. The actual implementation of these functions involves making database queries using the Firebase SDK. This code resides in a concrete class that implements this interface. This separation of interface and implementation allows for flexibility and testability in our code.
We have specified two data models in our interface. Let us look at one of these next.
Data Model Structure document to use in app
Here is the data class ChapterInfoFire
.
package com.example.androidally.play.data.remote
import ...
data class ChapterInfoFire(
val name: String = "",
@PropertyName("Title")
val title: String = "",
@PropertyName("Description")
val description: String = "",
)
The data class ChapterInfoFire
represents a Firestore document. Here’s an explanation of the attributes:
name
(String): This attribute serves as a unique identifier of a chapter or module within our quiz app. Given the Firestore hierarchy of fields within a document within a collection, we stash the name of the document along with the chapter’s title and description, for ease of retrieval of chapter-associated data in Firestore.title
(String): Thetitle
property is annotated with@PropertyName("Title")
to indicate that it corresponds to the “Title” field in our Firestore document. The@PropertyName
annotation allows us to specify the mapping between the property name in our data class and the field name in Firestore. In our app, this property represents the display title or name of the chapter/module.description
(String): Similar totitle
, thedescription
property is annotated with@PropertyName("Description")
. This property corresponds to the “Description” field in our Firestore document, representing a long description of the chapter/module.
In summary, ChapterInfoFire
is a Kotlin data class used to map Firestore documents into a format that our Android app can work with. Each instance of this data class represents a chapter/module in our quiz app and includes properties for the chapter’s name (name
), title (title
), and description (description
). The @PropertyName
annotations indicate how these properties are mapped to specific field names in Firestore documents. When we retrieve data from Firestore, we can use this data model to conveniently work with the retrieved chapter/module information in our app’s code.
We still need to flesh out the abstract function declared in the interface and include the query for data retrieval and any related processing. We will override the function in the implementation. Let’s look at a code snippet that gives a flavor of the querying process.
CRUD Implementation Implement querying functions
Here is a sample code with query and processing of the the result in a try-catch
block:
try {
val querySnapshot = firestore.collection("AndroidAllyContents").get().await()
for (document in querySnapshot.documents) {
var chapterInfoFire = document.toObject<ChapterInfoFire>()
chapterInfoFire = chapterInfoFire?.copy(
name = document.id
)
chapterInfoFire?.let {
moduleNames.add(it.toChapterInfo())
}
Log.d("FIRE_CRUD", "Got chapter id ${chapterInfoFire?.name} title ${chapterInfoFire?.title}")
}
} catch (e: Exception) {
e.printStackTrace()
}
The code block is a Kotlin snippet that uses the Firebase Firestore SDK to retrieve data from our collection “AndroidAllyContents” and then maps the result to a list of ChapterInfo
objects. Here’s an explanation of what each part of the code does:
- Firestore Query:
val querySnapshot = firestore.collection("AndroidAllyContents").get().await()
creates a query to fetch all documents from the Firestore collection “AndroidAllyContents.” It then uses the.get()
method to execute the query and retrieve the results as aQuerySnapshot
. The.await()
function is used with Kotlin Coroutines to suspend execution until the query operation is complete. This is common when working with asynchronous Firestore queries. - Loop Through Documents:
for (document in querySnapshot.documents) {}
iterates through each document (record) returned by the Firestore query. - Document Mapping:
var chapterInfoFire = document.toObject<ChapterInfoFire>()
maps each document toChapterInfoFire
, using thetoObject
method to perform this mapping, Then,chapterInfoFire = chapterInfoFire?.copy(name = document.id)
updates thename
property of thechapterInfoFire
object with theid
of the Firestore document. Thisid
is a unique identifier that is usually automatically assigned by Firestore to each document as a primary key. Here, we have manually provided theid
by concatenating title words into a composite word in camel case. Lastly,chapterInfoFire?.let { moduleNames.add(it.toChapterInfo()) }
checks ifchapterInfoFire
is not null. If it’s not null, it converts thechapterInfoFire
object to a Domain data modelChapterInfo
and adds it to themoduleNames
list for return. - Logging:
Log.d("FIRE_CRUD", "Got chapter id ${chapterInfoFire?.name} title ${chapterInfoFire?.title}")
logs information about each retrieved chapter/module. It displays thename
andtitle
properties of thechapterInfoFire
object in the Android log using the “FIRE_CRUD” tag. - Exception Handling: The entire code block is enclosed within a
try-catch
block to handle any exceptions that may occur during the Firestore query or mapping process. If an exception is thrown, it is printed to the Android log usinge.printStackTrace()
.
In summary, this code retrieves documents from our Firestore collection, maps them to a data model (ChapterInfoFire
), and then converts them into Domain data model (ChapterInfo
) before adding them to a list (moduleNames
) for returning the result. It also logs information about each retrieved chapter/module. Exception handling is included to handle any potential errors during the Firestore query or mapping operations.
Conclusion Takeaways and What's Next
At this point, we have what we need to retrieve information from Firebase Firestore in the Data layer. Where next? In MVVM architecture, the Domain layer houses the business logic whereby data are processed and prepared for display in the Presentation layer. Typically, the manner in which we structure data for storage differs from the manner in which structure data to work with in the app’s business logic. Hence, the Domain layer also provides the data models to use for this purpose. Let’s look at these models next.