Skip to content

xmartlabs/gong

Repository files navigation

Run lints and compile codebeat badge

Gong is Xmartlabs' official Android template project, written in Kotlin, and focused on providing a solid app architecture. One of the main objectives of this project is to supply a good starting point for all new android apps, which lets you move forward fast using the latest Android Components and libraries. We're using "clean architecture" to structure, decouple, expand, and maintain the code.

Table of content

Architecture

There are 4 layers within the application:

  • Domain layer - contains high-level abstraction of the application domain (like repositories, data access) and the use cases, which contain all of the application's business logic & domain rules.
  • Data layer - implements domain layer abstractions, the DataSources, related to data persistence, REST calls, etc.
  • Device layer - implements domain layer abstractions that are not related to data persistence or user interface but are specific to the android platform: android services, cloud messaging, and many others.
  • Presentation (UI) layer - all the functionality related to the Android user interface: built on top of Jetpack Compose.

Overview

In order to understand how this work together, it's important to talk about each component's role inside the presentation layer.

First of all, the architecture. When jetpack compose is used, it's convenient to use an architecture based on an state. In this case, a combination of MVI and Redux patterns was chosen. The MVI pattern has three main components: Intent, Model, and View. The intent refers to the intention to change the state of the app, so in Gong's case, it would be the actions, delivered then to ViewModel. ViewModel holds the model component of the pattern. It is responsible of creation of a new state, which is an immutable data structure. At any given moment, there is only one state in the app, which represents a single source of truth. The only way to change the state is to create a new one, triggered by the actions. But when and how is the new state created? The Redux pattern comes up at this point. Redux is a pattern and library for managing and updating application state, using events called "actions". More precisely, the main components of Redux are State, Action, and Reducer. In Gong the composables communicate actions to the viewModels so they can manage and emit the state back to view. This ensure state can only be updated in a predictable way. Then, inside the model, the reducer is called with a proper action and the latest state and forward its result as an output value of the model. Reducer is a function that takes the previous state and action and creates a new state, and in Gong this role is played by the function processAction, located in viewModels.

To continue with the insight of the project, let's see how this is done. With the shared flow, actions are broadcast to an unknown number (zero or more) of subscribers. In the absence of a subscriber, any posted action is immediately dropped. It is a design pattern to use for actions that must be processed immediately or not at all.

The ViewModel handles each action in the processAction method. Whenever an action is added to the "contract", it also has to be added here. So all actions can be managed from the same place. With the channel, each event is delivered to a single subscriber. An attempt to post an event without subscribers will suspend as soon as the channel buffer becomes full, waiting for a subscriber to appear. Posted events are never dropped by default. Then to handle this Ui effects and all things that should be displayed only once, "oneShotEvents" are used. Because Channels are hot and it is not necessary to show side effect again when orientation changed or UI become visible again.

Finally, for handling UiState, StateFlow is used. StateFlow is a state-holder observable flow that emits the current and new state updates to its collectors, similar to a LiveData but with an initial value. So a state is always present. It's also a kind of SharedFlow. It's always expected to receive last view state when UI become visible.

Gong's Workflow example:

Layers components and roles

Now, as a way to give you an overview of the other layers and how the interaction with the presentation layer is done, let's review it's components.

To the presentation layer, the UseCases are the ones who resolve each invocation from the ViewModels, and they both interact using coroutines library. A UseCase is a reusable component that might be used from different ViewModels. The same goes for Repositories, a repository can stand on its own without the ViewModel and be re-used from different use cases. All these classes exist with a clear goal and purpose. The logic is split sensibly. It is worth to say that these repositories refer to those of the repository pattern. Repository design pattern facilitates de-coupling of the business logic and the data access layers in your application with the former not having to have any knowledge on how data persistence would actually take place. Repositories have the function of communication between Domain Layer and Data Layer. More precisely, with coroutines help, they have to implement the necessary logic so they can call Remote and Local sources methods.

At the end of the chain, as mentioned, Data Layer is found. It is responsible for persisting and obtaining all the data required for the model using different sources. Repositories use the store library to combine those different sources. The remote sources are the ones who manage interaction with the different endpoints. The local sources manage data base logic.

The core library for the communication between layer components is: Coroutines, used to perform all background tasks.

Core Libraries

The main libraries that we are using are:

  • Android Architecture Components - Jetpack:
    • Jetpack Compose which is the library used by the UI with all its composables.
    • ViewModel which allows you navigate between composables while taking advantage of the Navigation component’s infrastructure and features.
    • Android Navigation Component used to navigate across different pieces of content within your app.
    • Room, a SQLite object mapping library.
  • Coroutines for asynchronous programming
  • Coil, an image loading library for Android backed by Kotlin Coroutines.
  • Koin, a lightweight dependency injection framework for Kotlin.
  • OkHttp and Retrofit for network communication.
  • Store, helps to manage the loading of data from different sources.
  • AndroidSwissKnife a set of extensions, helpers, and useful classes.
  • Timber one of the most popular loggers on Android.
  • Stetho, a sophisticated debug bridge for Android applications.
  • LeakCanary, a memory leak detection library for Android.
  • AndroidSnapshotPublisher, one of the most important tools used in the QA process, it's a Gradle plugin that prepares and distributes deliverable versions easily when they are ready to test.

Setup

To use this template, you can use the gong_setup.sh script that automatizes the setup process. You can run it remotely executing the following command:

bash <(curl -s https://raw.githubusercontent.com/xmartlabs/gong/main/gong_setup.sh)

It will clone and setup all variables that you need. If you prefer to do it manually, you have to follow these steps:

  • Clone the project
  • Update the applicationId in the app's build gradle file.
  • Change the package structure based on your application id.

Configuration and secrets

The app's version name is defined in the project's Gradle file. The app's version code is autogenerated based on the app's version name.

You have two files to define your constants: config.properties which stores all of the app's configuration, like the backend's base URL, for example. secrets/keys.properties which contains all of the secrets in your app, like a given API key for a third party service. That environment's variables are injected in the app's build.gradle, and they are accessible via the BuildConfig generated file. The app access to that variables using the Config file.

The keystores are stored in the secrets folder, which is not tracked in git.

The library versions are managed in a versions Gradle file

Product Flavors

The app uses two flavors, one for production (prod) and another for development (dev) build.

Each flavor defines and application class (App.kt), that is used to define custom configurations in each one. For example, the navigation logger listener is defined only for development builds.

What's next?

For an answer to this question you can check the current project status and if you happen to come up with a new idea you can always open a new issue!

About

Made with ❤️ by XMARTLABS