Files
kvitt/android/README.md
T

3.7 KiB
Raw Blame History

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; SPEC §6.2); 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.