Files
rippr/docs/v3/V3-04-live-map.md
uhryniuk b781ee36a8 Rename v4 to ROADMAP
v4 was always just the server-dependent programme (group rides, accounts, paid backup) plus V3-17. Renaming it to ROADMAP since it's about to become the home for an incoming list of bugs and features that need triage ahead of the next v3-style ticket batch -- that triage takes priority over the existing server-dependent items, none of which are blocking.
2026-08-23 11:32:29 -05:00

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:

  • 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 (ROADMAP).

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).