A template for creating Android projects at Q42.
main
branch of this template. This helps both your project and this template to stay up to date. python ./scripts/rename-project.py
or python3 ./scripts/rename-project.py
from the
project root, to change the project name and the package names. The script will ask for your new
project name and update all references.To contribute, simply create a PR and let developers from other projects approve and merge it. A note on changes:
Only basic features that almost all projects use, were added in this template:
If you need local signing of release builds, copy the upload keystore into the root of your project, and call it 'upload-keystore.jks'. Note that you probably don't need to use local signing. The CI build setup builds and signs your app for you.
This project uses Github Actions for CI. We have added these workflows for now:
devDebug
apk and a prodDebug
apk buildprodRelease
bundle and prodRelease
apk)Run the below command to generate an encoded text representation of your keystore. If you don't have one, you can generate a new keystore.
openssl base64 < upload-keystore.jks | tr -d '\n' | tee keystore.base64.txt
The above command will generate a new file called keystore.base64.txt
. Open the file, copy the
contents and save it to your repo's github secrets in the variable KEYSTORE_BASE_64.
Also add these repository secrets in github, and store them in your projects 1Password vault as well:
We use a firebase app github action ( in our github actions setup) to automatically upload release builds to firebase.
To change this to your own firebase project, you need to set two secrets:
FIREBASE_PROD_APP_ID
from your firebase project settings. It's in your firebase project
settings (for the app you want to publish), it looks something like 1:xxxxxxxxx:android:yyyyyyyy
FIREBASE_CREDENTIALS
from your firebase project. More info how to obtain it can be found
in creating a service account.You can run the worlkflows on our self-hosted runner (a mac mini). This is:
In a private repo you can enable the use of our self-hosted runner using [self-hosted, macOS]
as desribed in the .github/workflows/\*.yml
files. If you need more help:
the documentation is in Notion.
For security reasons, using the mac mini is not possible on a public repo (like this template repo).
Using Bitrise or another CI tool instead? Then you can skip the above and delete this folder:
./.github/workflows
We use Clean Architecture, to a very large extend. Our setup:
Data Transfer Models (DTO) are preferably generated from server source code/json/schemas and do not contain any changes compared to the server contract.
Model Entities live in the data layer and are the local storage objects.
Models map from XDTO to XEntity to X and then probably into a viewstate. Mapping is always done in the outside layer, before being transported to the inner layer. See the diagram for more info.
Core modules are used for common logic that is shared between features and is not related to any feature or model. If your core-feature is 1-2 classes only and most modules will use it, putting it in Core:utils is simpler than creating a separate module. It is the most general module in the diagram, please keep it small.
Examples of logic that can live in core:utils:
In January 2023: We wrote gradle files in KTS. But many things are cumbersome using KTS, like sharing gradle files between modules in a includeBuild: adds more steps to take per shared file. We rolled it back to Groovy, let's try again in a year?
To share gradle config over modules and make adding more modules as simple as possible, we have 2 base files:
build.module.feature-and-app.gradle
for all feature modules and the app modulebuild.module.library.gradle
for all other modules (library modules)Also, feature-specific gradle files are set up, like build.dep.compose.gradle
.
When a feature encompasses adding multiple dependencies and/or config and you might re-use it,
please add a separate gradle file for it.
We do not use non-android modules because we want to use Hilt across all modules. We prefer to clarity over faster compile time, for now.
Other options we considered and denied:
If you use this template to create a white label app, then you might be better off with option 1. Then you actually might want the wiring to be outside the domain and data modules.
We use Hilt, because compared to Dagger, it's simpler to get into for new developers. It also reduces the amount of code you need to write for DI. Because we use Hilt, and do not want the domain layer to know the data layer, app must know all modules.
We use a version catalog (toml) file for versioning. This is the latest feature from Gradle currently. It can be shared over (included) projects.
ViewStateStrings enables you to move String logic from the View to the ViewModel, especially plurals or replacement parameter logic.
Use @PreviewLightDark
to generate two previews from one function, one dark and one light. You
probably also want to
wrap your preview content with a PreviewAppTheme{..}
so that your previews have the correct theme
background.
BuildConfig (or gradle build type configuration) is only allowed in the app module,
for clarity and to avoid bugs
. If you need the config in a different module, use dependency injection and our app/ConfigModule
.
We use compose destinations for navigation because it's low risk, high gain:
We added extra logic to navigate from ViewModel . To enable this for your ViewModel:
private val navigator: RouteNavigator
to the constructor of your ViewModel.RouteNavigator by navigator
InitNavigator(navigator, viewModel)
from your screen.Call navigateTo(destination)
from your ViewModel to navigate somewhere. There are also popUpTo
methods, etc.
You can call navigateTo with a AppGraphRoutes
(in core.navigation) to navigate to the root of a
different
graph route.
We choose Retrofit over Ktor because we already use it in all of our projects and have a positive experience with it. Retrofit has many more features, but does not support Kotlin Multi Platform ( yet).
We use Kotlinx.serialization for all json parsing because it is fast, modern and has IDE integration (which warns you when you forget to add @Serializable annotations). It also has multiplatform support, so we can use it in our KMP projects as well.
We use Napier because it's usage is close to Timber/Tolbaaken, but Napier supports KMM.
We use Crashlytics for crash reporting. Note that Google Analytics is not added. Google [recommends] (https://firebase.google.com/docs/crashlytics/get-started?platform=android#before-you-begin) to enable it for more insight such as breadcrumbs and crash-free percentage. If you don't want to use Google Analytics, you can remove it by simply removing the dependency.
We did not include an image loading library is this template, because not every app might need it, but we do have a suggestion. Use Coil or Landscapist-Coil. Currently, Glide had memory issues with compose at HEMA.