D-124 serves Kvitt at https://<host> on 443. The README, the AppBuildInfo and AboutRepositoryImpl comments, the S-21/S-22 fake scenarios and the :data test fixtures drop the :8443 port. S-21's invite-created goldens and every S-22 golden re-recorded (link and server row now portless). The release-URL normaliser test checks a portless URL and keeps one explicit-port case (a typed port survives), on example.org instead of the owner's old host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
6.2 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.
Public host (D-121). The release server's host name is the build property kvitt.publicHost, never a literal in
the code: :app exposes it as BuildConfig.PUBLIC_HOST, as the manifest placeholder publicHost (the host of the
https://<host>/i/<CODE> invite links, AC-2.6) and as RELEASE_SERVER_URL = https://<host>, which S-1
prefills (AC-2.7). Gradle takes -Pkvitt.publicHost=… first (infra/publish-apk.sh passes it), else the key
kvitt.publicHost in android/local.properties, where the owner sets it (git-ignored, never committed), else the
placeholder kvitt.example.invalid, so a release built without it cannot talk to a real host. Debug builds pair with
the address typed on S-1. Tests and goldens never depend on it: they use fixed hosts.
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.