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>
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_HOMEenv var (the apps set it at the job level), or - locally: a gitignored
<app>/floret-kit/local.propertieswithsdk.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.libraryandorg.jetbrains.kotlin.plugin.compose— not a separateorg.jetbrains.kotlin.androidplugin (applying it errors). They setjvmTargetdirectly in akotlin { 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:
- No
foojaytoolchain 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 setjvmTargetdirectly instead of using a Java toolchain block that would need a resolver. - No published artifacts. Built from source, pinned by commit.
- 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:
identityexposes a theme factory; each app passes its own seed-derivedlightScheme/darkSchemeand typography. Seed colours and hand-tuned fallbacks are never shared constants.componentsare app-agnostic primitives. Where two apps differ, the difference is an explicit parameter (e.g.InlineTextField'scapitalization) 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.