* Milestone and ownership notes that no longer hold ("owned by M5/M6/M8", "M8 replaces it", "(M8b)", "M7x add",
"proposed decision in the M5 report", which is D-027 and D-028) in build scripts, :core:design, :core:fakes, :data,
:app, :core:model and the settings string files.
* SyncEngine: the clock_skew KDoc sat orphaned above restampAfterClockSkew's; it now documents renewMutationId.
* libs.versions.toml header: no milestone, the drift date.
* README: the module table as built (no owners, no DebugFakesModule, which M8 removed), the test fixtures in the
architecture check and the shared screenshot harness.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
5.5 KiB
Kvitt Android project
One Gradle project, modules as in SPEC §6.2 and §14.7. Build only through tools/gradle-serial.sh with
PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH (CLAUDE.md › Build and verify). The gate is tools/gradle-serial.sh check:
compile, unit tests (including every vector of contracts/test-vectors/), lint (MissingTranslation and
ExtraTranslation are errors), the architecture check and the screenshot tests.
Modules
| Module | Plugin (build-logic) | Notes |
|---|---|---|
:core:model |
kvitt.jvm.library |
UI contract (§14.2.4), domain types model.money (§14.5.1), analytics types model.analytics (§14.5.4) |
:domain |
kvitt.jvm.library |
§14.5 functions; VectorTest runs every vector file, copied by :domain:copyTestVectors |
:core:design |
kvitt.android.library + kvitt.android.compose + kvitt.android.screenshots |
KvittTheme, tokens, shared components, Canvas charts, formatters, shared failure texts and limits, shared strings (SPEC §6.2); its test fixtures are the shared test support (below) |
:core:fakes |
kvitt.android.library |
FakeRepositories(scenario), FakeScenarios.empty; scenarios of feature X go in fakes/<x>/ |
:data |
kvitt.android.library + kvitt.android.hilt + KSP + Room 3 |
Room entities and KvittDatabase, schema in data/schemas/, Ktor client, sync engine, repositories, updater |
:adapter:fcm |
kvitt.android.library + kvitt.android.hilt |
Firebase Cloud Messaging: the service, token calls and the mapping to PushEvent |
:feature:* |
kvitt.android.feature |
the screens; goldens in feature/<x>/src/test/screenshots/ (full screen 411 × 891 dp at 1x, D-098); top-level screens draw their own KvittNavigationBar (SPEC §13.2, D-071) |
:app |
kvitt.android.application + compose + hilt + screenshots |
Application, MainActivity, NavHost, push and notifications; both variants bind :data (DataBindingsModule through di/AppDataModule.kt, D-081); goldens in app/src/test/screenshots/ |
Architecture check
architectureCheck runs in every module's check (build-logic/.../ArchitectureRules.kt holds the rules as data).
It fails on a project dependency outside the §6.2 table, on :core:fakes in any configuration that is not a test or
debug configuration, on a module's test fixtures outside a test configuration, and, in :core:model and :domain, on an Android plugin, on a library outside
org.jetbrains.kotlin*, and on any import or fully qualified reference outside kotlin.*, kotlinx.* and
se.hagstedt.kvitt.* in src/main. A new module must be added to ArchitectureRules.allowed.
Feature convention
-
Each feature module exposes one composable entry point per screen. By default it obtains its ViewModel with
hiltViewModel()(fromandroidx.hilt:hilt-lifecycle-viewmodel-compose) and takes navigation as plain lambdas, for example:@Composable fun AccountDetailScreen( onOpenExpense: (ExpenseId) -> Unit, onAddExpense: (AccountId) -> Unit, onBack: () -> Unit, modifier: Modifier = Modifier, viewModel: AccountDetailViewModel = hiltViewModel(), )A stateless overload that takes the
*Stateand event lambdas is what previews and screenshot tests render. -
ViewModels are
@HiltViewModelclasses that receive repositories through the interfaces of:core:modelonly.:appbinds those interfaces to:datawith Hilt in both variants; tests and previews use:core:fakes. -
Features never reference another feature, the
NavHostor:data/:domain.:appowns theNavHostand wires the lambdas. -
Strings live in the feature's own
src/main/res/values/strings.xmlandvalues-sv/strings.xml, keys prefixed with the feature name; shared strings in:core:design;:appholds only the app name. -
Previews and screenshot tests use
FakeRepositories(scenario)from:core:fakes(debugImplementationandtestImplementation, added by the feature plugin). Fakes never reach a release build.
Screenshot tests
Roborazzi on Robolectric, wired by kvitt.android.screenshots (the plugin, outputDir src/test/screenshots and the
test libraries; the feature plugin applies it). The harness is shared: :core:design's test fixtures
(src/testFixtures, package se.hagstedt.kvitt.design.testing, testImplementation(testFixtures(project(":core:design"))),
added to every feature by its plugin) hold Variant, TestClock, TestEnvironment, ScreenSnapshots (capture modes
Root, Screen and ScreenWhenIdle), MainDispatcherRule and withMain. A module adds only its own frame around
the content (AnalyticsSnapshots, SettleSnapshots, SettingsSnapshots, :core:design's Snapshots). roborazzi.test.verify=true in gradle.properties makes every unit-test run compare with the
committed goldens. Re-record an intended change with tools/gradle-serial.sh recordRoborazziDebug and open every
changed PNG before committing.
Golden layout (D-098, revising D-066): three variants per state, <name>_sv_light.png, <name>_sv_dark.png and
<name>_en_light.png (English covers string length, dark covers the theme; there is no en_dark), plus
<name>_sv_light_200.png at 200 % font scale for the long states. Every capture renders at 1x density: the
Robolectric qualifier is w411dp-h<height>dp-mdpi, so the Galaxy S26 viewport of 411 × 891 dp is 411 × 891 px and a
golden is a true 1x render, not a downscaled one. :app's empty_screen_<light|dark>.png are the only other names.