-
Notifications
You must be signed in to change notification settings - Fork 709
Architecture Overview
The app architecture has three layers: a data layer, a domain layer and a UI layer.
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.
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.
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.
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.
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.
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.
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.
// TODO:: Will be updated soon
- Self Service API - https://demo.mifos.io/api-docs/apiLive.htm#selfbasicauth
- Join Firebase Android App Testing - https://appdistribution.firebase.dev/i/87a469306176a52a
- Kotlin Multiplatform - https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html
- JetBrains Toolbox - https://www.jetbrains.com/toolbox-app/
- Compose Multiplatform - https://www.jetbrains.com/compose-multiplatform/
- Fastlane - https://docs.fastlane.tools/