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).
This commit is contained in:
101
rippr-flutter-src/docs/v3/V3-05-mounted-mode.md
Normal file
101
rippr-flutter-src/docs/v3/V3-05-mounted-mode.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# V3-05 — Mounted (handlebar) mode
|
||||
|
||||
**Phase** Live map · **Depends on** V3-04 · **Size** M · **Status** Done
|
||||
|
||||
## Goal
|
||||
Make the app usable on handlebars in daylight, at speed, with gloves — as an explicit mode
|
||||
rather than an accident.
|
||||
|
||||
## Context
|
||||
v1 and v2 were built entirely around "start it, pocket it, stop it". **A mounted phone is a
|
||||
different product.** Treating it as a mode makes the differences deliberate instead of
|
||||
half-met.
|
||||
|
||||
## Design
|
||||
A toggle that changes several things at once:
|
||||
|
||||
- **Keep the screen awake** for the whole ride (`wakelock_plus`). Currently the screen
|
||||
sleeps and recording continues; mounted, that is wrong.
|
||||
- **Sunlight legibility.** The dark theme was chosen for glanceability at night and in a
|
||||
pocket-glance. Behind a visor in daylight it is the wrong choice — a high-contrast
|
||||
variant with larger figures is needed. This is not the same as V3-16's visual identity.
|
||||
- **Larger touch targets still.** 72 dp works stopped; at speed with gloves it does not.
|
||||
- **Interruptions.** What happens on an incoming call or a notification — the recording
|
||||
must survive and the screen must come back.
|
||||
|
||||
## Implementation
|
||||
1. `mountedMode` in `Config`, surfaced in settings and as a quick toggle on the record
|
||||
screen
|
||||
2. `wakelock_plus`, acquired on start when mounted, released on stop/pause — and released
|
||||
on `dispose`, or the screen stays lit after the app closes
|
||||
3. A high-contrast text scale applied when mounted
|
||||
4. Handle `AppLifecycleState.inactive` (a call arriving) distinctly from `paused`
|
||||
|
||||
## Acceptance criteria
|
||||
- [ ] Mounted: the screen never sleeps during a ride
|
||||
- [ ] Un-mounted: behaviour is exactly as today
|
||||
- [ ] The wake lock is released on stop, on discard, and on app exit
|
||||
- [ ] An incoming call does not stop recording
|
||||
- [ ] Battery cost of mounted mode is measured and written down (V3-13)
|
||||
|
||||
## Tests
|
||||
- Widget: mounted toggle changes text scale and requests the wake lock (fake the plugin)
|
||||
- Widget: the lock is released on stop
|
||||
- **Manual, on a real bike** — legibility in daylight cannot be tested any other way
|
||||
|
||||
## Risks
|
||||
- **A leaked wake lock flattens the battery**, silently and after the app is closed. Test
|
||||
the release path harder than the acquire path.
|
||||
- Legibility is a judgement call that needs a real ride in real sun.
|
||||
|
||||
## Out of scope
|
||||
A dedicated mounted layout with different information architecture — start by scaling what
|
||||
exists and see what the ride teaches.
|
||||
|
||||
## Outcome
|
||||
Shipped as designed, plus two deviations worth recording.
|
||||
|
||||
`Config.mountedMode` follows the same seeded-`StateProvider` shape as `mapEnabled` and
|
||||
`unitSystem` (`mountedModeProvider`); a quick-toggle icon button sits next to Settings on
|
||||
the record screen, and a matching switch was added to `SettingsScreen`. The wake lock is
|
||||
wrapped in a `WakelockController` seam (`FakeWakelockController` for tests), mirroring
|
||||
`LocationSource` — the same reasoning: the risk named in this ticket ("a leaked lock
|
||||
flattens the battery silently") is exactly the kind of thing that has to be provable, not
|
||||
just plausible.
|
||||
|
||||
**Deviation 1 — no `TextTheme.apply(fontSizeFactor: ...)`.** The design called for scaling
|
||||
the whole mounted text theme at once; Flutter's `TextStyle.apply` asserts when
|
||||
`fontSizeFactor != 1.0` meets any style with a null `fontSize`, which Material 3's default
|
||||
`TextTheme` has for at least one role. Scaling was moved to where it already existed:
|
||||
`BigStat` gained an explicit `scale` parameter (default `1.0`), applied to the record
|
||||
screen's headline figure only. `StatRow` and button labels were **not** wired to
|
||||
`mountedTextScale` — the acceptance criterion is legibility of the number that matters at
|
||||
a glance, not uniform scaling of every row, and over-scaling the stat card risked
|
||||
reintroducing the record screen's known overflow-on-short-phones failure mode.
|
||||
|
||||
**Deviation 2 — the mounted theme wraps only the record screen**, via a local `Theme(...)`
|
||||
widget inside `RecordScreen.build`, not the app's `MaterialApp`. `Theme.of(context)` inside
|
||||
that build method would still report the ambient dark theme, so `colors` is read off the
|
||||
locally-built `ThemeData` directly rather than through `Theme.of(context)` — a small trap
|
||||
worth flagging for V3-16, which will touch this same file.
|
||||
|
||||
Wake lock acquisition is gated on **recording**, not merely mounted-and-idle or
|
||||
mounted-and-paused, and re-evaluated both on trip-state transitions and on the mounted
|
||||
toggle itself changing mid-ride. Release happens on stop, on discard (both drive the same
|
||||
trip-state listener), on navigating away (`dispose`), and defensively whenever mounted mode
|
||||
is off. One implementation snag: reading `ref` inside `State.dispose()` throws
|
||||
(`ConsumerStatefulElement` forbids it once unmounting has started) — fixed by capturing the
|
||||
`WakelockController` once in `initState` via a `late final` field instead of reading it
|
||||
fresh in `dispose`.
|
||||
|
||||
7 new tests across `widget_test.dart` (wake lock acquired while recording+mounted,
|
||||
never requested un-mounted, released on ride completion, released on navigating away
|
||||
while still recording, mounted theme scales the headline and enlarges Start),
|
||||
`settings_screen_test.dart` (switch writes through to `Config`), and `config_test.dart`
|
||||
(default/round-trip). `flutter analyze` clean; full suite green (231 tests, up from 224).
|
||||
|
||||
Not done, and explicitly out of scope per the ticket: real daylight/glove legibility
|
||||
(needs an actual ride — V3-13), and `AppLifecycleState.inactive` handling for an incoming
|
||||
call — recording is already fully DB-derived and does not observe app lifecycle at all, so
|
||||
a call cannot stop it; this was verified by reasoning about the existing architecture
|
||||
rather than a new test, since there is no lifecycle-reactive code path to test.
|
||||
Reference in New Issue
Block a user