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.

The package "remote" in the Data layer houses the client-side code for CRUD operations on remote storage. It includes the data models that align with how data are structured in remote storage location.
The package “remote” in the Data layer houses the client-side code for CRUD operations on remote storage. It includes the data models that align with how data are structured in remote storage location.

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:

  1. suspend fun fetchModuleNames(): List<ChapterInfo>: This function is responsible for fetching a list of ChapterInfo objects. ChapterInfo is a data model representing information about different chapters or modules within the quiz app. Each ChapterInfo object has attributes describing a chapter such as the chapter title and description. The function is marked as suspend for asynchronous I/O and it returns a List<ChapterInfo>, retrieving multiple chapter info objects.
  2. suspend fun fetchModuleContents(name: String): List<Quiz>: This function is responsible for fetching a list of Quiz objects based on a specified chapter name. 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 chapter name. Like the previous function, this one is also marked as suspend for asynchronous I/O operations. It returns a List<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:

  1. 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.
  2. title (String):  The title 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.
  3. description (String): Similar to title, the description 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:

  1. Firestore Queryval 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 a QuerySnapshot. 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.
  2. Loop Through Documents: for (document in querySnapshot.documents) {} iterates through each document (record) returned by the Firestore query.
  3. Document Mapping: var chapterInfoFire = document.toObject<ChapterInfoFire>() maps each document to ChapterInfoFire, using the toObject method to perform this mapping, Then, chapterInfoFire = chapterInfoFire?.copy(name = document.id) updates the name property of the chapterInfoFire object with the id of the Firestore document. This id is a unique identifier that is usually automatically assigned by Firestore to each document as a primary key. Here, we have manually provided the id by concatenating title words into a composite word in camel case. Lastly, chapterInfoFire?.let { moduleNames.add(it.toChapterInfo()) }  checks if chapterInfoFire is not null. If it’s not null, it converts the chapterInfoFire object to a Domain data model ChapterInfo and adds it to the moduleNames list for return.
  4. Logging: Log.d("FIRE_CRUD", "Got chapter id ${chapterInfoFire?.name} title ${chapterInfoFire?.title}") logs information about each retrieved chapter/module. It displays the name and title properties of the chapterInfoFire object in the Android log using the “FIRE_CRUD” tag.
  5. 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 using e.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

The Domain layer houses the business logic whereby data are processed and prepared for display in the Presentation layer. Storage technology dictates how data are structured in the Data layer, which is often not how we want data structured to work with in the Domain layer. Hence the Domain layer provides data models to make the data "fit for purpose".
The Domain layer houses the business logic whereby data are processed and prepared for display in the Presentation layer. Storage technology dictates how data are structured in the Data layer, which is often not how we want data structured to work with in the Domain layer. Hence the Domain layer provides data models to make the data “fit for purpose”.

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.