Files
samplez/rippr-flutter-src/docs/v3/V3-02-settings-screen.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.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

  1. SettingsScreen + a go_router route
  2. Make Config reactive — it is currently read once into a provider; settings need writes to propagate. A ConfigNotifier over shared_preferences.
  3. Move the map toggle off trip detail
  4. Endpoint field validates it parses as a URL and is http(s)

Acceptance criteria

  • Every Config value 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 matching pubspec.yaml's 1.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-in showLicensePage needed 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.