Files
kvitt/android
jacobh2 8229ed60ea Android overhaul: one screenshot harness in :core:design's test fixtures; kvitt.android.screenshots wires Roborazzi
Eight modules each had a copy of the same configuration-context harness (ScreenSnapshots/Snapshots); M12f had to fix
the same capture bug in two of them. :core:design's test fixtures (src/testFixtures, package
se.hagstedt.kvitt.design.testing) now hold Variant, TestClock, TestEnvironment, ScreenSnapshots with its three
capture modes (Root; Screen, with the hand-advanced frame clock; ScreenWhenIdle), MainDispatcherRule and withMain.
Test fixtures rather than a :core:testing module: :core:design's own tests can use them without a dependency on
itself, no module is added to the §6.2 graph, and lint treats them as test sources. The architecture check rejects
test fixtures outside test configurations. Modules keep only their own frame (analytics household, settle size and
inline overlays, settings avatar pictures). Every golden is unchanged.

kvitt.android.screenshots applies Roborazzi with outputDir src/test/screenshots and adds the screenshot test
libraries; the feature plugin applies it and adds the test fixtures, so the seven feature build scripts lose their
copied blocks. Removed: roborazzi-junit-rule and roborazzi-compose (never imported), the explicit
lifecycle-viewmodel-compose (feature plugin, :app) and hilt-lifecycle-viewmodel-compose in :app (both still arrive
through the features; release and debug runtime classpaths are identical), and the unused catalog entries
androidx-hilt-navigation-compose and androidx-lifecycle-viewmodel-compose. @Before/@After setMain pairs in nine test
classes use MainDispatcherRule.

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
..

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/<x>/ (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/<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); 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:

    @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.

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.