Skip to content

Q42/Template.Android

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Template.Android

A template for creating Android projects at Q42.

Using this template

  1. Click the green "Use this template" button in the Github repo to create your own repository with this code template.
  2. Clone the new repo locally on your machine.
  3. Update dependencies and check whether everything still works. After that, create a PR to the main branch of this template. This helps both your project and this template to stay up to date.
  4. Run 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.
  5. Replace google-services.json with your own Firebase config file. The template Firebase project can be found here: Firebase project Google Services currently in use:
    • Crashlytics
  6. Setup your signing key secrets for usage in automated builds, . Steps are described in CI / Automated Builds
  7. You probably want to enable the self-hosted runner (our own, free and fast, mac-mini), as described in self hosted runner, when using Github Actions.
  8. Setup/adjust uploads to Firebase app distribution.
  9. Build your awesome project :-)
  10. Consider contributing to this template when you find something that could be improved.

Contributing

To contribute, simply create a PR and let developers from other projects approve and merge it. A note on changes:

  • Changes should be practical and pragmatic. Do not add complexity for the sake of aesthetics.
  • Additions should only be added if 90%+ of our projects use it.
  • In your PR description, please include a section: "Why is this important".

Wishlist for changes / new features

  • add this wishlist as issues in Git, so we can self-assign, etc. :) + remove it here.
  • small fix: don't use ENV vars in the build scripts, but sercrets.properties instead. Example from my project here
  • on most projects we only use one module for data, and one for domain. We could simplify the template by removing the data and domain modules and calling them 'data/main' and 'domain/main', in that way more modules can easily be added, but are not added by default.
  • Research android screen transition standards + update our animations
  • Switch build files to KTS, also build some custom plugins.
  • Use lifecycle events from Google (we use our own now)
  • Use preview light dark from google (we use our own now)
  • Optionally: Setup proper Typography in AppTheme, using custom (non-material) Typography keys, because that is what we have in 90%+ of our projects.
  • Optionally: Add compose events from VM to View (https://github.com/leonard-palm/compose-state-events) and showcase with snackbar or toast.
  • Optionally: Switch from retrofit to ktor (because Ktor is multiplatform)

Features

Only basic features that almost all projects use, were added in this template:

  • compose
  • theming
  • screen navigation (inc. multi-module navigation and navigation initiated from ViewModel)
  • dependency injection
  • networking
  • json parsing
  • action results
  • clean architecture
  • automated dependency updates

Project Setup

Signing Builds

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.

CI / Automated Builds

This project uses Github Actions for CI. We have added these workflows for now:

  • debug.yml: any commit on any branch triggers a devDebug apk and a prodDebug apk build
  • release.yml: any PR triggers signed release builds (prodRelease bundle and prodRelease apk)

Adding your own keystore's details on Github Actions

  • First create your own keystore and store it in 1pw. You do not add this keystore to the repo ( like we did in this template as an example).
  • add a KEYSTORE_BASE_64 new github repository secret that is the encoded text representation of the upload keystore of your app. CI will later decode it into a keystore file and then make it available for the system to sign the app.

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:

  • RELEASE_KEYSTORE_PASSWORD ('firstpass' for this template)
  • RELEASE_KEYSTORE_ALIAS ('template' for this template)
  • RELEASE_KEY_PASSWORD ('secondpass' for this template)

Firebase app distribution

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.

Self-hosted runner

You can run the worlkflows on our self-hosted runner (a mac mini). This is:

  • 2-3 times faster (compared to building with Github hosted runners)
  • a lot cheaper

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).

Removing Github Actions

Using Bitrise or another CI tool instead? Then you can skip the above and delete this folder:

  • ./.github/workflows

Setup decisions

Clean architecture

We use Clean Architecture, to a very large extend. Our setup:

Q42-CA

Clean Architecture layers

  • UI with Jetpack Compose
  • Presentation with the ViewModel
  • Domain for domain models, UseCases and other domain logic.
  • Data for data storage and retrieval.
Clean Architecture Module Dependencies
  • All dependency arrows point towards Domain. Domain has no dependencies on other modules. This is a Clean Architecture rule. Domain is the most stable module in this architecture. In this hierarchy, domain is not affected by changes in other layers that are less stable.
  • UI and Data are outer layers and unstable. In this setup, the many changes we do here will have less effect on other, more stable modules.
  • We chose to not have a Presentation module in this setup, to not have an overload of modules. The ViewModel lives in the feature module, in a feature/presentation package. The view classes are in a feature/ui package.
  • The app module is not drawn in this schema because it should only be used to start the app and specify the theme. Do not put code in the app module, because in the future, more app-types might be added (like tv, automotive, instant).

Use cases

  • Use cases are single-purpose: GetUserUseCase, but also: GetUserWithArticlesUseCase.
  • Use cases can call other use-cases.
  • Use cases do not have state, state preferably lives in the data layer.

DTO Models

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

Model Entities live in the data layer and are the local storage objects.

Model mapping

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.

  • Writing mapping functions between models, entities and DTO ojects is boring and cumbersome: let GitHub Copilot or some other AI tool in Android Studio generate your mapping functions.

Core modules

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:

  • extension methods on general kotlin classes
  • classes related to logging

Key architecture patterns and principles

Groovy instead of kotlin script build files

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?

Feature gradle files

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 module
  • build.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.

No non-android modules

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:

  1. Domain and data could have been kotlin modules if we used a Java inject to annotate injected constructors.
    • Pro: cleanliness, faster compile time.
    • Con: no HiltModules possible in domain and data; The app module will have to contain DataModule and DomainModule to wire interfaces to their implementations.
  2. We could have mixed dagger (non-android modules) with hilt (android modules). Con: Having two types of injection, depending on where you are, is confusing.
  3. We could have used dagger only. Con: Dagger is more complex and harder to grasp for new developers.

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.

Dependency injection

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.

Version catalog

We use a version catalog (toml) file for versioning. This is the latest feature from Gradle currently. It can be shared over (included) projects.

ViewStateString

ViewStateStrings enables you to move String logic from the View to the ViewModel, especially plurals or replacement parameter logic.

Compose Previews

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

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.

Libraries

Navigation

We use compose destinations for navigation because it's low risk, high gain:

  • It reduces the amount of custom logic and effort needed to use compose-navigation.
  • It's a layer over compose-navigation, so we can later easily switch to compose-navigation if we want to.

We added extra logic to navigate from ViewModel . To enable this for your ViewModel:

  • Add a navigator private val navigator: RouteNavigator to the constructor of your ViewModel.

  • Let your ViewModel delegate RouteNavigator: RouteNavigator by navigator

  • Call 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.

Networking

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).

JSON parsing

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.

Image loading

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.

Theming

Inspiration Material

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published