Android app review: fixes, performance and cleanups #336

Merged
kfickel merged 16 commits from android-app-review into main 2026-10-25 21:19:32 +01:00
Collaborator

The findings of a review of the Android app, one commit each. The architecture findings were filed as #332, #333, #334 and #335; all four are solved here.

Bugs

  • The filter of the Stream tab was dropped when the author or a switch of the Changes tab changed. It is now kept for the URL, branch and folder of the clone.
  • A worker that could not read the stream within two minutes withdrew every notification and forgot what had rung, so every reminder rang again. Streamd.ready() now returns a Readiness, and only a disconnected repository withdraws the notifications.
  • Accepting a host key, saving a token or importing a key could throw an IOException or a Keystore ProviderException, which ended the app. These now fail as GitException, and the screens show the failure.
  • The lines of the configuration list contained hard-coded English (extensible, 2 rules, breaks into …, or, 3 days, …). The helpers now return LineParts, which lineText words from strings.xml.

Performance

  • A load read every note twice. Stream::load of the core now gives the summary of every file, made while the text is read (store::load_shards_with), and the bindings carry it in LoadedStream.summaries.
  • health() read the whole repository after every save. Only the Changes tab and the configuration ask for it now, while they are shown.
  • The timesheet screen computed the report twice; timesheetReportWeek gives the report and its week in one call. The activity screen asked for a day twice; the facade now returns a day with every shard of it, and activityView is gone.

Duplicated and dead code

  • RepositoryConnection: written, renamed and writtenAll now share commitSaved.
  • StreamSession: withReady and failing replace the hand-copied checks and catches. Several methods caught only the core's exceptions, so a failing store could escape them. TextRange.indices() replaces six copies of the same conversion.
  • Moves / rememberMoves hold the move, undo and problem logic that StateListScreen and NoteScreen both copied, without the redundant reload after a refused move.
  • File.replaceAtomically replaces the atomic write that FileStore and Credentials each had.
  • Removed ruleText, DateScroll.dateAt and RangeDraft.applicable, which only the tests called.

Architecture

  • #332: core/ no longer uses Compose. ActivitySeries keeps its colour as text (hex), and ui/Activity.kt makes it a Color. core/Activity.kt is documented like the rest of core/, and Activities has no "Unavailable" defaults.
  • #333: StreamViewModel no longer implements eleven interfaces. Each interface has a small adapter in the new package adapters/, built on a StreamAccess that does the shared work once: the session or NO_STREAM, the moment of the call, and telling the repository about a write. App takes one AppActions instead of eleven parameters. The view model keeps the flows, the pins and the start.
  • #334: an interface the screens are given now sits next to the plain types it uses: the stream's in core/, the repository's in git/. RepositoryActions (from StreamViewModel.kt) and ChangesActions (from ui/Changes.kt) moved to git/RepositoryActions.kt, and AGENTS.md names the rule.
  • #335: StreamState.Ready has a StreamVersion, which the session makes anew with every change. The seven screens that took the bindings' Stream as Any to key their effects now take that version.

Checks

  • nix flake check passes.
  • nix build .#streamd-app passes, including the unit tests and the screenshot validation; the screenshots are unchanged.
  • New tests in the core, the bindings (Rust and Kotlin) and the app cover the summaries, the whole day of an activity, the combined timesheet call, the readiness, a host key or secret that cannot be kept, the line parts, the adapters and the versions of the stream.
