Fire Up Your App With  Firebase Firestore in MVVM Architecture With Jetpack Compose (Part V)

Repository Open a door to the Data layer 

Fire Up Your App With  Firebase Firestore in MVVM Architecture With Jetpack Compose (Part V)

We have coded CRUD operations and provided data models in Data layer to support I/O. We have modeled data to align with domain business logic and housed these models in the Domain layer for other layers to use. We have provided mapping functionality between sets of data models. So are we ready to access data?

There is some more work (!) to do, in order that we build a professional app that is responsive and doesn’t throw up surprises when contingencies occur.

We want to be mindful of the following:

  1. Remote I/O operations take an undefined amount of time and are unpredictable due to contingencies such as poor internet connectivity, insufficient access privilege or remote service outages. We need to account for these contingencies in our app.
  2. We want to offer a responsive User Experience (UX) that doesn’t keep the user waiting or require unnecessary clicks for user action to be completed. 

The repository pattern is where this management overhead resides. At the very least, we implement the following features as we open the door to data in our app:

  • Exception Handling: We wrap I/O operations in a try-catch block and print diagnostic error messages. We can further enhance this to look for specific errors and fine-tune how we handle contingencies.
  • Event Emissions: We emit results, whether success or failure, as events to take advantage of Kotlin flows in our app. Streaming events in this ways offers a superbly elegant way to serve content in a dynamically updating UI that is listening for events.

The effect is magical!

Data Contract Define the contract in Domain layer

Let’s look at sample code. Following the pattern of programming to an interface, we define the repository’s interface in the Domain layer in the package repository. Here is the interface, which resides in package “repository” in the Domain layer. 

	interface QuizRepository {
    suspend fun getModuleNames(
        fetchFromRemote: Boolean
    ): Resource<List<ChapterInfo>>

    suspend fun getModuleContents(
        name: String
    ): Flow<Resource<List<QuizQuestion>>>
}	
			
		    		
	    

The code defines an interface named QuizRepository that outlines the functions required for interacting with data related to the quiz modules. The functions are designed for use for asynchronous I/O in Kotlin Coroutines. They can take advantage of Kotlin flows to sequentially emit events in place of one return value. Let’s unpack the functions defined in this interface:

  1. suspend fun getModuleNames(fetchFromRemote: Boolean): Resource<List<ChapterInfo>>: This function fetches a list of module names (representing different sections or chapters) that are part of the quiz app. It is marked with suspend to indicate that it should be called from a coroutine or another suspending function, as it is designed to perform network I/O operations asynchronously. It takes a Boolean parameter fetchFromRemote, which suggests whether the data should be fetched from a remote data source (e.g., a server) or if it can be retrieved from a local cache or other sources. The exact behavior can be fine-tuned in the implementation. It returns a Resource object wrapping a List of ChapterInfo. The Resource class is often used to represent the result of asynchronous operations and can hold data, loading states, or error messages.
  2. suspend fun getModuleContents(name: String): Flow<Resource<List<Quiz>>>: This function is responsible for fetching the contents of a specific quiz module based on its name. It is also marked with suspend, indicating it’s a suspending function suitable for asynchronous operations. It takes a name parameter representing the identifier of the quiz module to retrieve. It returns a Flow of Resource wrapping around a List of Quiz objects. A Flow is a cold asynchronous data stream that emits values over time. The Resource class is designed to wrap around a generic type so as to encapsulate any result type from a successful I/O operation. 

In summary, the QuizRepository interface defines functions to fetch module names and module contents. These functions return data wrapped in a Resource object, indicating the status of the operation (e.g., success, loading, or error). The implementation of such an interface provides the actual logic for fetching data, including network requests, database queries, or other data retrieval methods, using modulating flags like fetchFromRemote to tune the behavior. This structure allows for flexibility in handling data sources and provides a clean separation of concerns in the app’s architecture.

Data Access Flesh out the contract in Data layer

Being in the Domain layer, the interface only knows the data models in the home layer. The implementation however requires use of assets we have built out in the Data layer. Hence, we put the implementation in the package “repository” in the Data layer. 

Do you see how the separation of concerns works? The repository opens a door to the Data layer and defines the contract in the interface. The contract, in the Domain layer, is written in the language of the domain. The contract’s implementation resides in the Data layer. We can choose to change the implementation should we make changes in the Data layer, for example, cache remote data locally. These changes need not affect the contract, wherein this detail is abstracted away.

Now let’s look at a concrete function where the I/O operation is implemented.

	 override suspend fun getModuleContents(name: String): Flow<Resource<List<QuizQuestion>>> {
	 return flow {
            emit(Resource.Loading(true))
            try {
                val knowledgeBank = fireStoreCRUD.fetchModuleContents(name)
                emit(Resource.Success(data = knowledgeBank))
            } catch (e: Exception) {
                emit(Resource.Failure(message = e.message ?: "Got not contents for ${name} in Firestore!"))
            } finally {
                emit(Resource.Loading(false))
            }
        }
    }	
			
		    		
	    

Here, we have an implementation of the getModuleContents function within the repository, which fetches the contents of a specific quiz module. This function returns a Flow of Resource<List<Quiz>>. Let’s break down how it works:

  1. override suspend fun getModuleContents(name: String): Flow<Resource<List<Quiz>>> { ... }: This function is marked with override as it provides the concrete implementation of the abstract suspend function of the same name in the interface.
  2. return flow { ... }: The function returns a Kotlin Flow, which is a cold asynchronous data stream that emits values over time. The emissions use the Resource class to represent the state of the I/O operation – whether success, failure or work-in-progress.
  3. emit(Resource.Loading(true)): The initial emission within the flow indicates that the operation is in a loading state, with Resource.Loading(true) signaling that loading has started.
  4. try { ... } catch (e: Exception) { ... } finally { ... }: The core logic of the function is enclosed within a try-catch-finally block to handle different scenarios during the data retrieval process.
  5. val knowledgeBank = fireStoreCRUD.fetchModuleContents(name): Inside the try block, it attempts to fetch the contents of the specified quiz module by calling function fetchModuleContents from a fireStoreCRUD instance passed to the repository via dependency injection. The function is responsible for making a request to the Firestore database. 
  6. emit(Resource.Success(data = knowledgeBank)): If the data retrieval is successful (i.e., no exceptions are thrown), it emits an event of type Resource.Success with the fetched List<Quiz> in variable knowledgeBank as the data payload. This indicates that the operation was successful, and makes the retrieved data available to the consumers of this Flow.
  7. catch (e: Exception) { ... }: If an exception is thrown during the data retrieval process, it catches the exception. It then emits an event of type Resource.Failure with an error message derived from the exception, or a default message if no message is available. This indicates that there was a failure in retrieving the data.
  8. finally { ... }: The finally block is used to ensure that the loading state is correctly updated, regardless of whether the data retrieval was successful or encountered an error. It emits an event of type Resource.Loading(false) to signal that the loading has completed.

In summary, this function returns a Flow that emits a sequence of Resource objects to represent the status of the data retrieval operation. It starts by emitting a loading state, then attempts to fetch the data. If successful, it emits a success state with the retrieved data; if an error occurs, it emits a failure state with an error message. Finally, it emits a loading state indicating that the operation has completed, whether successfully or with an error. This design allows the UI to respond to different states (loading, success, failure) and appropriately display the data or error messages to the user.

Resource Create class Resource for state emissions

A traditional function returns a single result at the end of the operation. By contrast, a Kotlin Flow allows us to emit events asynchronously as many times as needed in an operation. A sealed class is commonly used for handling different states or outcomes of such an asynchronous operation, when making network requests or performing database queries.

	sealed class Resource<T>(val data: T? = null, val message: String? = null) {
    class Success<T>(data: T?): Resource<T>(data)
    class Failure<T>(message: String, data: T? = null): Resource<T>(data, message)
    class Loading<T>(val isLoading: Boolean = true): Resource<T>(null)
}	
			
		    		
	    

Here, we have defined a sealed class named Resource<T> that has three subclasses:

  1. Success<T>: This subclass represents a successful outcome of an operation. It contains a nullable data attribute to hold the result of the operation, which is of a generic type T. It inherits from the Resource<T> class, passing the data to its parent class constructor
  2. Failure<T>: This subclass represents a failed outcome of an operation. It contains a message attribute, which can hold an error message or description, and an optional data attribute that can contain additional data related to the failure. It also inherits from the Resource<T> class, passing both data and message to its parent class constructor.
  3. Loading<T>: This subclass represents a loading or in-progress state of an operation. It includes a isLoading attribute, which is a boolean indicating whether the operation is still loading. It does not carry any data or message, so it passes null to the parent class constructor.

By using this sealed class, we can create instances of Resource to represent different states of our app’s operations, making it easier to handle and communicate the results, errors, and loading states in a consistent and safe manner throughout your app. We will use this class in ViewModel to provide real-time updates to our UI based on the state of asynchronous operations.

Conclusion Takeaway and Next Steps

Fire Up Your App With  Firebase Firestore in MVVM Architecture With Jetpack Compose (Part V)

Recap: 

  1. Create a repository interface in the package “repository” in Domain layer, which will be used by the Presentation layer.
  2. Implement the repository interface in the package “repository” in the Data layer.
  3. Implement sealed class Resource in package “util”.

Now we are ready to use the repository access data in the Presentation layer. However, before we can use the repository in View Model, there is one more set up we need to take care of. This is, setting up the repository for injection into the View Model using Dagger-Hilt!