KvittTheme with every Material and Kvitt colour role light and dark, the 14 category colours with containers, chart and avatar colours, type scale with tabular amount styles, spacing, shapes (stable fallbacks: MaterialShapes is not in Material 3 1.4.0), elevation and motion springs; 191 Material Symbols Rounded vector drawables (33 category tokens filled and outline, UI icons); 253 shared strings from docs/ux/strings.csv in values and values-sv with plurals; pure-Kotlin MoneyFormatter, DateFormatter (ISO weeks) and AmountInput with unit tests; every component of design-spec §3 with TalkBack semantics, including the ledger rows and editor fields (D-019) and Canvas charts; a preview catalogue in the debug source set; 212 Roborazzi goldens (49 sheets x light/dark x sv/en plus 16 at 200 %). The two :app empty-screen goldens are re-recorded because they render the real theme now (PNG only). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
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 |
KvittTheme placeholder; M6 owns the module |
:core:fakes |
kvitt.android.library |
FakeRepositories(scenario), FakeScenarios.empty; scenarios of feature X go in fakes/<x>/ |
:data |
kvitt.android.library + KSP + Room 3 |
Room entities and KvittDatabase v1, schema in data/schemas/; M5 owns the module |
:adapter:fcm |
kvitt.android.library |
shell; M8 owns it |
:feature:* |
kvitt.android.feature |
shells with one entry composable; M7a to M7f own them |
:app |
kvitt.android.application + compose + hilt + Roborazzi |
Application, MainActivity, 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, 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:data(release) or:core:fakes(debug and tests) with Hilt (M8). -
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. 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.