Skip to content

en-taha-saad/android-guidelines

Repository files navigation

Android App Development Guidelines

1. Project Overview

These guidelines aim to standardize the architecture, structure, and development practices for Android apps in our organization. The sample app, CleanTasky, demonstrates all practices mentioned here.


2. Architecture Guidelines

  • Use Clean Architecture with three main layers: presentation, domain, and data.
  • Enforce SOLID principles across all layers.
  • Use OOP principles such as abstraction, encapsulation, and polymorphism.
  • Apply Design Patterns like Repository, UseCase, Factory, and Observer where appropriate.

3. UI Guidelines

  • Use Jetpack Compose for all UI development.
  • Follow State Hoisting and Unidirectional Data Flow (UDF).
  • Implement Theming with Material3: support light/dark modes and dynamic colors.

4. Data Layer

  • Use Room for local database storage.
  • Use DataStore (Preferences and Proto) for key-value and structured settings.
  • Abstract data sources using the Repository pattern.

5. Concurrency

  • Use Kotlin Coroutines with proper Dispatchers.
  • Use Flow for data streams and reactive patterns.
  • Handle side effects and thread context switching cleanly.

6. Gradle Structure

  • Keep the app single-module for simplicity and better maintainability.
  • Use Version Catalogs for dependency management.
  • Configure Product Flavors: mock, staging, prod.
  • Set up Build Environments: dev, uat, prod.
  • Support Gradle Build Config Fields and environment constants per flavor.
  • Use BuildConfig constants to manage API URLs, keys, and other env-specific settings.
  • Apply custom ProGuard rules per flavor if needed.
  • Structure your build.gradle.kts to keep each flavor maintainable.

Suggested Flavors Setup

flavorDimensions += "env"

productFlavors {
    create("qa") {
        dimension = "env"
        applicationIdSuffix = ".qa"
        versionNameSuffix = "-qa"
        buildConfigField("String", "BASE_URL", '"https://staging-api.cleantasky.com"')
        firebaseAppDistribution {
            serviceCredentialsFile = "<path-to-service-account-qa.json>"
            artifactType = "APK"
            releaseNotesFile = "<path-to-release-notes-qa.txt>"
            testersFile = "<path-to-testers.txt>"
        }
    }
    create("prod") {
        dimension = "env"
        buildConfigField("String", "BASE_URL", '"https://api.cleantasky.com"')
        firebaseAppDistribution {
            serviceCredentialsFile = "<path-to-service-account-prod.json>"
            artifactType = "APK"
            releaseNotesFile = "<path-to-release-notes-prod.txt>"
            testersFile = "<path-to-testers.txt>"
        }
    }
}

✅ Place secrets like tokens in local.properties, and access them securely using gradle.properties or environment variables.

✅ This structure gives clear separation for dev, test, and release cycles while using a single module.

  • Keep the app single-module for simplicity and better maintainability.
  • Use Version Catalogs for dependency management.
  • Configure Product Flavors: mock, staging, prod.
  • Set up Build Environments: dev, uat, prod.

Why Firebase App Distribution?

Firebase App Distribution provides an efficient and secure way to distribute pre-release versions of your app to QA testers and internal stakeholders. It allows:

  • Early access to new builds via over-the-air installation
  • Automatic notifications for testers
  • Integration with CI/CD pipelines (e.g., Fastlane, GitHub Actions)
  • Release management with version tracking and notes
  • Authentication and access control for testers

Firebase Integration in CleanTasky

In your project:

1. Apply Plugin in Root build.gradle.kts:

plugins {
    id("com.google.firebase.appdistribution") version "5.1.1" apply false
}

2. Apply Plugin in App build.gradle.kts:

plugins {
    id("com.google.firebase.appdistribution")
}

3. Configure firebaseAppDistribution in flavor (example - prod)

firebaseAppDistribution {
    serviceCredentialsFile = "<path-to-service-account-prod.json>"
    artifactType = "APK"
    releaseNotesFile = "<path-to-release-notes-prod.txt>"
    testersFile = "<path-to-testers.txt>"
}

