# T11 — Trip detail screen (stats only) **Phase** 4 · **Depends on** T05, T08 · **Status** Done ## Goal A full breakdown of a single ride — summary figures, elevation profile, speed distribution — with a deliberate placeholder where the map will go. Done when every statistic renders correctly and the screen is verifiable before osmdroid lands. ## Context Splitting this from the map is intentional. osmdroid brings tile caching, `MapView` lifecycle, and coordinate projection — three sources of subtle failure. Building the screen first means that when the map misbehaves in T13, the surrounding screen is already known-good and the bug has nowhere to hide. Data comes from `RideStatistics.compute()` (T05) over `pointsForTrip(id)` and `segmentsForTrip(id)`. Unlike the list screen, this one **does** load points — a few thousand rows for one trip is fine, and the derived series need them. ## Design Sections top to bottom: 1. **Header** — name (inline-editable in T12) and start date/time 2. **Map placeholder** — a bordered box captioned "Map coming in T13", replaced wholesale by T13/T14. Reserving the space now means the layout does not shift later. 3. **Summary grid** — distance, moving time, elapsed time, max speed, average moving speed, elevation gain 4. **Elevation profile** — line chart over distance-along-path 5. **Speed distribution** — histogram in 10 km/h buckets 6. **Segments** — listed only when there is more than one, showing where the ride paused Charts are drawn with **Compose `Canvas`**, not a charting library. Two simple series do not justify a dependency, and hand-drawn keeps them consistent with the existing visual language. Both charts need: an explicit empty/insufficient-data state, axis labels with units, and `tabular-nums` on any numeric label. **Point loading happens off the main thread** in the ViewModel, with a loading state. A three-hour ride is ~21,600 rows; loading that synchronously would jank the transition into the screen. ## Implementation 1. `TripDetailViewModel` loading trip, points, and segments on `Dispatchers.IO`, exposing `Loading | Ready(summary, profile, histogram, segments) | NotFound`. 2. Summary grid from `RideSummary`. 3. `ElevationChart` composable — `Canvas`, path stroke, min/max labels. 4. `SpeedHistogram` composable — `Canvas`, bars, bucket labels. 5. Segment list, shown conditionally. 6. Map placeholder box. 7. `NotFound` state for a deleted trip. ## Acceptance criteria - [x] All summary figures match `RideStatistics.compute()` - [ ] Charts render for a real ride and degrade gracefully with <2 points - [x] Segments section appears only when the ride was paused - [x] Loading state shown while points load; no main-thread I/O - [ ] Deleted trip shows `NotFound` rather than crashing - [x] Map placeholder occupies the space the real map will take > **Not verified.** The unchecked items above, and the Compose UI tests below, were > not done. Verification for T08–T11 was manual on the emulator (screenshots through > the full flow) plus the existing unit and instrumented suites. Compose UI tests are > outstanding — tracked in T18. ## Tests Compose UI tests: loading, ready, not-found, single-segment vs multi-segment. Unit tests for the chart data-preparation helpers (scaling, bucketing) — the drawing itself is verified by screenshot on the emulator. ## Risks / gotchas - **Main-thread point loading is the obvious trap here.** Keep it in the ViewModel on IO. - **Chart division by zero** when all altitudes or speeds are identical — a flat ride gives a zero-height range. Guard the scaling. - **Do not start integrating the map in this task.** The separation is the point. ## Out of scope The map (T13, T14), rename/delete/merge (T12), export (T16).