V3-04 through V3-07, V3-10, V3-16 shipped complete; V3-11/V3-12/V3-14 shipped code-complete pending device/account verification; V3-08/V3-09 deferred behind a new V3-17 (self-hosted OSRM investigation). 316 tests passing, up from 221. The APK is a fresh release build (debug-signed, no release signing config exists yet) with two build fixes applied: core library desugaring enabled for flutter_local_notifications, and sentry_flutter bumped to 9.27.0 (8.14.2's bundled Kotlin plugin was incompatible with this project's Kotlin 2.4.0 toolchain).
4.5 KiB
V3-04 — Live map on the recording screen
Phase Live map · Depends on nothing · Size M · Status Done
Goal
While recording, show the path as it is drawn, on the recording screen.
Context
v2 shipped without one deliberately: the phone rides in a pocket, so a live map would burn battery for something nobody is looking at. That decision is reversed — Dylan wants it for visual appeal, and because a mounted phone is now a real use case (V3-05).
The map component already exists (RideMap) and already handles per-segment polylines,
render-only decimation and the zoom clamp. This ticket is mostly about lifecycle, not
drawing.
Design
Two constraints survive the reversal and are non-negotiable:
RecordingEnginemust never reference a map. Rendering belongs to a visible screen's widget lifecycle. The engine already exposes everything needed.- No tile fetch or redraw while backgrounded. A pocketed phone must cost exactly what it costs today.
Feed the map from a stream of the current trip's points. watchTripStats exists but
returns aggregates; this needs the points themselves — add a watchPointsForTrip Drift
stream, which updates naturally on each writer flush (~2 s), not per fix.
Follow the rider: keep the latest point centred, with a manual-pan override that stops auto-follow until re-enabled.
Behind the existing map toggle, off by default while it is unproven on battery.
Implementation
watchPointsForTrip(tripId)inAppDatabase- Hoist the map above the stats card on the record screen, behind the toggle
WidgetsBindingObserver— onAppLifecycleState.paused, stop tile fetching; resume onresumed. This is the load-bearing part.- Auto-follow with a pan override
- Keep the numeric readout visible; the map must not push SPEED off screen (the record
screen already scrolls — see the overflow fix in
port/PROGRESS.md)
Acceptance criteria
- The path appears and extends while recording
- Backgrounding the app stops all tile activity, verified in a network log
RecordingEnginestill has no map import — grep it- With the toggle off, no map widget is constructed at all
- Speed and elapsed remain visible without scrolling on a common phone size
Tests
- Widget: map appears only when recording and the toggle is on
- Widget: lifecycle transition to paused stops the tile layer
- The existing map tests still pass
Risks
- Battery. This is the whole reason v2 said no. Measure before defaulting it on (V3-13).
- Redrawing per fix rather than per flush would be wasteful; drive from the database stream, which is already batched.
Out of scope
Mounted mode (V3-05). Other riders on the map (v4).
Outcome
Shipped as designed. AppDatabase.watchPointsForTrip/watchSegmentsForTrip feed two
autoDispose.family providers (livePointsProvider, liveSegmentsProvider) keyed by
trip id; a _LiveMap adapter widget on the record screen reads them and hands the result
to the existing RideMap, unchanged in shape. RecordingEngine was never touched —
verified by test/architecture_test.dart, which greps the source rather than trusting a
comment.
RideMap itself grew two small, general capabilities rather than a parallel "live"
widget: a WidgetsBindingObserver that drops the TileLayer entirely (not just visually,
via widget tree omission) outside AppLifecycleState.resumed, and an optional follow
flag that recentres on the latest point via didUpdateWidget + a post-frame
MapController.move, cancelled permanently by the first user-gesture pan. Both apply
to the trip-detail map too, which is a free win: a backgrounded detail screen no longer
holds tiles fetching either.
Three existing record-screen tests (recording swaps to PAUSE and STOP, paused offers RESUME, discard asks before destroying anything) had to gain an explicit map: false —
they predate this ticket and would otherwise have started constructing a real
FlutterMap/TileLayer against an active trip, which is exactly the tile-fetch-in-tests
problem the trip-detail tests already route around.
3 new tests: the grep-based engine-purity check in architecture_test.dart, plus two in
widget_test.dart — map presence/absence by toggle and trip state, and the lifecycle
transition (paused drops TileLayer but keeps PolylineLayer; resumed brings it back),
driven via the standard flutter/lifecycle platform-message technique rather than a
private binding API. flutter analyze clean; full suite green (224 tests, up from 221).