T19/T21: map and export, completing Phase 4

flutter_map with the same OSM tiles and no-API-key reasoning that chose osmdroid.
All four load-bearing behaviours ported and, unlike the native app's map, tested:
one polyline per segment so a pause is a real gap, render-only decimation, the
zoom clamp at OSM's max tile zoom with a short-ride fallback, and a real user
agent. Speed colouring is bucketed per run rather than per-vertex, since neither
osmdroid nor flutter_map makes per-vertex paint reasonable.

Export via share_plus, which also handles the iPad popover anchor a naive port
forgets. It passes the raw stored points, never the map's decimated path.

164 tests passing, analyze clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 16:27:42 -05:00
parent bcc12b314f
commit d501528e69
20 changed files with 1085 additions and 478 deletions

View File

@@ -576,3 +576,88 @@ Realistically this machine needs headroom freed outside the dev tooling before P
Next: **Phase 4, the UI** — six screens, and the first widget tests this project has ever
had.
---
## Phase 4 — UI · **complete**
**T15** shell (theme, `go_router`, shared components) · **T16** record screen · **T17**
trips list · **T18** trip detail with stats and charts · **T19** map · **T20**
rename/delete/merge · **T21** export · **T23** widget tests, brought forward.
**164 tests passing, analyze clean.**
### The theme's load-bearing detail, made structural
Compose's `Surface` set `LocalContentColor`; removing it once made a 64 sp speed figure
render black-on-black, and **no test caught it — only a screenshot did**. The Dart theme
sets `bodyColor`/`displayColor` on `TextTheme` explicitly rather than relying on a
wrapping widget, so the failure cannot recur by someone deleting a container. `BigStat`
also names its colour directly, and a widget test asserts that colour differs from the
ground.
### The widget tests immediately earned their keep
**A real layout bug, first run:** with a ride active the record screen grows to six stat
rows and `RenderFlex overflowed by 20 pixels`. Compose *clips this silently*, so the same
bug may well be latent in the native app and simply invisible. Fixed by making the screen
scrollable while still centring when there is room — which matters more here than usual,
because 72 dp glove-sized controls make the content genuinely tall.
### Two Flutter-testing traps, both costly
**`pumpAndSettle` never settles against a repeating timer.** The elapsed clock ticks every
second, so the first widget-test run sat at the framework's 10-minute timeout — for
*every* test. Two fixes: the ticker now runs **only while a ride is active** (better
behaviour regardless — an idle screen has no clock to advance), and tests that do have a
live ride use explicit `pump()` calls.
**`flutter_test` asserts no `Timer` is pending after disposal**, which Drift trips: it
keeps a stream query alive briefly after its last listener leaves so re-subscribing is
cheap. Tests now run through a `screenTest` wrapper that removes the tree and pumps past
that window. Run time went from *timeout* to **two seconds**.
> And again: a killed background job reports exit code 0. Twice during this phase a
> "completed" run had actually been terminated. Always re-read the log.
### Map
`flutter_map`, same OSM raster tiles and same no-API-key reasoning that chose osmdroid.
All four load-bearing behaviours carried over and now **tested**, which the native app's
map never was:
- one polyline per segment, so a pause is a visible gap — the test puts two segments a
degree apart and asserts no polyline straddles it
- decimation **render-only**; a test asserts vertices drop while endpoints survive
- the **zoom clamp** at OSM's max tile zoom of 19, plus a `shortRideZoom` fallback for
degenerate bounds — this is the v2.0 empty-grid bug, now pinned by a test
- a real user agent, or the tile servers return 403
Speed colouring is bucketed into one polyline per run rather than per-vertex paint —
`PolyChromaticPaintList` was fiddly in osmdroid and flutter_map has no equivalent either.
**Still unvalidatable:** no simulator produces velocity, so every path renders in one
colour until a real ride.
### Export
`share_plus` replaces `FileProvider` + `ACTION_SEND`, and handles the iOS popover anchor
an iPad needs. Files are written to the temporary directory — they are a transfer
artefact, not storage. The export deliberately passes the **raw stored points**, never
the map's decimated path.
---
## Remaining
Phases 0–4 are complete. What is left is verification and cutover:
- **T22** uploader + config (the last piece of parity; still no UI, exactly as today)
- **T24** integration tests on both platforms — needs the emulator, so check disk first
- **T25** the real-ride checklist on **both** platforms. Nothing above substitutes for it:
neither simulator produces velocity, so max speed, moving time and speed colouring are
all still unverified.
- **T26** iOS release readiness · **T27** parity audit, including switching the
applicationId from `com.rippr.port` back to `com.rippr`
**Known parity gap so far:** notification actions (Pause/Resume in the shade), lost with
`flutter_foreground_task`.