The findings of a review of the Android app, one commit each. The architecture findings were filed as #332, #333, #334 and #335; all four are solved here. **Bugs** - The filter of the Stream tab was dropped when the author or a switch of the Changes tab changed. It is now kept for the URL, branch and folder of the clone. - A worker that could not read the stream within two minutes withdrew every notification and forgot what had rung, so every reminder rang again. `Streamd.ready()` now returns a `Readiness`, and only a disconnected repository withdraws the notifications. - Accepting a host key, saving a token or importing a key could throw an `IOException` or a Keystore `ProviderException`, which ended the app. These now fail as `GitException`, and the screens show the failure. - The lines of the configuration list contained hard-coded English (`extensible`, `2 rules`, `breaks into …`, `or`, `3 days`, …). The helpers now return `LinePart`s, which `lineText` words from `strings.xml`. **Performance** - A load read every note twice. `Stream::load` of the core now gives the summary of every file, made while the text is read (`store::load_shards_with`), and the bindings carry it in `LoadedStream.summaries`. - `health()` read the whole repository after every save. Only the Changes tab and the configuration ask for it now, while they are shown. - The timesheet screen computed the report twice; `timesheetReportWeek` gives the report and its week in one call. The activity screen asked for a day twice; the facade now returns a day with every shard of it, and `activityView` is gone. **Duplicated and dead code** - `RepositoryConnection`: `written`, `renamed` and `writtenAll` now share `commitSaved`. - `StreamSession`: `withReady` and `failing` replace the hand-copied checks and catches. Several methods caught only the core's exceptions, so a failing store could escape them. `TextRange.indices()` replaces six copies of the same conversion. - `Moves` / `rememberMoves` hold the move, undo and problem logic that `StateListScreen` and `NoteScreen` both copied, without the redundant reload after a refused move. - `File.replaceAtomically` replaces the atomic write that `FileStore` and `Credentials` each had. - Removed `ruleText`, `DateScroll.dateAt` and `RangeDraft.applicable`, which only the tests called. **Architecture** - #332: `core/` no longer uses Compose. `ActivitySeries` keeps its colour as text (`hex`), and `ui/Activity.kt` makes it a `Color`. `core/Activity.kt` is documented like the rest of `core/`, and `Activities` has no "Unavailable" defaults. - #333: `StreamViewModel` no longer implements eleven interfaces. Each interface has a small adapter in the new package `adapters/`, built on a `StreamAccess` that does the shared work once: the session or `NO_STREAM`, the moment of the call, and telling the repository about a write. `App` takes one `AppActions` instead of eleven parameters. The view model keeps the flows, the pins and the start. - #334: an interface the screens are given now sits next to the plain types it uses: the stream's in `core/`, the repository's in `git/`. `RepositoryActions` (from `StreamViewModel.kt`) and `ChangesActions` (from `ui/Changes.kt`) moved to `git/RepositoryActions.kt`, and AGENTS.md names the rule. - #335: `StreamState.Ready` has a `StreamVersion`, which the session makes anew with every change. The seven screens that took the bindings' `Stream` as `Any` to key their effects now take that version. **Checks** - `nix flake check` passes. - `nix build .#streamd-app` passes, including the unit tests and the screenshot validation; the screenshots are unchanged. - New tests in the core, the bindings (Rust and Kotlin) and the app cover the summaries, the whole day of an activity, the combined timesheet call, the readiness, a host key or secret that cannot be kept, the line parts, the adapters and the versions of the stream.
The filter of the Stream tab was remembered by the whole settings, so
changing the author or a switch of the Changes tab dropped it. It is now
kept for the notes of one clone: its URL, branch and folder.
Streamd.ready() gave null both when no repository was connected and when
the stream was not read within two minutes, and the worker withdrew every
notification and forgot what had rung in either case, so the reminders
rang again on the next check. ready() now says which (Readiness), and only
a disconnected repository withdraws them.
The app made the card of each file from its summary, and read the file a
second time for it after the core had loaded the stream. Stream::load now
gives the summary of every root shard, made while the text was read
(store::load_shards_with), and the bindings carry it in LoadedStream.
health() reads the text of every file and lists the folder, and App asked
for it with every new stream, after every save. Only the Changes tab and
the configuration show it, so they ask for it, while they are open.
written, renamed and writtenAll of RepositoryConnection each repeated the
check of the settings, the commit of what changed of their paths and the
push. commitSaved does it once, given the files and the wording.
Most methods of StreamSession repeated the check that the stream is read
and the catch of what the core and the store throw, and list, preview,
overview, week, move and undo caught only the core, so a failing store
escaped them. withReady and failing do it once, for every method; a range
of the bindings becomes the characters of a string by indices().
StateListScreen and NoteScreen each held the way back from a move, its
timer and the problem, and asked for their list or note again after a
refused move, which the new stream of the session already makes them do.
Moves and rememberMoves hold it once.
FileStore and Credentials each wrote a temporary file beside the target
and moved it over it. File.replaceAtomically does it for both.
The timesheet screen asked for the report and then for its week, which
computed the report again; timesheetReportWeek of the bindings gives both
from one. The activity screen asked for a day twice, the second time for
every shard its timeline shows; the facade now gives a day with every
shard of it, whatever the limit, and activityView is gone.
ruleText, DateScroll.dateAt and RangeDraft.applicable were not used by the
app; their tests now hold what is used instead.
Accepting a host key wrote known_hosts and saving a token or an imported
key wrote through the Keystore, and an IOException or a ProviderException
there escaped the coroutine of the view model and ended the app. Both now
fail as GitException, which the clone and the key say.
fix(android): word the lines of the configuration from the resources
All checks were successful
Continuous Integration / Conventional Commits (push) Successful in 15s
Continuous Integration / Lint, Check & Test (push) Successful in 5m58s
Continuous Integration / Build Package (push) Successful in 15m22s
9eeaa63f93
effectText, dimensionText, commandText and triggerText wrote English
words into the lines of the configuration (extensible, not passed on,
2 rules, breaks into, done into, or, 3 days, 2 series). They now give
LineParts, which lineText words from strings.xml.
ActivitySeries carried a Compose Color, so core/ and its tests depended on
Compose UI. It keeps the colour as the text the bindings give (hex), and
ui/ makes it a colour. core/Activity.kt is documented like the rest of
core/, and Activities has no defaults that only fakes needed.

Closes #332
refactor(android): split the view model into an adapter per concern
Some checks failed
Continuous Integration / Conventional Commits (push) Successful in 17s
Continuous Integration / Lint, Check & Test (push) Successful in 1m40s
Continuous Integration / Build Package (push) Has been cancelled
0483d5c20a
StreamViewModel implemented eleven interfaces, each method wrapping the
session with the clock, the failure without a repository and the commit
after a write, and App took thirteen parameters that were all of it. Each
interface now has its adapter in adapters/, on a StreamAccess that does
what they share once; App takes one AppActions, and the view model keeps
the flows, the pins and the start.

Closes #333
RepositoryActions was in StreamViewModel.kt and ChangesActions in
ui/Changes.kt, while every other interface the screens are given is in
core/ next to its records. Both are now in git/, next to the types of the
repository they use, and AGENTS.md names the rule.

Closes #334
refactor(android): give the screens a version of the stream to reload by
All checks were successful
Continuous Integration / Conventional Commits (push) Successful in 14s
Continuous Integration / Build Package (push) Successful in 38s
Continuous Integration / Lint, Check & Test (push) Successful in 1m26s
4591dce37b
StateListScreen, NoteScreen, TimesheetScreen, TaskFilterSheet,
ActivityScreen, ActivityShardsScreen and ConfigScreen took the Stream of
the bindings as Any, only to key their effects by it. StreamState.Ready
now has a StreamVersion, which the session makes anew with every change,
and the screens take that.

Closes #335
kfickel deleted branch android-app-review 2026-10-25 21:19:32 +01:00
Sign in to join this conversation.
No reviewers
No labels
android
cli
epic
planned
tui
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
kfickel/streamd!336
No description provided.