Files
floret-kit/docs/ARCHITECTURE.md
Jean-Luc Makiola 25ec248e73 docs: changelog, docs/ set, CONTRIBUTING, LICENSE
Bring floret-kit up to the family's repo conventions:
- CHANGELOG.md (Keep a Changelog) — what each module added.
- README.md — full module table (core-time/core-crash/identity/components) +
  consumption notes and a docs index.
- docs/README.md (index), docs/ARCHITECTURE.md (module layering, the submodule +
  composite-build consumption model, JVM vs android-library, the reproducibility
  rules, the share-mechanics-not-look contract), docs/ROADMAP.md (extracted vs
  deferred vs deliberately-not-shared, from the cross-app survey).
- CONTRIBUTING.md — the no-app-coupling rules, how to add a module, how an app
  adopts the kit.
- LICENSE (MIT, matching the apps).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 13:42:53 +02:00

6.1 KiB

floret-kit — architecture

How the kit is structured, built, and consumed as it stands today. For what is and isn't extracted, see ROADMAP.md; for the practical how of adding a module, see ../CONTRIBUTING.md.


1. The thesis in one sentence

floret-kit is a Gradle build of small, focused modules — plumbing and a Material 3 Expressive design layer — that the family's apps draw from instead of re-deriving the same code. The whole design hangs off one rule:

Share the mechanics, keep the look per-app.

Theme machinery, content-provider plumbing, and component primitives are shared; seed colours, palettes, identity chips, domain types, and app-specific wording stay in each app. The kit never knows about a specific app's domain.


2. How an app consumes the kit

Each app embeds this repo as a git submodule and wires it in as a Gradle composite build. The kit is therefore always built from source — there are no published binaries — which keeps every consuming app reproducible (a requirement for official F-Droid).

// <app>/settings.gradle.kts
includeBuild("floret-kit")
// <app>/app/build.gradle.kts
implementation("de.jeanlucmakiola.floret:core-time")
implementation("de.jeanlucmakiola.floret:components")

Every module sets group = "de.jeanlucmakiola.floret" (via the root build's subprojects {}), so Gradle's dependency substitution maps de.jeanlucmakiola.floret:<module> to the local project(":<module>") of the included build. Editing the kit is picked up by the app's next build with no publish step.

Each app pins a specific kit commit through its submodule gitlink. So a breaking change in the kit can't reach an app until that app deliberately re-pins — apps stay on independent cadences (important while Calendula ships and Agendula moves fast).

SDK location for the included build

An included Android build needs to locate the Android SDK independently of the host app. Provide it via:

  • CI: an ANDROID_HOME env var (the apps set it at the job level), or
  • locally: a gitignored <app>/floret-kit/local.properties with sdk.dir.

Pure-JVM modules (core-time) need neither.

Submodules in CI / F-Droid

Consuming apps must check out submodules (submodules: recursive in the checkout step) so floret-kit/ isn't empty. For an app published via official F-Droid reproducible builds (Calendula), the fdroiddata recipe additionally needs submodules: true on the build entry, and the app's reproducibility guard should scan the submodule's Gradle scripts (see §5).


3. Module layering

Layer Modules Notes
Plumbing core-time (JVM) Pure logic; no Android, no deps.
Plumbing core-crash (Android) Self-contained subsystem; parameterized per app.
Design — identity identity (Android) Theme factory + motion. App supplies seed/palette.
Design — components components (Android) Shared Compose primitives. App composes screens.

Dependencies between modules are kept minimal: core-time depends on nothing in the kit; identity and components depend only on Compose + (for components) material-icons-core. There is no app-level or cross-domain coupling.

Each module's package is de.jeanlucmakiola.floret.<area> (e.g. de.jeanlucmakiola.floret.components).


4. Module types & the build

Two kinds of module:

  • JVM library (core-time) — org.jetbrains.kotlin.jvm, jvmTarget = 17, JUnit 5 + Truth tests. The right home for pure logic; cheapest to build and test.
  • Android library (core-crash, identity, components) — com.android.library + the Compose plugin, compileSdk 37 / minSdk 29.

AGP 9.x provides built-in Kotlin compilation. Android modules therefore apply only com.android.library and org.jetbrains.kotlin.plugin.composenot a separate org.jetbrains.kotlin.android plugin (applying it errors). They set jvmTarget directly in a kotlin { compilerOptions { … } } block.

Versions live in gradle/libs.versions.toml, kept in lockstep with the apps' catalogs.


5. Reproducibility rules

Because consuming apps build the kit from source — and at least one publishes via official F-Droid reproducible builds — the kit follows the same invariants:

  1. No foojay toolchain resolver, anywhere (not even as a token in a comment). It can fetch a JDK over the network at build time, which the offline F-Droid source scanner rejects. Modules set jvmTarget directly instead of using a Java toolchain block that would need a resolver.
  2. No published artifacts. Built from source, pinned by commit.
  3. Android library modules emit no APK-level metadata of their own (no buildConfig); the consuming app controls APK-level reproducibility (vcsInfo/dependenciesInfo).

A consuming app's reproducibility guard should scan the submodule's *.gradle.kts for the resolver too, since the included build is part of the from-source graph.


6. The design-divergence contract

The design modules (identity, components) carry the family's look, so they extract mechanics and leave identity to the app:

  • identity exposes a theme factory; each app passes its own seed-derived lightScheme/darkScheme and typography. Seed colours and hand-tuned fallbacks are never shared constants.
  • components are app-agnostic primitives. Where two apps differ, the difference is an explicit parameter (e.g. InlineTextField's capitalization) or a callback, never a silently-picked winner. Apps keep their own identity chips/icons and compose their own screens.
  • String resources in the kit are English fallbacks; an app overrides them (with its localized values) via resource merge. App-specific wording stays in the app.

When a primitive is shared by adopting one app's version as canonical (e.g. components' GroupedSurface follows Agendula's cleaner abstraction), that is a deliberate, signed-off convergence — the other app's surfaces change when it re-pins, reviewable at that point.