4. Firebase SDK and API usage in code (e.g. MainActivity)

  • FirebaseAppDistribution.getInstance() to access distribution service
  • signInTester() to authenticate the tester
  • checkForNewRelease() to determine if a new version is available
  • updateApp() to trigger the update process

5. Show update prompt and download progress in Compose

if (showUpdateDialog.value) {
    UpdateAvailableDialog(...)
}

if (isUpdating.value) {
    UpdateLoadingDialog(progress = updateProgress.value)
}

✅ Recommended for teams aiming to streamline internal testing workflows and reduce manual release management effort.


Gradle Setup Checklist

  • ✅ Use Kotlin DSL (build.gradle.kts) everywhere

  • ✅ Set up libs.versions.toml for dependency versions

  • ✅ Add and sync Compose, Material3, Lifecycle, Hilt, Coroutines, Room, DataStore, Navigation, Serialization

  • ✅ Use the latest non-conflicting versions across AndroidX, Kotlin, and third-party libraries

  • ✅ Enable namespace and Jetpack Compose support in the build.gradle.kts

  • ✅ Apply and configure Hilt plugin without using alias for kapt

  • ✅ Ensure correct plugin setup in libs.versions.toml (exclude kapt alias)

  • ✅ Include kapt manually via id("org.jetbrains.kotlin.kapt") in the app-level build file

  • ✅ Add Product Flavors like mock, staging, prod (for different app variants)

  • ✅ Add Environment Configs using buildConfigField or resValue per flavor

  • ✅ Add Versioning Strategy with auto increment/versioning logic (optional via CI/CD)

  • ✅ Setup Custom Build Types if needed: debug, release, benchmark, etc.

  • ✅ Use Kotlin DSL (build.gradle.kts) everywhere

  • ✅ Set up libs.versions.toml for dependency versions

  • ✅ Add and sync Compose, Material3, Lifecycle, Hilt, Coroutines, Room, DataStore dependencies

  • ✅ Use the latest non-conflicting versions across AndroidX and Kotlin

  • ✅ Enable namespace and Jetpack Compose support in the build.gradle.kts

  • ✅ Apply and configure Hilt plugin without using alias for kapt

  • ✅ Ensure correct plugin setup in libs.versions.toml (exclude kapt alias)

  • ✅ Include kapt manually via id("org.jetbrains.kotlin.kapt") in the app-level build file

  • ✅ Store version components (MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION, BUILD_NUMBER) in gradle.properties.

  • ✅ Calculate versionCode using: (major * 10000) + (minor * 100) + (patch * 10) + build)

  • ✅ Generate versionName as a string: "major.minor.patch" or "major.minor.patch-build" (if including build number).

  • ✅ Ensure each release increases versionCode to maintain Play Store compatibility.

  • ✅ Keep versioning logic centralized and overrideable (e.g., via CI/CD using -PBUILD_NUMBER=99).

The following dependencies are used to support Clean Architecture, Jetpack Compose, Hilt, Room, DataStore, and other essential libraries:

Must-have Libraries

  • Jetpack Compose (UI, tooling, material3)
  • Lifecycle & ViewModel
  • Kotlin Coroutines & Flow
  • Room (Database)
  • DataStore (Proto/Preferences)
  • Dagger Hilt (DI)
  • Retrofit & OkHttp (Networking, optional for sample app)
  • Timber (Logging)
  • Testing: JUnit, Compose UI testing, Coroutine test support

Suggested Gradle Libraries Block

Make sure the following are defined in libs.versions.toml and referenced in build.gradle.kts:

[versions]
hilt = "2.51.1"
room = "2.6.1"
datastore = "1.1.1"
coroutines = "1.8.0"
retrofit = "2.11.0"
okhttp = "4.12.0"
timber = "5.0.1"

[libraries]
# Hilt
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }

# Room
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }

# DataStore
datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
datastore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastore" }

# Coroutines
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }

# Network (optional)
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }

# Timber
timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }

Ensure you apply the kotlin-kapt and dagger.hilt.android.plugin plugins for Hilt to work correctly.

[plugins]
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }

