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 moduleGraphkeepsdocs/MODULE_GRAPH.mdin sync with the module dep tree - Baseline profile pipeline:
:baselineprofilemacrobenchmark module generates a startup profile shipped with the release APK (~15–30% cold-start win)
- 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.
Do not clone this repository directly. The recommended way to use this template is to create your own repository from it.
- Click the Use this template button on the main repository page and select Create a new repository.
- Give your new project a name and description. This creates a completely new and independent repository.
- Clone your new repository to your local machine and open it in Android Studio.
- 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.
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.
After creating your new repo via Use this template, a one-shot helper sits in your Actions tab:
- Open your new repo on GitHub.
- Go to Actions → Bootstrap from template → Run workflow.
- 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.
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.
- License header company name: open
gradle.propertiesand settemplate.company(consumed by the rootbuild.gradle.ktsSpotless config). Then run./gradlew spotlessApplyto rewrite every header. - License file: open
LICENSE.mdand replace[year]and the placeholder name with your own. - 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
*.mdso it doesn't break upstream-template links. - Feature module: replace the placeholder content in
:feature-example(start withExampleScreen.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.
- Project name: in
settings.gradle.kts, changerootProject.name. - Application ID & namespaces: in
app/build.gradle.ktsand every library module'sbuild.gradle.kts, changenamespace(andapplicationIdin:app) fromcom.thecompany.consultmeto your new ID. - Package name: rename the
com.thecompany.consultmepackage via Android Studio refactor — that handles source file moves, package declarations, and imports. - Theme + application class: rename
ConsultMeTheme,Theme.ConsultMe(inapp/src/main/res/values/themes.xml), andConsultMeApplication(class + filename +AndroidManifest.xmlreference) to match your new project name. - App display name: in
app/src/main/res/values/strings.xml, changeapp_name. - Convention plugin IDs: rename the four files under
build-logic/convention/src/main/kotlin/consultme.android.*.gradle.ktsand update everyid("consultme.android.*")reference in module build scripts. - 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/assigneesblocks from.github/dependabot.yml. - Replace the
contact_linksURLs in.github/ISSUE_TEMPLATE/config.ymlwith your own (or delete them).
- Delete
- Then continue with the post-bootstrap steps above.
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.
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 exampleExampleItem.:core-common— pure-Kotlin shared utilities; ships theDispatcherqualifier +AppDispatchersenum.:core-domain— pure-Kotlin use-cases + repository ports (interfaces); depends on:core-model.:core-data— repository implementations (adapters) of:core-domainports + entity↔model mappers; Room-backed via:core-database.:core-database— Room database (entity/DAO/@Database+ Hilt module) viaconsultme.android.room.:core-testing— re-exports JUnit/Truth/Turbine/MockK/Hilt-testing/Espresso viaapi(...), plusHiltTestRunner.:baselineprofile— macrobenchmark + baseline-profile generator that ships the profile with:app's release APK. Seedocs/MODULE_GRAPH.mdfor the producer→consumer wiring.
consultme.android.application— the:appmodule.consultme.android.library— generic Android library (no Compose).consultme.android.compose— adds Compose BOM, ui/material3 deps, enablesbuildFeatures.compose.consultme.android.hilt— Hilt + KSP wiring.consultme.android.feature—library + compose + hilt + standard feature deps + :core-testing.consultme.android.room— KSP + Room runtime/ktx/compiler + schema export dir.consultme.android.test—com.android.test, for benchmark/macrobenchmark modules.consultme.android.baselineprofile—consultme.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:moduleGraphto emitdocs/MODULE_GRAPH.md.
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.
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.
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.
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(...).
Each module ships its own lint-baseline.xml. Regenerate after adding code that introduces new lint warnings (rather than hand-editing):
./gradlew :feature-example:updateLintBaselineReplace :feature-example with the module you're updating. CI runs lintRelease and fails on any non-baselined violation.
The template already draws edge-to-edge — MainActivity 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 theWindowSizeClassto branch between compact/medium/expanded layouts.androidx.compose.material3.adaptive:adaptive-layoutand: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.
- Spotless + ktlint: Consistent formatting and license-header enforcement on every
.kt/.gradle.ktsfile. - Android Lint: Kotlin and Compose correctness checks; CI runs
lintReleaseand fails on any non-baselined violation. - Kover: Aggregated test coverage.
./gradlew koverHtmlReportproduces a project-wide report underbuild/reports/kover/html/. Generated Hilt/Room/Compose code is excluded from instrumentation inconsultme.kover.gradle.kts. - Module graph:
docs/MODULE_GRAPH.mdis regenerated by./gradlew moduleGraph. The renderer is pluggable (Strategy pattern viaModuleGraphRenderer); ships with a Mermaid implementation. CI fails if the committed graph is stale. - Baseline profile:
:baselineprofileis a producer macrobenchmark module that emitsapp/src/main/baseline-prof.txt. The committed profile is consumed byandroidx.profileinstallerat install time and gives a measurable cold-start win. Regenerate via./gradlew :app:generateReleaseBaselineProfile(uses the existingpixel6api30GMD).
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)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.
mainis protected — every change goes through a PR;build_and_testis a required check. PR conventions (Conventional Commits, one scope per PR) live inCONTRIBUTING.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 commitCI 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
pixel6api30Gradle 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.ymlreflect what's currently deferred; the migration playbook lives indocs/IMPROVEMENT_PLAN.md. For AGP majors specifically, install Google'sagp-9-upgradeClaude 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; seeCLAUDE.mdfor the full policy.
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.
| 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. |
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.
This project is licensed under the MIT License - see the LICENSE.md file for details.