Skip to content

Repository files navigation

Baseline Android Template

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"

Quick start

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-orders

2. 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 assembleDebug

The sample feature hits jsonplaceholder.typicode.com, so a fresh clone produces something that actually runs. Replacing it is step one of real work.


Adding a feature

./new-feature.sh checkout

Generates :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() }

What you get

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

Layout

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

Commands

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

Requirements

  • 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 ktlintFormat after bootstrapping instead.

Before your first real feature

  1. Set BASE_URL in core/network/build.gradle.kts.
  2. Replace the placeholder palette in core/designsystem/.../theme/Color.kt. The contrast test fails the build if the new colours miss WCAG AA.
  3. Replace app/src/main/res/drawable/ic_launcher_foreground.xml.
  4. Add a real signing config — release currently signs with the debug key.
  5. 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).

Why it is built this way

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages