Ocho
- Role
- Solo Developer
- Timeline
- 2026–Present
- Links
- Repository · Release
- Status
- ACTIVELY MAINTAINED
[+] On This Page
Context
Built for reliability during physical exercise: a minimal, single-purpose workout interval timer for EMOM and Tabata sessions, rather than a general-purpose fitness app with a timer bolted on. The constraint driving every design decision is that you are mid-effort and usually not looking at the screen.
The name fits the same brief. An ocho is a figure-eight step in tango, and it is Spanish for eight, the round count of a classic Tabata.
Architecture
Kotlin and Jetpack Compose on Material 3, with Hilt for dependency injection and a clean-architecture split across domain, data, and ui. The domain layer carries no android.* imports, which is what makes the timing logic testable without an emulator. The engines, the config models, and the version comparison all run as plain JVM unit tests.
Drift-free timing
Every interval boundary is an absolute timestamp anchored to startTime + N × intervalMillis and recalculated on each tick, rather than a sum of delay() calls. A late or missed tick self-corrects instead of compounding; over a twenty-minute session that is the difference between finishing on the minute and finishing several seconds late.
Pause works the same way, accumulating total paused duration and subtracting it:
effectiveElapsed = now - startTime - totalPausedMs
The anchoring survives any number of pauses, because nothing is ever accumulated forward.
The phase color system
The session screen’s background is the primary information channel, not decoration. Four states, each owning one full-bleed plate:
| Phase | Light plate | Dark plate | On-plate text |
|---|---|---|---|
PREPARE |
#E8A317 |
#A8720A |
Ink / White |
WORK |
#E5484D |
#B3282D |
White |
REST |
#B0D8BE |
#5CA47C |
Ink |
COMPLETE |
#7C6CF0 |
#5645C4 |
White |
Rest is a light plate deliberately, and it is the one phase that does not drop to a 700 step in dark theme. Red-500 and green-500 sit at nearly the same lightness, so under deuteranopia (roughly 8% of men, in a category that skews male) work and rest converge into two similar mid-tones and the primary information channel fails outright. Separating them by lightness as well as hue keeps them distinct with no color vision at all, and the on-plate text flips from white to ink as a second, redundant signal.
fun phaseTheme(phase: Phase, dark: Boolean): PhaseTheme = when (phase) {
Phase.PREPARE -> if (dark) PhaseTheme(Amber700, Paper) else PhaseTheme(Amber500, Ink)
Phase.WORK -> if (dark) PhaseTheme(Red700, Paper) else PhaseTheme(Red500, Paper)
Phase.REST -> if (dark) PhaseTheme(Green400, Ink) else PhaseTheme(Green200, Ink)
Phase.COMPLETE -> if (dark) PhaseTheme(Violet700, Paper) else PhaseTheme(Violet500, Paper)
}
Phase is never carried by color alone. The plate, the uppercase label, the audio tone, and the haptic all say the same thing. Because all four signal colors are spent on phases, errors cannot be red-on-red: they render as a tinted plate inside the layout instead, and an error raised mid-session waits for the next rest interval rather than covering the running clock.
Distribution outside Google Play
An in-app checker queries the GitHub Releases API and installs through DownloadManager and PackageInstaller, so the app updates itself without a store. Stable and dev channels ship as separate applicationIds with separate data, so a dev build installs alongside the stable one and neither offers the other’s updates. Dev prereleases are cut on every push to dev, stable builds on every tag off main.
Build discipline
Kotlin, detekt, and Android Lint all run with warnings-as-errors, and every public declaration in src/main requires KDoc. Unit tests and static analysis gate CI on both branches. Presets are DataStore-backed per timer type; sound feedback plays on the alarm stream so silent mode cannot mute a workout cue, and vibration is independently toggleable.
Trade-offs
Staying off the Play Store costs discovery, and it forces every user through the “install unknown apps” prompt. What it buys is that a release reaches devices the moment CI tags it, with no review queue in between, and the in-app updater closes the loop from there.
Disabling Material You gives up personalization that Android users reasonably expect. I don’t think a timer that encodes meaning in color can delegate its palette to a wallpaper. Work has to be red on every device.
Warnings-as-errors and mandatory KDoc make small changes cost more up front. The payoff is that the domain layer stayed dependency-free long enough to be worth testing, instead of eroding one convenient import at a time.
Outcome
Nine tagged releases so far, from v1.0.0 through v3.0.0. That last one carried the rebrand from DK Timer to Ocho, a documented design system, and GPL-3.0 licensing with a dual-licensing provision. I still actively develop and maintain it.