Fire Up Your App With Firebase Firestore in MVVM Architecture With Jetpack Compose (Part IV)
Business Logic Model data for ingestion into the app
We define data models in the Data layer for storage and separate data models in the Domain layer for implementing the core business logic of our app. Storage can take various forms, such as when we introduce caching, requiring its own set of data models in the Data layer for local database operations.
But why the need for distinct data models? The choice of storage technology significantly influences our data design. In the case of relational databases, like Room DB for caching, data must conform to an atomic entity structure. Consequently, we place the corresponding data models in the package “local” within the Data layer and employ them for CRUD operations.
In contrast, NoSQL databases, like Firebase Firestore, offer more flexibility. Firestore’s data organization revolves around collections of documents, where each document contains named slots for numeric or textual data. This organizing principle is reflected in the design of our data models, which we place in the package “remote” within the Data layer.
The Domain layer harmonizes over differences by presenting data models in a manner closer to how we typically conceptualize domain entities, ensuring they fit seamlessly into our app’s business logic. We introduce a package “mapper” in the Data layer to house the code for mapping between these different sets of models. We house this code in the Data layer, adhering to the principle that the Domain layer may not know about other layers but the other layers know about Domain layer.
Mapper Write extension functions for mapping
We implement a mapper as an extension function of the source class that returns an object of the target class. This one-one mapping is fairly typical. The simplicity of business logic in your use-case means we do not need to deal with nested data structures, complex joins or aggregates.
Here is a sample mapper function.
fun ChapterInfoFire.toChapterInfo(): ChapterInfo {
return ChapterInfo(
name = name,
title = title,
description = description
)
}
The ChapterInfoFire.toChapterInfo()
function is an extension function that maps an instance of the ChapterInfoFire
data class into an instance of the ChapterInfo
data class. In Kotlin, an extension function allows adding new functionality to an existing class without modifying its source code.
Here’s a breakdown of how the function works:
ChapterInfoFire.toChapterInfo()
: This is the function signature, indicating that it’s an extension function for theChapterInfoFire
class, and it will return aChapterInfo
object.return ChapterInfo(...)
: The function returns a newChapterInfo
object, and within the parentheses, it initializes the properties of theChapterInfo
object.name = name, title = title, description = description
: Here, it’s copying the values of the properties from theChapterInfoFire
object to the corresponding properties in theChapterInfo
object. Thename
,title
, anddescription
properties in theChapterInfo
object are set equal to the values of the same-named properties in theChapterInfoFire
object.
In summary, this extension function simplifies the process of converting a ChapterInfoFire
object into a ChapterInfo
object by copying the values from one to the other.
This example shows how we map data from the Data layer to the Domain layer. It allows seamless data flows while ensuring separation of concerns in MVVM architecture.
Conclusion Takeaways and Next Steps
Recap:
- Create data models in the “remote” package in Data layer for Firestore CRUD operations.
- Create data models in the “domain” package in Domain layer to use in the app’s business logic.
- Implement mapper functions in the “mapper” package to map data models between Data and Domain layers.
We have now set the stage for ingestion of data into the app. But we have yet to address all aspects of data management for a professional app. For example, our query may fail due to a poor network connection. We need to address this possibility and ensure the app handles off-the-happy-path scenarios gracefully.
Another important consideration is, we want to use Kotlin’s data structures to manage data flows to deliver a responsive user experience that doesn’t keep the user waiting and minimizes or eliminates unnecessary clicks. In other words, we need a repository pattern. That’s coming up next.