Files
kvitt/android
jacobh2 7730a31af6 M6: design system in :core:design (Kalas muted, design-spec §2 and §3)
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
2026-10-18 09:30:49 +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 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() (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 (release) or :core:fakes (debug and tests) with Hilt (M8).

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