T15-T18, T20, T23: app shell, three screens, and the first UI tests
Theme, go_router shell, record screen, trips list with selection, trip detail with stats and charts, rename/delete/merge. 15 widget tests -- the first this project has ever had, closing the gap docs/v3/BACKLOG.md names as v2's largest. The theme sets bodyColor and displayColor explicitly rather than relying on a wrapping widget, so the black-on-black regression cannot recur by someone removing a container, and a widget test now asserts the headline names a colour distinct from the ground. Those tests immediately found a real layout bug: with a ride active the record screen grows to six stat rows and overflows a short screen. Compose clipped this silently, so it may be latent in the native app; Flutter reports it. Fixed by making the screen scrollable while still centring when there is room. Two harness lessons. pumpAndSettle never settles against a repeating timer, so the elapsed clock now runs only while a ride is active -- better behaviour anyway, since an idle screen has no clock to advance. And flutter_test asserts no Timer is pending after disposal, which Drift's stream-query keep-alive cache trips; tests now tear the tree down and pump past that window. 160 tests passing, analyze clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
70
lib/src/ui/router.dart
Normal file
70
lib/src/ui/router.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
/// Ported from `com.rippr.ui.RipprNavHost`.
|
||||
///
|
||||
/// Three destinations, same shape as the Compose original. `go_router` replaces
|
||||
/// `navigation-compose`; the path parameter is parsed explicitly, mirroring the Kotlin
|
||||
/// comment about ids arriving as the wrong type when the argument type is left implicit.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'detail/trip_detail_screen.dart';
|
||||
import 'record/record_screen.dart';
|
||||
import 'trips/trips_screen.dart';
|
||||
|
||||
abstract final class Routes {
|
||||
static const record = '/';
|
||||
static const trips = '/trips';
|
||||
static const tripDetail = '/trip/:tripId';
|
||||
|
||||
static String detailFor(int tripId) => '/trip/$tripId';
|
||||
}
|
||||
|
||||
GoRouter buildRouter() => GoRouter(
|
||||
initialLocation: Routes.record,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: Routes.record,
|
||||
builder: (context, state) => RecordScreen(
|
||||
onOpenTrips: () => context.push(Routes.trips),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.trips,
|
||||
builder: (context, state) => TripsScreen(
|
||||
onOpenTrip: (id) => context.push(Routes.detailFor(id)),
|
||||
onBack: () => context.pop(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.tripDetail,
|
||||
builder: (context, state) {
|
||||
// Parsed explicitly. A malformed or missing id must land on a real screen
|
||||
// saying so, never on a blank one or a crash.
|
||||
final id = int.tryParse(state.pathParameters['tripId'] ?? '');
|
||||
if (id == null) {
|
||||
return const _NotFound();
|
||||
}
|
||||
return TripDetailScreen(tripId: id, onBack: () => context.pop());
|
||||
},
|
||||
),
|
||||
],
|
||||
errorBuilder: (context, state) => const _NotFound(),
|
||||
);
|
||||
|
||||
class _NotFound extends StatelessWidget {
|
||||
const _NotFound();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.canPop()
|
||||
? context.pop()
|
||||
: context.go(Routes.record),
|
||||
),
|
||||
),
|
||||
body: const Center(child: Text('That ride could not be found.')),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user