A bootstrap template for greenfield Android apps. MVI presentation over a Google-style state-production pipeline, Hilt, Retrofit + kotlinx.serialization, offline-first Room, Navigation 3 — with the quality gates and performance tooling already wired up.
One command turns it into a named project.
./bootstrap.sh --package com.acme.orders --name "Acme Orders"1. Create your repo. Click Use this template → Create a new repository at the top of this page, then clone it.
Or from the command line
gh repo create acme-orders --template jethro234/baseline-android-template --private --clone
cd acme-orders2. Bootstrap it.
./bootstrap.sh --package com.acme.orders --name "Acme Orders"This rewrites the package tree, namespaces, applicationId, project name, convention-plugin
prefix, database and DataStore file names, and the Application class — then normalises import
order and deletes itself. Your git history and remote are left alone.
| Flag | Effect |
|---|---|
--package |
Kotlin package root and applicationId. Lowercase, dot separated, 2+ segments. |
--name |
Launcher label, Gradle project name, Application class prefix. |
--no-sample |
Drop the worked example feature, leaving core infrastructure and an empty nav graph. |
--dry-run |
Print what would change and exit. |
3. Build.
./gradlew assembleDebugThe sample feature hits jsonplaceholder.typicode.com, so a fresh clone produces something
that actually runs. Replacing it is step one of real work.
./new-feature.sh checkoutGenerates :feature:checkout:api and :feature:checkout:impl — build files, NavKey, MVI
contract, ViewModel, screen, route and test skeleton — and wires both into
settings.gradle.kts and app/build.gradle.kts.
One step is left to you on purpose, because it is a decision rather than boilerplate. Add the
entry to AppNavDisplay.kt:
entry<CheckoutKey> { CheckoutRoute() }| Presentation | Sealed Intent + single onIntent() router; cold-flow pipeline with stateIn(WhileSubscribed(5s)); one-off events modelled in state, not Channels |
| DI | Hilt, with injected dispatchers |
| Network | OkHttp + Retrofit + kotlinx.serialization — no reflection, no R8 keep rules |
| Persistence | Room as single source of truth, DataStore for preferences, offline-first repository |
| Navigation | Navigation 3 — back stack as an observable list |
| Modules | :core:* plus :feature:<name>:{api,impl}; features link via :api only |
| Build | Composite build-logic, precompiled script plugins, version catalog, configuration cache |
| Testing | JUnit 5, Turbine, fakes, shared fixtures, Roborazzi screenshots, Kover |
| Performance | Baseline profiles, macrobenchmark startup + scroll, R8 full mode, Compose stability |
| CI | GitHub Actions: structural checks, ktlint, detekt, lint, tests, screenshots, coverage, release build |
app/ Composition root — Hilt, NavDisplay, theme
build-logic/convention/ Precompiled script plugins
core/
model/ common/ domain/ Pure Kotlin. No Android.
network/ database/ datastore/ data/
designsystem/ ui/ testing/
feature/items/api/ Sample feature — NavKey contract
feature/items/impl/ Sample feature — MVI ViewModel, screens, tests
macrobenchmark/ Startup + scroll benchmarks, baseline profile generation
tools/ check-consistency.py, sort-imports.py
docs/ARCHITECTURE.md Every decision, with reasoning and rejected alternatives
docs/CONVENTIONS.md The day-to-day rules
python3 tools/check-consistency.py . # structural checks, ~1s, no Gradle
./gradlew ktlintFormat # fix formatting
./gradlew ktlintCheck detekt # style and static analysis
./gradlew testDebugUnitTest # unit tests
./gradlew recordRoborazziDebug # write/refresh screenshot goldens
./gradlew verifyRoborazziDebug # fail on visual diff (what CI runs)
./gradlew lintDebug # Android lint
./gradlew koverXmlReport # coverage
./gradlew :app:generateReleaseBaselineProfile # regenerate after startup-path changes
./gradlew :macrobenchmark:connectedReleaseAndroidTest # measure startup and scroll
./gradlew assembleRelease -Pcompose.compiler.reports=true # find unskippable composables- JDK 17 (the toolchain target; AGP 9 requires 17+)
- Android Studio on a version that supports AGP 9
- Python 3 — optional, used by
tools/and by the scripts to normalise import order. Without it, run./gradlew ktlintFormatafter bootstrapping instead.
- Set
BASE_URLincore/network/build.gradle.kts. - Replace the placeholder palette in
core/designsystem/.../theme/Color.kt. The contrast test fails the build if the new colours miss WCAG AA. - Replace
app/src/main/res/drawable/ic_launcher_foreground.xml. - Add a real signing config — release currently signs with the debug key.
- Check the version catalog. Versions were current when the template was cut; bump deliberately and read the pin comments first, since several must move together (Kotlin/KSP, JUnit 5/Mannodermaus, AGP/Hilt).
docs/ARCHITECTURE.md records every decision, the alternatives
rejected, and — where this template knowingly departs from Google's official guidance (§5) —
the argument for doing so, including the strongest critique of the approach that it does not
answer.
docs/CONVENTIONS.md is the short version: the rules you actually
apply day to day.