Add a native Android client that keeps working offline #6

Merged
brian merged 1 commit from android-app into master 2026-08-05 01:05:16 +00:00
Owner

A Kotlin/Compose app in android/, talking to the same REST API the PWA
does, so a phone and a browser are interchangeable views of one instance.
The theme is a direct port of frontend/src/index.css — same colour tokens
and radii, DM Sans/DM Mono bundled as resources — and the icons are
redrawn from the same 24x24 paths the web components inline.

Offline is the reason this exists rather than the PWA. Reads cache as the
raw JSON each list endpoint returned: every screen here consumes whole
lists (GET /medications, /groups, /medications/history), so a keyed blob
cache buys what a relational store would without a second annotation
processor in the build. Writes the server never saw go to an outbox and
replay in order from a WorkManager job that survives the app closing.
Queued ops are also replayed optimistically over the cache, so a dose
logged with no signal moves the card out of Overdue immediately, and a
queued dose stores the instant it was taken rather than the instant it
uploads.

Adding a medication or group has no offline path on purpose — the server
mints the id every queued op refers to — and both say so rather than
failing quietly. Scanning and the assistant need the server for the same
reason it does: they call Claude through it.

Three things are absent by design, not oversight. Admin stays web-only.
OIDC sign-in would need a deep link the backend does not emit, since the
provider redirect lands on the web frontend; username/password works
alongside SSO as before. Web Push registers a browser endpoint an app
cannot claim on the user's behalf, so ntfy and SMS are the reminders
offered here, configured exactly as on the web.

Driven against a local backend in an emulator, which is what caught two
things worth naming: a bare host:port defaulted to HTTPS and so could
never reach a LAN address (private and loopback hosts now default to
HTTP, an explicit scheme always winning), and screens dropped the
optimistic overlay after a sync without re-fetching, briefly showing a
just-synced dose as still overdue.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

A Kotlin/Compose app in android/, talking to the same REST API the PWA does, so a phone and a browser are interchangeable views of one instance. The theme is a direct port of frontend/src/index.css — same colour tokens and radii, DM Sans/DM Mono bundled as resources — and the icons are redrawn from the same 24x24 paths the web components inline. Offline is the reason this exists rather than the PWA. Reads cache as the raw JSON each list endpoint returned: every screen here consumes whole lists (GET /medications, /groups, /medications/history), so a keyed blob cache buys what a relational store would without a second annotation processor in the build. Writes the server never saw go to an outbox and replay in order from a WorkManager job that survives the app closing. Queued ops are also replayed optimistically over the cache, so a dose logged with no signal moves the card out of Overdue immediately, and a queued dose stores the instant it was taken rather than the instant it uploads. Adding a medication or group has no offline path on purpose — the server mints the id every queued op refers to — and both say so rather than failing quietly. Scanning and the assistant need the server for the same reason it does: they call Claude through it. Three things are absent by design, not oversight. Admin stays web-only. OIDC sign-in would need a deep link the backend does not emit, since the provider redirect lands on the web frontend; username/password works alongside SSO as before. Web Push registers a *browser* endpoint an app cannot claim on the user's behalf, so ntfy and SMS are the reminders offered here, configured exactly as on the web. Driven against a local backend in an emulator, which is what caught two things worth naming: a bare host:port defaulted to HTTPS and so could never reach a LAN address (private and loopback hosts now default to HTTP, an explicit scheme always winning), and screens dropped the optimistic overlay after a sync without re-fetching, briefly showing a just-synced dose as still overdue. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Kotlin/Compose app in android/, talking to the same REST API the PWA
does, so a phone and a browser are interchangeable views of one instance.
The theme is a direct port of frontend/src/index.css — same colour tokens
and radii, DM Sans/DM Mono bundled as resources — and the icons are
redrawn from the same 24x24 paths the web components inline.

Offline is the reason this exists rather than the PWA. Reads cache as the
raw JSON each list endpoint returned: every screen here consumes whole
lists (GET /medications, /groups, /medications/history), so a keyed blob
cache buys what a relational store would without a second annotation
processor in the build. Writes the server never saw go to an outbox and
replay in order from a WorkManager job that survives the app closing.
Queued ops are also replayed optimistically over the cache, so a dose
logged with no signal moves the card out of Overdue immediately, and a
queued dose stores the instant it was taken rather than the instant it
uploads.

Adding a medication or group has no offline path on purpose — the server
mints the id every queued op refers to — and both say so rather than
failing quietly. Scanning and the assistant need the server for the same
reason it does: they call Claude through it.

Three things are absent by design, not oversight. Admin stays web-only.
OIDC sign-in would need a deep link the backend does not emit, since the
provider redirect lands on the web frontend; username/password works
alongside SSO as before. Web Push registers a *browser* endpoint an app
cannot claim on the user's behalf, so ntfy and SMS are the reminders
offered here, configured exactly as on the web.

Driven against a local backend in an emulator, which is what caught two
things worth naming: a bare host:port defaulted to HTTPS and so could
never reach a LAN address (private and loopback hosts now default to
HTTP, an explicit scheme always winning), and screens dropped the
optimistic overlay after a sync without re-fetching, briefly showing a
just-synced dose as still overdue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
brian merged commit e54ebe4784 into master 2026-08-05 01:05:16 +00:00
brian deleted branch android-app 2026-08-05 01:05:16 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
brian/medsync!6
No description provided.