UI-01: persistent 4-tab shell with a shared background map
Replaces the push/pop stack rooted at Record with a StatefulShellRoute (Map/Rides/Plan/Settings), each tab keeping its own navigator so Trip Detail and the route planner push within their own branch. AppShell/ShellScaffold host one shared RideMap instance behind every tab -- full opacity and interactive on Map, dimmed and non-interactive elsewhere -- so the camera position and live path survive a tab switch instead of being refetched. Every tab's AppBar/header is removed per the redesign's no-chrome mandate. Fixes a bug this surfaced: RideMap rendered a structurally different tree for empty vs. non-empty points, which crashed once the map became a long-lived shell background instead of a fresh per-screen widget. Unified the background-map tree shape so it no longer remounts mid-recording. Verified on Medium_Phone_API_35: all four tabs, live recording surviving tab switches with the camera preserved, and the real HOME-key lifecycle tile-drop from V3-04 still firing correctly in the shell context. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
This commit is contained in:
@@ -19,7 +19,7 @@ v3 held to.
|
||||
|
||||
| # | Ticket | Size | Depends on | Status |
|
||||
|---|---|---|---|---|
|
||||
| [UI-01](UI-01-tab-shell-background-map.md) | Persistent tab shell with an always-visible background map | L | — | Not started |
|
||||
| [UI-01](UI-01-tab-shell-background-map.md) | Persistent tab shell with an always-visible background map | L | — | Done |
|
||||
| [UI-02](UI-02-offline-skeleton-map.md) | Offline / no-connection skeleton map | S | UI-01 | Not started |
|
||||
| [UI-03](UI-03-glass-component-kit.md) | Shared floating-glass component kit | M | UI-08 | Not started |
|
||||
| [UI-04](UI-04-customizable-hud-widgets.md) | Customizable HUD telemetry widgets (drag, resize, visibility toggle) | L | UI-03 | Not started |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# UI-01 — Persistent tab shell with an always-visible background map
|
||||
|
||||
**Depends on** nothing · **Size** L · **Status** Not started
|
||||
**Depends on** nothing · **Size** L · **Status** Done
|
||||
|
||||
## Goal
|
||||
Replace the current push/pop stack rooted at Record with a persistent 4-tab shell (Map,
|
||||
@@ -98,3 +98,85 @@ from a persistent bar.
|
||||
The skeleton/offline map state (UI-02). Any visual redesign of what's inside a tab
|
||||
(UI-04/05/06) — this ticket only has to make the shell and shared background map exist,
|
||||
correctly, with today's screen content still working inside it.
|
||||
|
||||
## Outcome
|
||||
|
||||
Shipped as designed: `lib/src/ui/router.dart` now builds a single `StatefulShellRoute
|
||||
.indexedStack` with four branches (Map `/`, Rides `/rides`, Plan `/plan`, Settings
|
||||
`/settings`), each with Trip Detail / the route planner nested as a child `GoRoute`
|
||||
inside its own branch so pushing/popping there never disturbs the other tabs or the
|
||||
active tab index. `lib/src/ui/app_shell.dart` is new: `AppShell` is a thin go_router
|
||||
adapter around `ShellScaffold`, a plain-parameter (`currentIndex`/`onDestinationSelected`
|
||||
/`child`) `ConsumerWidget` that owns the one shared `RideMap` instance, the dimming scrim,
|
||||
and the bottom `NavigationBar`. Splitting the two was necessary for testability —
|
||||
go_router only ever constructs a real `StatefulNavigationShell` itself, so widget tests
|
||||
drive `ShellScaffold` directly with a plain `int` instead.
|
||||
|
||||
Every top-level tab screen (`RecordScreen`, `TripsScreen`, `RoutesListScreen`,
|
||||
`SettingsScreen`) had its `AppBar`/back-button header removed and its `Scaffold`
|
||||
background set to transparent so the shared map shows through. `RecordScreen` also lost
|
||||
its per-screen `onOpenTrips`/`onOpenSettings`/`onOpenRoutes` navigation callbacks and its
|
||||
embedded `_LiveMap` card entirely — navigation is the shell's nav bar now, and the map is
|
||||
the shell's permanent background rather than something each screen constructs for
|
||||
itself. One deliberate temporary trade: the mounted-mode toggle's only remaining button
|
||||
was on `RecordScreen`'s removed header; its sole access point until UI-05 restyles the
|
||||
Record screen is now Settings' existing `mounted-mode-switch`.
|
||||
|
||||
**Bug found and fixed during this ticket, not anticipated by the plan:** `RideMap`
|
||||
returned a structurally different widget tree for empty vs. non-empty points in its
|
||||
`showEmptyLabel: false` (background) mode — a bare `FlutterMap` vs. `ClipRRect >
|
||||
FlutterMap`. Once the map became a long-lived shell background instead of a
|
||||
freshly-mounted per-screen widget, this became visible: the moment a live ride's first
|
||||
point arrived, Flutter unmounted/remounted the differing subtree mid-flight, and
|
||||
`RideMap`'s own `didUpdateWidget`-scheduled `_controller.camera` post-frame callback fired
|
||||
against the stale element, throwing "Looking up a deactivated widget's ancestor is
|
||||
unsafe." Fixed by unifying the `showEmptyLabel: false` and non-empty branches into one
|
||||
tree shape (`ClipRRect > FlutterMap` always, `MapOptions`/children varying only by
|
||||
whether points exist); the `showEmptyLabel: true` path (a finished-ride card, which never
|
||||
transitions live) was left as its original text-only branch since it carries no such
|
||||
risk and an existing test (`ride_map_test.dart`) already asserted no `FlutterMap` renders
|
||||
there.
|
||||
|
||||
**Tests:** `flutter analyze` clean (pre-existing `deprecated_member_use` infos only,
|
||||
unrelated to this ticket). `flutter test` green at 315 tests (was 316 before this ticket;
|
||||
net -1 after removing two now-meaningless per-screen entry-point tests and adding one
|
||||
shell-nav-bar test — see below). Two tests referencing the removed
|
||||
`onOpenSettings`/`onOpenRoutes` callbacks were deleted outright (the concept no longer
|
||||
exists); a new "shell nav bar" group in `test/widget_test.dart` asserts all four
|
||||
`NavigationDestination`s are present and that tapping each calls back with the right
|
||||
index. The two V3-04 background-map tests were rewritten to pump `ShellScaffold` and
|
||||
assert on `shell-background-map`/`TileLayer` instead of the old per-screen `live-map` key
|
||||
— both pass, including the lifecycle-driven tile-drop/restore test that exposed the bug
|
||||
above.
|
||||
|
||||
**Android emulator verification** (`Medium_Phone_API_35`, API 35, `flutter run --debug`):
|
||||
confirmed visually for all four tabs —
|
||||
- Map tab: full-opacity, interactive map, no header, `START RECORDING` visible.
|
||||
- Rides/Plan/Settings tabs: same map dimmed via the `0xB3000000` scrim, non-interactive,
|
||||
no header, each tab's real content on top.
|
||||
- Started a live recording (with `adb emu geo fix` supplying a location) and switched
|
||||
tabs repeatedly: the point count and elapsed timer kept advancing across tab switches
|
||||
(11 → 31 → 41 points, uninterrupted) and the camera position did not reset — confirming
|
||||
the shared instance survives tab switches rather than being torn down and recreated.
|
||||
- Backgrounding via the real HOME key (not just the synthetic lifecycle message the
|
||||
widget test uses) dropped the map to a flat, untiled gray — the same lifecycle-based
|
||||
tile-teardown from V3-04 firing correctly in the shell context, not just in tests.
|
||||
- One visual red herring investigated and ruled out: a flat gray rectangle appeared
|
||||
transiently in the dimmed background on the Rides/Plan tabs. Ruled out as an app bug by
|
||||
reproducing it identically on two different tab screens at the same absolute screen
|
||||
position, and by confirming a fresh app launch never shows it — it is flutter_map's
|
||||
normal "tile chunk not yet loaded" placeholder at the low zoom level the background
|
||||
map starts at before any location fix narrows it, not a shell defect.
|
||||
- Known pre-existing rough edge, not a regression: the background map's follow-mode
|
||||
moves the camera center to the latest point but does not adjust zoom on the first fix,
|
||||
so a freshly-started ride's background map can look zoomed far out until the user
|
||||
manually zooms. This behavior predates UI-01 (the same `_controller.move(point, current
|
||||
Zoom)` call existed in the old standalone `_LiveMap`) and is out of scope here.
|
||||
|
||||
Deferred, not a gap: a widget test asserting Trip Detail/route-planner push-and-pop stays
|
||||
within its own tab (rather than affecting `currentIndex`) was not written — the
|
||||
`StatefulShellBranch`-per-tab nesting in `router.dart` structurally guarantees this via
|
||||
go_router's own navigator-per-branch semantics, and it was verified manually on-device
|
||||
that `router.dart`'s nested `GoRoute`s compile and resolve correctly. A future ticket
|
||||
touching Rides/Plan navigation should add explicit coverage rather than relying on this
|
||||
note.
|
||||
|
||||
Reference in New Issue
Block a user