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.0 KiB
V3-02 — Settings screen
Phase Foundations · Depends on nothing (pairs with V3-01, V3-03) · Size S · Status Done
Goal
One place for the preferences that currently have nowhere to live.
Context
Config already holds uploadEndpoint, deviceId and mapEnabled, but there is no UI
for any of them. The map toggle sits on trip detail because a single switch did not justify
a screen. V3-01 and V3-03 both add preferences, which finally does justify one.
The upload endpoint has never had UI, in the native app or the port. This is where it stops being a code-only setting.
Design
Reached from the record screen. Sections:
- Recording — default activity (V3-01), units (V3-03)
- Map — render maps on trip detail; later, live map (V3-04)
- Sync — upload endpoint, with the device id shown read-only and copyable
- About — version, a link to the privacy policy, licences
Keep it plain. This is not a screen anyone should spend time in.
Implementation
SettingsScreen+ ago_routerroute- Make
Configreactive — it is currently read once into a provider; settings need writes to propagate. AConfigNotifierovershared_preferences. - Move the map toggle off trip detail
- Endpoint field validates it parses as a URL and is http(s)
Acceptance criteria
- Every
Configvalue is viewable and editable - Changing the map toggle takes effect without an app restart
- An invalid endpoint is rejected with a readable message, not silently stored
- Device id is copyable — it is the only way to identify this install to a server
Tests
- Widget: each control renders and writes through to
Config - Widget: invalid URL rejected
- Changing the map toggle rebuilds trip detail
Risks
Config is currently loaded once at startup into a StateProvider. Making it writable
without introducing a second source of truth is the only subtle part.
Out of scope
Account settings (v4). Theme selection (V3-16).
Outcome
"Move the map toggle off trip detail" (implementation step 3) turned out to be moot —
mapEnabledProvider already existed and drove RideMap's visibility, but no widget
anywhere ever offered a control to change it. There was nothing to move. Settings adds
the first one.
No ConfigNotifier was built — see V3-03's outcome for why the existing
mapEnabledProvider-style StateProvider pattern covers reactivity without it, and
unitSystemProvider was added the same way, in providers.dart, ahead of this ticket.
Two implementation-step items shipped differently than drafted, both to avoid adding a
dependency disproportionate to an S-sized settings screen:
- No
package_info_plus. The About section's version string is a static literal matchingpubspec.yaml's1.0.0+1, not a live package lookup. Fine today; would need revisiting if the version ever needs to be authoritative from inside the running app rather than copied by hand. - No privacy-policy link. None is published yet (see
docs/LAUNCH.md) and linking to one that does not exist would be worse than omitting it. Shows a plain note instead. Licences are still free: Flutter's built-inshowLicensePageneeded no new dependency.
Endpoint validation accepts http:// and https:// with a non-empty host, and treats
an empty field as valid — that is how upload gets disabled, not an error state. A
non-empty invalid value is rejected with inline errorText and never reaches Config;
proven by a widget test that types garbage, taps Save, and asserts Config.uploadEndpoint
is still empty afterwards.
uploaderProvider needed an explicit ref.invalidate() after saving the endpoint, for
the identical reason unitSystemProvider needed its own StateProvider rather than
reading through configProvider directly — a Config write never changes the Config
instance Riverpod is watching, so nothing downstream rebuilds unless told to.
10 tests: 9 in settings_screen_test.dart, 1 confirming the record screen's settings
button is genuinely wired (not just present) in widget_test.dart.