Skip to content

Latest commit

 

History

190 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ConsultMe

Android CI Latest release License: MIT Platform Min API PRs Welcome

GitHub stars GitHub forks

ConsultMe is a template project for Jetpack Compose applications, featuring integrated tools for code quality and automation. It includes:

  • Spotless + ktlint: Automated code formatting and license-header enforcement
  • Android Lint: Kotlin and Compose correctness checks (release variant, fail-on-error)
  • Kover: Aggregated test coverage with HTML + XML reports
  • Module-graph generation: ./gradlew moduleGraph keeps docs/MODULE_GRAPH.md in sync with the module dep tree
  • Baseline profile pipeline: :baselineprofile macrobenchmark module generates a startup profile shipped with the release APK (~15–30% cold-start win)

Features

  • Fully configured for Jetpack Compose and a multi-module architecture.
  • Worked end-to-end example: a layered slice (Room → repository → use-case → ViewModel) and a Navigation 3 list→detail flow, so the layers are demonstrated, not just scaffolded.
  • Code quality tools included and pre-configured.
  • 100% Kotlin codebase, using Coroutines and Flow.
  • Dependency injection with Hilt.

Getting Started

Do not clone this repository directly. The recommended way to use this template is to create your own repository from it.

  1. Click the Use this template button on the main repository page and select Create a new repository.
  2. Give your new project a name and description. This creates a completely new and independent repository.
  3. Clone your new repository to your local machine and open it in Android Studio.
  4. Follow the instructions in the "How to Rename and Refactor" section below to customize it for your project.

Note on Forking: If your intention is to contribute changes back to this template, you should fork the repository instead.

How to Rename and Refactor

There are two equivalent paths — pick whichever fits your workflow. Both run the same scripts/rename-template.py under the hood, so they produce the same result.

Option A: One click in the GitHub UI

After creating your new repo via Use this template, a one-shot helper sits in your Actions tab:

  1. Open your new repo on GitHub.
  2. Go to Actions → Bootstrap from template → Run workflow.
  3. Fill in package (e.g. com.acme.myapp) and app name (e.g. My App) and click Run workflow.

The workflow runs the rename script with your inputs, commits the result directly to your default branch, and self-deletes itself in the same commit so it doesn't keep haunting your Actions tab. If your repo has branch protection on the default branch, the push will be rejected — use Option B instead, or temporarily relax protection.

Option B: Locally with Python

If you'd rather rename offline, run the same script on your machine:

python3 scripts/rename-template.py com.acme.myapp "My App Name"

The first argument is the new package (also used as applicationId). The second is the user-facing app name; its PascalCase form (MyAppName) becomes rootProject.name, the theme name, and the Application class name. The four convention plugin IDs under build-logic/ are also rewritten (consultme.android.*myappname.android.*). Re-running with the same arguments is a no-op.

The script also scrubs three template-maintainer-personal files so they don't carry the upstream owner's identity into your repo: .github/FUNDING.yml is deleted, the reviewers/assignees blocks in .github/dependabot.yml are stripped, and .github/ISSUE_TEMPLATE/config.yml is rewritten to a commented contact_links stub. Re-add your own once your fork has a public URL.

Post-bootstrap steps (both options)

  1. License header company name: open gradle.properties and set template.company (consumed by the root build.gradle.kts Spotless config). Then run ./gradlew spotlessApply to rewrite every header.
  2. License file: open LICENSE.md and replace [year] and the placeholder name with your own.
  3. README and docs: update the badges (CI, stars, forks) to point at your repo, and replace the project description in this file. The script intentionally skips *.md so it doesn't break upstream-template links.
  4. Feature module: replace the placeholder content in :feature-example (start with ExampleScreen.kt), and rename the module (:feature-example:feature-yourname) once you know what you're building.

If you'd rather rename by hand, expand the manual fallback below.

Manual rename fallback

Use Android Studio's Refactor > Rename for the package step.

  1. Project name: in settings.gradle.kts, change rootProject.name.
  2. Application ID & namespaces: in app/build.gradle.kts and every library module's build.gradle.kts, change namespace (and applicationId in :app) from com.thecompany.consultme to your new ID.
  3. Package name: rename the com.thecompany.consultme package via Android Studio refactor — that handles source file moves, package declarations, and imports.
  4. Theme + application class: rename ConsultMeTheme, Theme.ConsultMe (in app/src/main/res/values/themes.xml), and ConsultMeApplication (class + filename + AndroidManifest.xml reference) to match your new project name.
  5. App display name: in app/src/main/res/values/strings.xml, change app_name.
  6. Convention plugin IDs: rename the four files under build-logic/convention/src/main/kotlin/consultme.android.*.gradle.kts and update every id("consultme.android.*") reference in module build scripts.
  7. Maintainer-personal files (Options A and B do this for you; manual renamers need to do it explicitly):
    • Delete .github/FUNDING.yml.
    • Strip the reviewers/assignees blocks from .github/dependabot.yml.
    • Replace the contact_links URLs in .github/ISSUE_TEMPLATE/config.yml with your own (or delete them).
  8. Then continue with the post-bootstrap steps above.

