Removes AI-writing patterns across the docs, store listing and templates: dash-heavy rhythm, not-X-but-Y contrasts, one-line kickers and decorative bold. Facts, code, links and headings are unchanged. Also fixes leftovers copied from the sibling apps: the crash report template promised "no calendar content", and CONTRIBUTING described Agendula's task-provider layout, test seams and commit style.
6.1 KiB
Contributing to Clockula
Thanks for your interest in Clockula, a Material 3 Expressive clock app for
alarms, timers, stopwatch and world clock. Unlike its siblings it owns its
storage, because there is no open provider behind a clock. Before you start, skim docs/ARCHITECTURE.md
(how it's built), docs/ROADMAP.md (what's next), and
docs/PLAN.md (the design rationale). This file covers the
practical how.
The one architectural rule
Everything above the data layer talks to a repository and sees only domain types
and Flows. Room entities, DAOs and @Query strings never leak above the data
layer. Clockula owns its storage, which makes this rule more important here
than in the siblings, not less: it is the only thing keeping the schema a
private implementation detail rather than the app's de-facto domain model. If a
change would expose a Room type to a ViewModel or the UI, it's in the wrong
layer; see docs/PLAN.md §0.
Prerequisites
- JDK 17
- Android SDK: compileSdk 37, build-tools 36.0.0 (the Gradle wrapper handles AGP/Kotlin)
- A real device for anything touching the alarm engine. An emulator will not tell you whether an alarm survives doze, a reboot, or an OEM battery-saver, and that is the only question that matters about an alarm.
Build, test, lint
./gradlew :app:assembleDebug # build the debug APK
./gradlew :app:testDebugUnitTest # JVM unit tests (JUnit5 + Truth + Turbine)
./gradlew lintDebug # Android lint (CI runs this on every PR)
CI (.forgejo/workflows/ci.yaml, on Codeberg) runs a reproducible-release invariant check,
then lint → unit tests → debug build on every pull request, so run these locally
before opening a PR. Keep CI green.
Translations
Never edit a values-*/strings.xml file in a pull request. Translations are
owned by a self-hosted Weblate that writes to this repository directly, and a
hand-edit is overwritten on the next sync.
Adding a new English string to values/strings.xml is normal PR work; Weblate
picks it up and offers it to translators. Partial translations are expected and
fine; missing keys are informational. Stale and orphaned keys are not, so run
python3 scripts/check_translations.py
before pushing. It reports those more clearly than lint's MissingTranslation
does, and it's what the Translations check runs on every PR.
A new language also needs one <locale> line in
app/src/main/res/xml/locales_config.xml. That file is the single source of
truth for both the in-app picker and the Android 13+ per-app language setting.
Where to put code
| Layer | Lives in | Rule of thumb |
|---|---|---|
| Pure logic (models, scheduling and ring policies, formatting, the self-check rules) | domain/ |
No Android imports; must be JVM-unit-testable. |
| Storage | data/ (db/, alarms/, timers/, stopwatch/, worldclocks/, prefs/, backup/) |
The only place that knows Room or DataStore exists. Everything above it goes through a repository interface. |
| Engines and their Android seams | alarm/, timer/, stopwatch/, ring/, interop/, widget/ |
The engine is plain Kotlin; AlarmManager, services and receivers sit behind seams in its android/, receiver/ and service/ subpackages. |
| Screens | ui/<area>/ |
One ViewModel + immutable UiState per area; Compose for the screen. |
docs/ARCHITECTURE.md §3 lists every package.
Code style & conventions
- Kotlin, 4-space indent, LF line endings, final newline, no trailing
whitespace, all enforced by
.editorconfig(2-space for yaml/toml/json/md). Match the surrounding code. - Material 3 Expressive for all UI: use
MaterialExpressiveTheme, the colour-scheme tokens (never hardcoded colours), and canonical M3 components (e.g.ListItemfor rows). Consult thematerial-3skill before designing a new screen or component. - Prefer the domain layer for anything testable; keep Android APIs behind the
seams in each engine's
android/subpackage so the rest stays JVM-testable.
Tests
- New domain logic (scheduling, ring policies, formatting, mappers) must
come with JVM unit tests under
app/src/test/. Repositories and engine seams are the JVM-testable boundary: fake them rather than reaching for instrumentation. - Add an instrumented test only when a path genuinely needs a real device (Room migrations, AlarmManager, a foreground service).
Commits & PRs
- Write focused commits with clear messages (the existing history uses short,
conventional subjects like
feat(widgets): .../fix(ringtones): ...). - Update
CHANGELOG.mdunder[Unreleased]for any user-visible change. Its sections feed the release notes and F-Droid "What's New" (seedocs/RELEASING.md). - If your change shifts the architecture or completes a milestone, update
docs/ARCHITECTURE.md/docs/ROADMAP.mdin the same PR. - Don't bump
versionName/versionCodein a regular PR. The committedversionNameis bumped only when cutting a release (that bump reachingmainis what triggers the release; the pipeline then mints the tag). Seedocs/RELEASING.md.
Scope
v1 is four surfaces (alarms, timers, stopwatch, world clock) done properly,
and nothing else. Widgets, a Quick Settings tile, screensaver mode and a bedtime
schedule are all wanted, and all deliberately deferred until v1 is finished; see
docs/ROADMAP.md.
Two things are out of scope by design. A sync stack of our own: your alarms export to a documented JSON file, and syncing them is a job for something that already does syncing well. Anything that makes an alarm less reliable in exchange for a feature: if a change trades away the certainty that an alarm rings, it loses, however nice the feature is.
License
By contributing you agree your contributions are licensed under the project's MIT License.