Files
samplez/rippr-flutter-src/docs/v3/V3-04-live-map.md
uhryniuk 4c634354bd Refresh the Flutter snapshot: v3 tickets V3-04 through V3-16, plus a fresh installable APK
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).
2026-08-19 13:36:04 -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 (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).