Then reference them in your build.gradle.kts:

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
    alias(libs.plugins.kotlin.kapt)
    alias(libs.plugins.dagger.hilt)
}

✅ This setup supports Clean Architecture, dependency injection, offline storage, state management, and modular expansion in the future.

  • ✅ Use Kotlin DSL (build.gradle.kts) everywhere
  • ✅ Set up libs.versions.toml for dependency versions
  • ✅ Add and sync Compose, Material3, Lifecycle, Hilt, Coroutines, and Room dependencies
  • ✅ Use the latest non-conflicting versions across AndroidX and Kotlin
  • ✅ Enable namespace and Jetpack Compose support in the build.gradle.kts

Tip: Use platform(libs.androidx.compose.bom) to manage Compose versions consistently.

Sample Root Project Structure:

CleanTasky/
│
├── build.gradle.kts (Root build config)
├── settings.gradle.kts
├── gradle.properties
├── gradle/wrapper/gradle-wrapper.properties
├── gradle/libs.versions.toml
├── .github/ (CI workflows)
├── .gitignore
│
├── app/ (Main App module containing all features, data, domain, and UI layers)
│   ├── build.gradle.kts
│   ├── proguard-rules.pro

7. Git & GitHub Workflow

  • Follow Git Flow methodology to manage branching and releases.
  • Use Conventional Commits for commit messages.
  • Enforce PR templates and review checklists.
  • Protect main, develop, and all release/* branches.

Git Flow Branching Strategy (with QA and Production Separation)

To streamline QA and production processes, we adopt a customized Git Flow:

  • main: production-ready code only (deployed to production).
  • develop: ongoing development, all new features and bugfixes go here.
  • feature/feature-name: used for new features (merged into develop).
  • fix/bug-description: used for bug fixes (merged into develop).
  • qa-release/vX.X.X: frozen branch prepared for internal QA and testing.
  • prod-release/vX.X.X: used to finalize the QA-passed build and deploy to production.
  • hotfix/vX.X.X: urgent fixes for production (branched from main, merged into main and develop).

Branch Usage Rationale

hotfix vs bugfix

  • hotfix/*: Used only for urgent production fixes. Branched from main, and after merging, it must be merged into both main and develop.
  • fix/*: Used for non-urgent bug fixes during development. Branched from develop and merged back into develop.

✅ Use hotfix/* when something must be immediately fixed in production. ✅ Use fix/* when fixing normal bugs during development.


  • Developers work in feature/* branches and merge to develop via PRs.
  • QA engineers test code from qa-release/*, which is merged from develop when ready for testing.
  • Once QA passes, the qa-release/* branch is used to create prod-release/* for final production deployment.

This separation allows parallel work: QA can test a stable release while developers continue building new features.


# Initial setup
> git init
> git checkout -b develop

# Create a new feature
> git checkout -b feature/your-feature-name

# Create a new release branch
> git checkout -b release/v1.0.0

# Create a hotfix
> git checkout -b hotfix/v1.0.1

Example Commit Message (Conventional Commit)

feat: add login screen with basic form validation
fix: correct task sorting issue on main screen
chore: setup base project structure and .gitignore

Suggested .gitignore

# Built application files
*.apk
*.aar
*.ap_
*.aab

# Files for the ART/Dalvik VM
*.dex

# Java class files
*.class

# Generated files
bin/
gen/
out/

# App Release Files
app/release/*
release/

# Gradle files
.gradle/
build/

# Local configuration file (sdk path, etc)
local.properties

# Proguard folder generated by Eclipse
proguard/

# Log Files
*.log

# Android Studio Navigation editor temp files
.navigation/

# Android Studio captures folder
captures/

# IntelliJ
*.iml
.idea/
.idea/libraries
.idea/caches
.idea/modules.xml
.idea/navEditor.xml
.idea/codeStyles/

# Keystore files
*.jks
*.keystore

# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/

# Google Services (e.g. APIs or Firebase)
google-services.json

# Freeline
freeline.py
freeline/
freeline_project_description.json

# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md

# Version control
vcs.xml

# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/

# MacOS
.DS_Store

# App Specific cases 
app/release/output.json

8. CI/CD & Deployment

  • Use GitHub Actions + Fastlane for automation.
  • Configure workflows for pull requests, build, test, release.
  • Automate versionCode and versionName.

9. Testing

  • Write Unit Tests for business logic in the domain layer using JUnit and MockK.
  • Test Kotlin Flows and suspend functions using Turbine.
  • Write UI tests with Jetpack Compose testing APIs.
  • Add Instrumentation Tests to validate full app integrations and interactions.

Testing Setup (CleanTasky)

✅ Benefits of Testing

  • Unit tests ensure your logic works independently and correctly.
  • UI tests verify the actual user interface behaves as expected on real devices/emulators.
  • Instrumentation tests simulate full app experiences, including navigation and database access.
  • Flow testing ensures your reactive streams emit correctly over time.

✅ Applied Test Types

1. Unit Testing (test/)

  • Frameworks: JUnit, MockK
  • Example (in ExampleUnitTest.kt):
@Test
fun addition_isCorrect() {
    assertEquals(4, 2 + 2)
}

2. Flow Testing

  • Framework: Turbine
  • Example:
@Test
fun flowEmitsCorrectly() = runTest {
    val flow = flowOf("Hello", "World")

    flow.test {
        assert(awaitItem() == "Hello")
        assert(awaitItem() == "World")
        awaitComplete()
    }
}

3. UI Testing (androidTest/)

  • Framework: Compose UI Test
  • Example (in GreetingUiTest.kt):
@get:Rule
val composeTestRule = createComposeRule()

@Test
fun greetingIsDisplayed() {
    composeTestRule.setContent {
        Greeting("Android")
    }
    composeTestRule.onNodeWithText("Hello Android!").assertIsDisplayed()
}

4. Instrumentation Tests

  • Used for testing integration with Android components.
  • Example: Accessing context, navigation, Room DB, etc.

Dependencies Used

    // Unit Testing
    testImplementation(libs.junit)
    testImplementation(libs.mockk)
    testImplementation(libs.turbine)
    testImplementation(libs.kotlinx.coroutines.test)

    // Instrumented / UI Testing
    androidTestImplementation(platform(libs.androidx.compose.bom))
    androidTestImplementation(libs.androidx.ui.test.junit4)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
    debugImplementation(libs.androidx.ui.test.manifest)
    debugImplementation(libs.androidx.ui.tooling)

10. Code Quality

  • Use Ktlint for static analysis.
  • Format code using built-in or custom rules.
  • Maintain consistent naming, styling, and documentation.
  • Define Ktlint configs in the config/ folder.

Ktlint Implementation (Applied in CleanTasky)

Below is an example of how Ktlint is integrated into the project:

Root build.gradle.kts:

plugins {
    // ... other plugins
    alias(libs.plugins.ktlint) apply false // Ktlint plugin not applied at root
}

app/build.gradle.kts:

plugins {
    alias(libs.plugins.ktlint) // Ktlint plugin is actually applied here
}

ktlint {
    android.set(true)
    outputToConsole.set(true)
    coloredOutput.set(true)
    verbose.set(true)
    ignoreFailures.set(false)
}

What This Does:

  • Installs Ktlint tasks (e.g., ktlintCheck, ktlintFormat).
  • Enforces style checks on Kotlin files.
  • Fails the build if any style violations exist (ignoreFailures=false).
  • Provides colorized console output for easier reading.

To run checks:

./gradlew ktlintCheck
./gradlew ktlintFormat

This ensures your code adheres to a consistent style and best practices.


11. Logging

  • Use Timber for logging. Initialize Timber.DebugTree() in debug builds only.

Sample App: CleanTasky

The sample app, CleanTasky, will implement all the concepts outlined in this document:

  • Clean Architecture in a single-module setup
  • Jetpack Compose UI with Material3
  • ViewModel-based state management
  • Offline-first data layer using Room and DataStore
  • Git Flow-based branching and CI/CD via GitHub Actions
  • Fully documented and tested codebase

The app structure and implementation should evolve alongside this guideline to serve as a living reference and onboarding resource.


About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages