# 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`, tokens, shared components, `Canvas` charts, formatters and shared strings (M6, `3080804`; fixes M6b, `3a1bd14`; SPEC §6.2); M6 owns the module | | `:core:fakes` | `kvitt.android.library` | `FakeRepositories(scenario)`, `FakeScenarios.empty`; scenarios of feature X go in `fakes//` (added by M7a to M7f) | | `: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` | screens built by M7a to M7f (landed 2026-10-18), goldens in `feature//src/test/screenshots/` (full screen 411 × 891 dp, xhdpi); top-level screens draw their own `KvittNavigationBar` (SPEC §13.2, D-071); M7g does the cleanups after M6b | | `:app` | `kvitt.android.application` + compose + hilt + Roborazzi | Application, MainActivity, goldens in `app/src/test/screenshots/`; release binds `:data` (`DataBindingsModule`, `e6ad858`), debug binds the fakes in `src/debug/.../DebugFakesModule.kt` (`72f21b2`) until M8 deletes it; M8 owns the module | ## 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()` (from `androidx.hilt:hilt-lifecycle-viewmodel-compose`) and takes navigation as plain lambdas, for example: ```kotlin @Composable fun AccountDetailScreen( onOpenExpense: (ExpenseId) -> Unit, onAddExpense: (AccountId) -> Unit, onBack: () -> Unit, modifier: Modifier = Modifier, viewModel: AccountDetailViewModel = hiltViewModel(), ) ``` A stateless overload that takes the `*State` and event lambdas is what previews and screenshot tests render. * ViewModels are `@HiltViewModel` classes that receive repositories **through the interfaces of `:core:model`** only. `:app` binds those interfaces to `:data` with Hilt (release since `e6ad858`, debug too once M8 deletes the temporary debug fakes module); tests and previews use `:core:fakes`. * Features never reference another feature, the `NavHost` or `:data`/`:domain`. `:app` owns the `NavHost` and wires the lambdas. * Strings live in the feature's own `src/main/res/values/strings.xml` and `values-sv/strings.xml`, keys prefixed with the feature name; shared strings in `:core:design`; `:app` holds only the app name. * Previews and screenshot tests use `FakeRepositories(scenario)` from `:core:fakes` (`debugImplementation` and `testImplementation`, 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.