How to add a new feature module

The consultme.android.feature convention plugin makes a new feature module a one-liner. Create feature-<name>/build.gradle.kts:

plugins {
    id("consultme.android.feature")
}

android {
    namespace = "com.thecompany.consultme.feature.<name>"
}

Add include(":feature-<name>") to settings.gradle.kts and depend on it from :app via implementation(projects.feature<NameInPascalCase>). The feature convention composes library + compose + hilt and pulls in the standard feature deps: lifecycle-runtime-compose, lifecycle-viewmodel-compose, hilt-navigation-compose, :core-designsystem, :core-ui, and :core-testing for unit + instrumented tests.

Module layout

The template ships these modules (NIA-aligned):

  • :app — application module, Compose root + Navigation 3 host (NavDisplay).
  • :feature-example — placeholder feature module demonstrating a screen end-to-end (list + detail, ViewModel, Nav3 routes); replace with your own and rename.
  • :core-designsystem — Compose theme (ConsultMeTheme), color/typography tokens.
  • :core-ui — shared Compose composables (loading/empty/error states). Scaffold.
  • :core-model — pure-Kotlin data classes (no Android); ships the example ExampleItem.
  • :core-common — pure-Kotlin shared utilities; ships the Dispatcher qualifier + AppDispatchers enum.
  • :core-domain — pure-Kotlin use-cases + repository ports (interfaces); depends on :core-model.
  • :core-data — repository implementations (adapters) of :core-domain ports + entity↔model mappers; Room-backed via :core-database.
  • :core-database — Room database (entity/DAO/@Database + Hilt module) via consultme.android.room.
  • :core-testing — re-exports JUnit/Truth/Turbine/MockK/Hilt-testing/Espresso via api(...), plus HiltTestRunner.
  • :baselineprofile — macrobenchmark + baseline-profile generator that ships the profile with :app's release APK. See docs/MODULE_GRAPH.md for the producer→consumer wiring.

Other available convention plugins

  • consultme.android.application — the :app module.
  • consultme.android.library — generic Android library (no Compose).
  • consultme.android.compose — adds Compose BOM, ui/material3 deps, enables buildFeatures.compose.
  • consultme.android.hilt — Hilt + KSP wiring.
  • consultme.android.featurelibrary + compose + hilt + standard feature deps + :core-testing.
  • consultme.android.room — KSP + Room runtime/ktx/compiler + schema export dir.
  • consultme.android.testcom.android.test, for benchmark/macrobenchmark modules.
  • consultme.android.baselineprofileconsultme.android.test + androidx.baselineprofile + macro/uiautomator deps; used by :baselineprofile.
  • consultme.android.lint — pure-Kotlin module that contributes custom Lint checks.
  • consultme.jvm.library — pure-Kotlin module (no AGP), e.g. for :core-model / :core-domain.
  • consultme.kover — coverage instrumentation (auto-applied by every Android/JVM convention; opt out by removing the line).
  • consultme.modulegraph — root-only; registers :moduleGraph to emit docs/MODULE_GRAPH.md.

Using vector icons

material3 ships a small set of icons (Icons.Default.MoreVert, Icons.Filled.Add, etc.). For the full Material catalog (Icons.Default.Restaurant, Icons.Filled.Star, …), add to your feature module:

dependencies {
    implementation("androidx.compose.material:material-icons-extended")
}

The Compose BOM (already on the classpath via consultme.android.compose) supplies the version. Not bundled by default because material-icons-extended is ~1.5 MB — adopters who only need a handful of icons can import individual icon files via material-icons-core instead.

Adding product flavors

Many adopters with a Play Store presence run multiple flavors (e.g. lite / pro for a free/paid split). The template ships a single-variant :app with defaultConfig.applicationId = "com.thecompany.consultme". When you add product flavors, each flavor overrides the default applicationId — the defaultConfig value never ships if any flavor declares its own applicationId. Adopters new to AGP have miswired Firebase / lost Play Store identity continuity by treating the default as authoritative.

Typical wiring in app/build.gradle.kts:

android {
    flavorDimensions += "tier"
    productFlavors {
        create("lite") {
            dimension = "tier"
            applicationId = "com.thecompany.consultme.lite"
            versionNameSuffix = "-lite"
        }
        create("pro") {
            dimension = "tier"
            applicationId = "com.thecompany.consultme.pro"
            versionNameSuffix = "-pro"
        }
    }
}

