Files
floret-kit/docs/ARCHITECTURE.md
T
makiolaj 394ac12236 feat(core-android): receiver, permission, alarm and Flow helpers
New Android module (no Compose, no DI) with broadcastTicks, a coroutine
BroadcastReceiver.goAsync, Context.hasPermission, AlarmManager.setWakeup and
Flow.stateWhileSubscribed.
2026-10-15 16:52:14 +02:00

163 lines
7.1 KiB
Markdown

# 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`](ROADMAP.md); for the practical how of
adding a module, see [`../CONTRIBUTING.md`](../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).
```kotlin
// <app>/settings.gradle.kts
includeBuild("floret-kit")
```
```kotlin
// <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. |
| Plumbing | `core-prefs` (Android) | Typed DataStore layer. The store's name and construction stay with the app. |
| Plumbing | `core-android` (Android) | Receiver, permission, alarm and Flow helpers. No Compose, no DI. |
| 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`, `core-android`, `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.compose` —
> **not** a separate `org.jetbrains.kotlin.android` plugin (applying it errors).
> They set `jvmTarget` directly in a `kotlin { compilerOptions { … } }` block.
> **An Android module with no Compose in it applies only `com.android.library`**
> — `core-prefs` is the first such module. The Compose plugin is added per
> module, when the module actually has composables.
> **`core-di` is the one module carrying an annotation processor** — the KSP
> plugin plus `hilt-compiler`. A `@Module @InstallIn(...)` only reaches a
> consuming app's Hilt component if the *library* is processed: the app's
> processor reads `@AggregatedDeps` metadata off the classpath, it does not
> reprocess library sources. As a non-Android module it uses `hilt-core` +
> `hilt-compiler` rather than the Android Hilt Gradle plugin. Every other module
> stays DI-free.
Versions live in [`gradle/libs.versions.toml`](../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.