Files
kvitt/android/README.md
T
jacobh2 a92c236689 Android overhaul: stale comments and the module README
* 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
2026-10-18 23:13:12 +02:00

72 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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()` (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 in both variants; 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, 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.