Each flavor's applicationId is what the Play Store sees and what Firebase / FCM match client SDKs against. Pick deliberately on day one — changing it later means re-publishing as a new app. See AGP's product flavors documentation for the full DSL.

If you wire flavors after generating a baseline profile, also update the TARGET_PACKAGE reference in :baselineprofile — the macrobenchmark targets a specific applicationId, and the default-flavor value won't match.

Logging

The template ships no logging library — picking one is a cross-cutting decision (alongside networking, analytics, image-loading) that downstream forks tend to make on their own terms. This section is a pointer, not a default.

The conventional baseline for an app of this shape is Timber:

// app/build.gradle.kts (you add this; the template does not)
dependencies {
    implementation("com.jakewharton.timber:timber:5.0.1")
}

// ConsultMeApplication.kt
override fun onCreate() {
    super.onCreate()
    if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
    // else Timber.plant(CrashlyticsTree())  // see below
}

For release builds, the standard pattern is a custom Timber.Tree that forwards to your crash reporter — most commonly CrashlyticsTree forwarding to FirebaseCrashlytics.recordException(...). Keep the DebugTree debug-only so noisy logs don't ship to production.

If you'd rather use platform android.util.Log directly, SLF4J + Logback, or roll a thin Logger interface that adopts a different vendor later, that's fully supported — the template doesn't wire anything that would conflict.

How to write a Hilt-aware test

Every module declares :core-testing for both unit and instrumented tests, so JUnit/Turbine/MockK/Hilt-testing/Espresso are already on the classpath:

testImplementation(projects.coreTesting)
androidTestImplementation(projects.coreTesting)

For an instrumented test that needs Hilt injection, annotate with @HiltAndroidTest and use the runner that the convention plugins already wire in (com.thecompany.consultme.core.testing.HiltTestRunner):

@HiltAndroidTest
class MyFeatureTest {
    @get:Rule val hilt = HiltAndroidRule(this)

    @Before fun setUp() { hilt.inject() }

    @Test fun feature_does_something() { /* ... */ }
}

No need to redeclare JUnit/Hilt-testing dependencies in the module's build.gradle.kts:core-testing re-exports them with api(...).

How to regenerate lint baselines

Each module ships its own lint-baseline.xml. Regenerate after adding code that introduces new lint warnings (rather than hand-editing):

./gradlew :feature-example:updateLintBaseline

Replace :feature-example with the module you're updating. CI runs lintRelease and fails on any non-baselined violation.

How to support adaptive layouts (large screens & foldables)

The template already draws edge-to-edgeMainActivity calls enableEdgeToEdge() and the root Scaffold consumes window insets — so content lays out correctly behind the system bars on every device.

What the template intentionally does not ship is large-screen layout adaptation, because :feature-example is a throwaway placeholder. When you build your real feature, add the adaptive dependencies to gradle/libs.versions.toml (they're left out to keep the template's dependency graph lean):

  • androidx.compose.material3:material3-window-size-class — read the WindowSizeClass to branch between compact/medium/expanded layouts.
  • androidx.compose.material3.adaptive:adaptive-layout and :adaptive-navigation — list-detail and supporting-pane scaffolds that fold/unfold with available width.

