# 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: - **`RecordingEngine` must 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 1. `watchPointsForTrip(tripId)` in `AppDatabase` 2. Hoist the map above the stats card on the record screen, behind the toggle 3. `WidgetsBindingObserver` — on `AppLifecycleState.paused`, stop tile fetching; resume on `resumed`. This is the load-bearing part. 4. Auto-follow with a pan override 5. 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 - [ ] `RecordingEngine` still 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).