Widget-refresh plumbing that Calendula's 2.24.0 widget refactor needs, and that Agendula already hand-rolls. - `GlanceAppWidget.updatePlaced(context, receiverClass)` redraws every placed instance using ids from `AppWidgetManager` + `getGlanceIdBy`. `updateAll` uses Glance's provider-name map, which is empty in a process that hasn't hosted a widget session yet, so the redraw gets dropped (calendula#18, calendula#420). - `updatePlaced(..., bumpRefreshStamp = true)` also moves each instance's `currentRefreshStamp()`. An update to a running session only recomposes and never re-runs `provideGlance`, so a widget that keys its data load on the stamp reloads there too. This is the per-widget `data_stamp` idea from calendula#420. - `WidgetRefresher` turns a burst of "data changed" requests into one debounced redraw, the generic part of Agendula's `TaskWidgetUpdater`. Calendula will use it for its own writes, so they don't have to wait for `PROVIDER_CHANGED`. - `FloretWidgetUpdateReceiver.extraRearmInterval` lets extra actions also re-arm the rollover alarm, at most once per interval. This moves Calendula's 15-minute `PROVIDER_CHANGED` backstop upstream. It defaults to off, so Agendula is unchanged. Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de> Reviewed-on: https://codeberg.org/jlmakiola/floret-kit/pulls/18
70 lines
5.3 KiB
Markdown
70 lines
5.3 KiB
Markdown
# floret-kit
|
|
|
|
The shared **Material 3 Expressive design system + plumbing** for the Floret
|
|
family of Android apps ([Calendula](https://gitea.jeanlucmakiola.de/makiolaj/calendula),
|
|
[Agendula](https://gitea.jeanlucmakiola.de/makiolaj/agendula),
|
|
[Clockula](https://gitea.jeanlucmakiola.de/makiolaj/clockula), and future siblings).
|
|
|
|
"Floret" is the family's design language: a Calendula flower head is a cluster of
|
|
many small *florets*, and each app is one of them. This repo is the bloom they
|
|
share — so a new app doesn't reinvent a settings screen, an edit form, or a theme;
|
|
it draws from here.
|
|
|
|
## How it's consumed
|
|
|
|
Each app embeds this repo as a **git submodule** and wires it in with a Gradle
|
|
**composite build**, so the kit is always built **from source** (no published
|
|
artifacts — keeps every app reproducible for F-Droid):
|
|
|
|
```kotlin
|
|
// app's settings.gradle.kts
|
|
includeBuild("floret-kit")
|
|
```
|
|
```kotlin
|
|
// app module's build.gradle.kts
|
|
implementation("de.jeanlucmakiola.floret:core-time")
|
|
```
|
|
|
|
Gradle's dependency substitution maps `de.jeanlucmakiola.floret:<module>` to the
|
|
local source module — edit the kit and the app picks it up immediately. Android
|
|
modules need an SDK location for the included build: set `ANDROID_HOME` (CI) or
|
|
add a gitignored `<app>/floret-kit/local.properties` with `sdk.dir` (locally).
|
|
See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the full consumption model.
|
|
|
|
## Modules
|
|
|
|
| Module | Type | What it holds |
|
|
|--------|------|---------------|
|
|
| `core-time` | JVM | Date/time helpers: `DayWindow` local-day windows, locale/zone-aware `Instant` formatting, `TimeBridge` (millis ↔ `Instant`). No Android, no deps. |
|
|
| `core-reminders` | JVM | Reminder-lead plumbing: the lead-time unit model (`ReminderUnit`, `decomposeReminderMinutes`), the per-target override model (`ReminderOverride` + `reminderLeadFor` / `applyReminderOverride`) and the stored-format codec (`ReminderOverrideCodec`, separators configurable per app). Each app layers its own DataStore + string labels on top. |
|
|
| `core-locale` | Android | Everything "what language are we in". Per-app language plumbing (`AppLanguage`): read the shipped languages from the app's `res/xml/locales_config.xml`, get/set the applied language via `AppCompatDelegate`, render each language's autonym. Plus `currentLocale()` (observes the applied language from Compose) and `localizedDateFormatter(locale, skeleton)` (lays a field skeleton out in the locale's own order) and `relativeTimeAgo(…)` ("5 minutes ago"). The app passes its own `locales_config`. |
|
|
| `core-prefs` | Android | The typed DataStore layer: `Pref<T>` + `PrefStore` (typed read/write, distinct-until-changed flows), `toEnum()` for values that degrade instead of throwing, and the family's shared appearance prefs (`ThemeMode`, `dynamicColor`). The store's name and construction stay with the app. |
|
|
| `core-di` | JVM | The shared DI vocabulary: `@IoDispatcher`, `@DefaultDispatcher`, `@ApplicationScope` and the Hilt module that provides them. |
|
|
| `core-android` | Android | Small Android plumbing: `broadcastTicks` (a Flow that ticks on system broadcasts), a coroutine `BroadcastReceiver.goAsync { }`, `Context.hasPermission`, `AlarmManager.setWakeup` (exact, or inexact allow-while-idle without the capability) and `Flow.stateWhileSubscribed`. No Compose, no DI. |
|
|
| `core-crash` | Android | Privacy-respecting on-device crash capture + report dialog + issue-tracker hand-off. Parameterized per app via `CrashConfig`. |
|
|
| `identity` | Android | The M3 Expressive theme factory `FloretExpressiveTheme(…)`, `rememberNavSlideSpec()`, the content transitions (`expandEnter`/`collapseExit`/`itemEnter`/…) and the `predictiveBack` peek. Each app supplies its own seed/palette. |
|
|
| `components` | Android | Shared Compose vocabulary + recipes: `GroupedSurface`/`GroupedRow`/`GroupedSwitchRow`/`GroupedHint`, `IconInfoCard`, `InlineTextField`, `OptionCard`, `CollapsingScaffold`, `OptionPicker`/`OptionRows`/`CustomExpandRow`, `ReorderableColumn`, `DebugRibbon`, `pastelize()`, `DialogControls`, `OnboardingScaffold`, `OptionalFormSection`, `AboutCard`, `LanguagePickerRow`, `SnackChip`. |
|
|
| `glance` | Android | Home-screen-widget plumbing on Jetpack Glance: `FloretGlanceTheme`, the rounded `WidgetSurface` (solid/tinted/transparent backgrounds), `WidgetHeader`/`WidgetActionButton`, the position-aware `WidgetGroupedRow`, `WidgetMessage`, `WidgetRollover` (the midnight-rollover alarm), `FloretWidgetUpdateReceiver`, and the refresh helpers `updatePlaced` / `WidgetData` / `WidgetRefresher`. App supplies its own colour schemes, data and receiver glue. |
|
|
|
|
_The principle: the **mechanics** are shared, the **look** stays per-app. See
|
|
[`docs/ROADMAP.md`](docs/ROADMAP.md) for what's extracted, deferred, and
|
|
deliberately not shared._
|
|
|
|
## Docs
|
|
|
|
See [`docs/`](docs/) — start with [`docs/README.md`](docs/README.md)
|
|
([ARCHITECTURE](docs/ARCHITECTURE.md), [ROADMAP](docs/ROADMAP.md)) and
|
|
[`CONTRIBUTING.md`](CONTRIBUTING.md) to add or change a module.
|
|
[`CHANGELOG.md`](CHANGELOG.md) tracks what landed.
|
|
|
|
## Build
|
|
|
|
```sh
|
|
./gradlew :core-time:test # JVM module tests
|
|
./gradlew :core-crash:testDebugUnitTest
|
|
./gradlew :core-prefs:testDebugUnitTest
|
|
./gradlew :core-di:test
|
|
./gradlew :core-android:testDebugUnitTest
|
|
./gradlew build # everything (Android modules need an SDK; see above)
|
|
```
|