Brings the branches the sibling apps actually ship from back onto main, so floret-kit stops forking per project. Merged in: - `vouch/settings` (what vouch pins), which already contains `feat/clockula` (what clockula pins) and `chore/deps-2026-10`: core-prefs, core-di, CollapsingScaffold FAB/no-back slots, the glance module, LocaleManager on API 33+, and the dependency bump - `feat/glance-module`: the three glance commits clockula's branch didn't have yet (progress ring/tile/check button, expressive shapes + header pill, WidgetDoneState) Both merged without conflicts. Version ends up at 0.7.0 (untagged). Heads-up for consumers: the deps bump moves the toolchain to Gradle 9.8.0 / AGP 9.4.1 / Kotlin 2.4.20. Since floret-kit is a composite build, a consumer must be on Gradle >= 9.6 to pin anything from here on. Calendula builds, tests and lints against this branch with just that toolchain bump, no source changes. Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de> Reviewed-on: https://codeberg.org/jlmakiola/floret-kit/pulls/11
7.0 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. |
| Plumbing | core-prefs (Android) |
Typed DataStore layer. The store's name and construction stay with the app. |
| Plumbing | core-di (JVM) |
Dispatcher/scope qualifiers + their Hilt module. The only module that knows about DI. |
| 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,core-prefs,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.
An Android module with no Compose in it applies only
com.android.library—core-prefsis the first such module. The Compose plugin is added per module, when the module actually has composables.
core-diis the one module carrying an annotation processor — the KSP plugin plushilt-compiler. A@Module @InstallIn(...)only reaches a consuming app's Hilt component if the library is processed: the app's processor reads@AggregatedDepsmetadata off the classpath, it does not reprocess library sources. As a non-Android module it useshilt-core+hilt-compilerrather than the Android Hilt Gradle plugin. Every other module stays DI-free.
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.