Where things live: put shared adaptive containers in :core-ui (it's Hilt-free and already the home for reusable UI scaffolds); keep pane/selection logic in the feature module. Full adaptive navigation (NavigationSuiteScaffold, SceneStrategy) builds on the Navigation 3 graph the template already ships in :app — see the navigation/navigation-3 skill for extending it.

The official jetpack-compose/adaptive Claude Code skill is a step-by-step playbook for the above.

Code Quality

  • Spotless + ktlint: Consistent formatting and license-header enforcement on every .kt / .gradle.kts file.
  • Android Lint: Kotlin and Compose correctness checks; CI runs lintRelease and fails on any non-baselined violation.
  • Kover: Aggregated test coverage. ./gradlew koverHtmlReport produces a project-wide report under build/reports/kover/html/. Generated Hilt/Room/Compose code is excluded from instrumentation in consultme.kover.gradle.kts.
  • Module graph: docs/MODULE_GRAPH.md is regenerated by ./gradlew moduleGraph. The renderer is pluggable (Strategy pattern via ModuleGraphRenderer); ships with a Mermaid implementation. CI fails if the committed graph is stale.
  • Baseline profile: :baselineprofile is a producer macrobenchmark module that emits app/src/main/baseline-prof.txt. The committed profile is consumed by androidx.profileinstaller at install time and gives a measurable cold-start win. Regenerate via ./gradlew :app:generateReleaseBaselineProfile (uses the existing pixel6api30 GMD).

Common commands

The same gates CI runs. Run locally before opening a PR:

./gradlew spotlessApply spotlessCheck   # license header + ktlint
./gradlew test                          # unit tests
./gradlew lintRelease                   # Android Lint, release variant
./gradlew :app:assembleRelease          # exercises R8 + resource shrinking
./gradlew koverHtmlReport               # aggregated coverage at build/reports/kover/html/
./gradlew moduleGraph                   # regenerate docs/MODULE_GRAPH.md (CI fails if stale)
./gradlew connectedAndroidTest          # instrumented tests (needs device/emulator)
./gradlew :app:generateReleaseBaselineProfile  # regenerate baseline profile (uses GMD)

Targeted variants:

./gradlew :feature-example:testDebugUnitTest --tests "*ExampleViewModelTest"
./gradlew :feature-example:updateLintBaseline       # regen one module's lint baseline
./gradlew pixel6api30DebugAndroidTest               # GMD instrumented tests (no physical device)

Maintaining your fork

After bootstrap (rename script + customizing :feature-example), the ongoing maintenance shape is:

  • Dependabot runs weekly. Grouped PRs bump AndroidX, Kotlin/coroutines, Compose, Gradle plugins, and testing libs (groups defined in .github/dependabot.yml). Skim the upstream changelog, then squash-merge.

  • Branch protection. main is protected — every change goes through a PR; build_and_test is a required check. PR conventions (Conventional Commits, one scope per PR) live in CONTRIBUTING.md.

  • Module graph stays in sync with code. Whenever you add or remove an inter-module dependency, regenerate and commit:

    ./gradlew moduleGraph
    git add docs/MODULE_GRAPH.md && git commit

    CI fails if the committed graph drifts from what's in the build files.

  • Lint baselines. When new Lint warnings appear (usually from new code), regenerate the affected module's baseline rather than hand-editing the XML:

    ./gradlew :<module>:updateLintBaseline
  • Baseline profile (cold-start AOT). Regenerate periodically or after notable UI changes. Uses the bundled pixel6api30 Gradle Managed Device:

    ./gradlew :app:generateReleaseBaselineProfile
    git add app/src/main/baseline-prof.txt && git commit
  • Major migrations (AGP, Kotlin, Hilt) ship as dedicated PRs, never passive Dependabot bumps. The pinned versions in .github/dependabot.yml reflect what's currently deferred; the migration playbook lives in docs/IMPROVEMENT_PLAN.md. For AGP majors specifically, install Google's agp-9-upgrade Claude Code skill — it's the canonical playbook.

  • Release tags. Cut a tag at each phase boundary, not arbitrarily. Pre-release suffixes (vX.0.0-rc.N) for major-migration deferreds so adopters can preview before promotion. Each tag ships as a GitHub Release with auto-generated notes; see CLAUDE.md for the full policy.

Versioning

Tags follow SemVer with a template-adopter lens: MAJOR = breaking change for downstream forks (minSdk bump, AGP/Kotlin major migration, convention-plugin API rename), MINOR = a phase landing or new opt-in tooling, PATCH = bug fixes and dep bumps. Tags align with phase boundaries in docs/IMPROVEMENT_PLAN.md, and every tag ships as a GitHub Release. See CLAUDE.md for the full policy.

Project documentation

File What's in it
ARCHITECTURE.md Layered module diagram, UDF data-flow sequence, module responsibilities table, navigation/dispatcher conventions. Read this first if you're orienting in the codebase.
CLAUDE.md Orientation for AI coding assistants and humans — common commands, module graph, conventions, CI / branch protection, versioning policy.
CONTRIBUTING.md Local setup, the local CI loop, PR conventions, license header, where things live, bug/security reporting paths.
docs/IMPROVEMENT_PLAN.md Living roadmap. Every phase has state, scope, rationale, and concrete deltas. Read before non-trivial work.
docs/MODULE_GRAPH.md Auto-generated Mermaid graph of inter-module dependencies.
SECURITY.md Security disclosure policy.
CODE_OF_CONDUCT.md Contributor Covenant v2.1.

Template origin

Scaffolded from Tarek-Bohdima/ConsultMe — a Compose multi-module Android template. The upstream tracks plumbing improvements (convention plugins, build infrastructure, dependency migrations, baseline-profile and module-graph tooling) independent of any one fork's product code. If you scaffolded from it, the upstream Releases page and docs/IMPROVEMENT_PLAN.md are where new tooling and migration playbooks land. The bootstrap script (scripts/rename-template.py) skips .md files, so this notice survives renames.

License

This project is licensed under the MIT License - see the LICENSE.md file for details.

Releases

Sponsor this project

Used by

Contributors

Languages