Skip to content

Architecture Guidelines

Aditya3609_@ edited this page Dec 26, 2024 · 6 revisions

Architecture

Overview

The app architecture has three layers: a data layer, a domain layer and a UI layer.

architecture overview

The architecture follows a reactive programming model with unidirectional data flow. With the data layer at the bottom, the key concepts are:

  • Higher layers react to changes in lower layers.
  • Events flow down.
  • Data flows up.

The data flow is achieved using streams, implemented using Kotlin Flows.


Data layer

The data layer is where all the UI-independent data is stored and retrieved. It consists of raw data sources and higher-level "repository" and "manager" classes.

Note that any functions exposed by a data layer class that must perform asynchronous work do so by exposing suspending functions that may run inside coroutines while any streaming sources of data are handled by exposing Flows.

Diagram showing the data layer architecture

Each repository has its own models. For example, the BeneficiaryRepository has a Beneficiary model and the InvoiceRepository has a Invoice model.

Repositories are the public API for other layers, they provide the only way to access the app data. The repositories typically offer one or more methods for reading and writing data.

In some cases a source of data may be continuously observed and in these cases a repository may choose to expose a StateFlow that emits data updates using the DataState wrapper.

Data Sources

The lowest level of the data layer are the "data source" classes. These are the raw sources of data that include data persisted in to Datastore, data retrieved from network requests using Ktorfit, and data retrieved via interactions with the Mifos Fineract Backend.

Note: that these data sources are constructed in a manner that adheres to a very important principle of the app: that function calls should not throw exceptions (see the style and best practices documentation for more details.) In the case of data sources, this tends to mean that suspending functions like those representing network requests should return a Result type. This is an important responsibility of the data layer as a wrapper around other third party libraries, as dependencies like ktorfit and Ktor tend to throw exceptions to indicate errors instead.

Repositories

Repository classes represent the outermost level of the data layer. They can take data sources, managers, and in rare cases even other repositories as dependencies and are meant to be exposed directly to the UI layer. They synthesize data from multiple sources and combine various asynchronous requests as necessary in order to expose data to the UI layer in a more appropriate form. These classes tend to have broad responsibilities that generally cover a major domain of the app, such as authentication (AuthenticationRepository) or self service (SelfServiceRepository).

Repository classes also feature functions that do not throw exceptions, but unlike the lower levels of the data layer the Result type should be avoided in favor of custom sealed classes that represent the various success/error cases in a more processed form. Returning raw Throwable/Exception instances as part of "error" states should be avoided when possible.

In some cases a source of data may be continuously observed and in these cases a repository may choose to expose a StateFlow that emits data updates using the DataState wrapper.


Domain layer

The domain layer contains use cases. These are classes which have a single invocable method (operator fun invoke) containing business logic.

These use cases are used to simplify and remove duplicate logic from ViewModels. They typically combine and transform data from repositories.

For example, LoginUseCase combines and update data from the AuthenticationRepository and ClientRepository to log in a user.

Notably, the domain layer in this project does not (for now) contain any use cases for event handling. Events are handled by the UI layer calling methods on repositories directly.


UI Layer

architecture-4-ui-layer

The UI layer adheres to the concept of unidirectional data flow and makes use of the MVVM design pattern. Both concepts are in line what Google currently recommends as the best approach for building the UI-layer of a modern Android application and this allows us to make use of all the available tooling Google provides as part of the Jetpack suite of libraries. The MVVM implementation is built around the Android ViewModel class and the UI itself is constructed using the Jetpack Compose, a declarative UI framework specifically built around the unidirectional data flow approach.

Each screen in the app is associated with at least the following three classes/files:

  • A ...ViewModel class responsible for managing the data and state for the screen.
  • A ...Screen class containing the Compose UI implementation.
  • A ...Navigation file containing the details for how to add the screen to the overall navigation graph and how navigate to it within the graph.

ViewModels / MVVM

In the Model-View-ViewModel (MVVM) architecture, the ViewModel serves as a bridge between the UI components and the data layer, managing UI-related data in a lifecycle-conscious manner. This separation ensures that the UI remains responsive and resilient to configuration changes such as screen rotations.

Key Responsibilities of ViewModels:

State Management: ViewModels hold and manage UI-related data, ensuring that the UI reflects the current state of the application. They expose this data to the UI layer, often through observable data holders like StateFlow or LiveData, enabling the UI to react to data changes.

Business Logic Handling: While the domain layer contains the core business logic, ViewModels can handle UI-specific logic, such as form validation or user input processing, to prepare data for display.

Event Handling: ViewModels process user interactions by invoking appropriate use cases or repository methods, facilitating a clear separation between the UI and data layers.

Integration with Jetpack Compose:

In Jetpack Compose, ViewModels play a crucial role in managing and providing state to composable functions. By collecting data from StateFlow or LiveData within composables, the UI can automatically recompose in response to state changes, maintaining synchronization between the UI and underlying data.