Fix Google Navigation initialization timing issues

Restructures navigation session initialization to occur after the view is
created, eliminating race conditions. Session initialization now happens in
onViewCreated callback with proper delay before setting destination.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jean-Philippe Brule
2025-11-15 20:49:20 -05:00
parent 46af8f55a2
commit 9cb5b51f6d
11 changed files with 558 additions and 192 deletions
+71 -163
View File
@@ -11,8 +11,6 @@ import '../utils/breakpoints.dart';
import '../components/map_sidebar_layout.dart';
import '../components/dark_mode_map.dart';
import '../components/delivery_list_item.dart';
import '../components/collapsible_routes_sidebar.dart'
show CollapsibleRoutesSidebar;
class DeliveriesPage extends ConsumerStatefulWidget {
final int routeFragmentId;
@@ -29,22 +27,38 @@ class DeliveriesPage extends ConsumerStatefulWidget {
}
class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
late PageController _pageController;
int _currentSegment = 0;
late ScrollController _listScrollController;
Delivery? _selectedDelivery;
int? _lastRouteFragmentId;
@override
void initState() {
super.initState();
_pageController = PageController();
_listScrollController = ScrollController();
}
@override
void dispose() {
_pageController.dispose();
_listScrollController.dispose();
super.dispose();
}
Future<void> _autoScrollToFirstPending(List<Delivery> deliveries) async {
final firstPendingIndex = deliveries.indexWhere((d) => !d.delivered && !d.isSkipped);
if (_listScrollController.hasClients && firstPendingIndex != -1) {
await Future.delayed(const Duration(milliseconds: 200));
// Scroll to position first pending delivery at top of list
// Each item is approximately 70 pixels tall
final scrollOffset = firstPendingIndex * 70.0;
_listScrollController.animateTo(
scrollOffset.clamp(0, _listScrollController.position.maxScrollExtent),
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
}
}
@override
Widget build(BuildContext context) {
final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId));
@@ -58,6 +72,14 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
),
body: deliveriesData.when(
data: (deliveries) {
// Auto-scroll to first pending delivery when page loads or route changes
if (_lastRouteFragmentId != widget.routeFragmentId) {
_lastRouteFragmentId = widget.routeFragmentId;
WidgetsBinding.instance.addPostFrameCallback((_) {
_autoScrollToFirstPending(deliveries);
});
}
final todoDeliveries = deliveries
.where((d) => !d.delivered && !d.isSkipped)
.toList();
@@ -76,27 +98,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
currentRoute = routes.isNotEmpty ? routes.first : null;
}
return Row(
children: [
if (context.isDesktop && routes.isNotEmpty)
CollapsibleRoutesSidebar(
routes: routes,
selectedRoute: currentRoute,
onRouteSelected: (route) {
if (route.id != widget.routeFragmentId) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => DeliveriesPage(
routeFragmentId: route.id,
routeName: route.name,
),
),
);
}
},
),
Expanded(
child: MapSidebarLayout(
return MapSidebarLayout(
mapWidget: DarkModeMapComponent(
deliveries: deliveries,
selectedDelivery: _selectedDelivery,
@@ -105,75 +107,25 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
_selectedDelivery = delivery;
});
},
onAction: (action) => _selectedDelivery != null
? _handleDeliveryAction(context, _selectedDelivery!, action, token)
: null,
),
sidebarWidget: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: SegmentedButton<int>(
segments: const [
ButtonSegment(
value: 0,
label: Text('To Do'),
),
ButtonSegment(
value: 1,
label: Text('Delivered'),
),
],
selected: <int>{_currentSegment},
onSelectionChanged: (Set<int> newSelection) {
setState(() {
_currentSegment = newSelection.first;
_pageController.animateToPage(
_currentSegment,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
});
},
),
),
Expanded(
child: PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentSegment = index;
});
},
children: [
DeliveryListView(
deliveries: todoDeliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onAction: (delivery, action) =>
_handleDeliveryAction(context, delivery, action, token),
),
DeliveryListView(
deliveries: completedDeliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onAction: (delivery, action) =>
_handleDeliveryAction(context, delivery, action, token),
),
],
),
),
],
sidebarWidget: UnifiedDeliveryListView(
deliveries: deliveries,
selectedDelivery: _selectedDelivery,
scrollController: _listScrollController,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onItemAction: (delivery, action) {
_handleDeliveryAction(context, delivery, action, token);
_autoScrollToFirstPending(deliveries);
},
),
),
),
],
);
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
@@ -187,70 +139,23 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
_selectedDelivery = delivery;
});
},
onAction: (action) => _selectedDelivery != null
? _handleDeliveryAction(context, _selectedDelivery!, action, token)
: null,
),
sidebarWidget: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: SegmentedButton<int>(
segments: const [
ButtonSegment(
value: 0,
label: Text('To Do'),
),
ButtonSegment(
value: 1,
label: Text('Delivered'),
),
],
selected: <int>{_currentSegment},
onSelectionChanged: (Set<int> newSelection) {
setState(() {
_currentSegment = newSelection.first;
_pageController.animateToPage(
_currentSegment,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
});
},
),
),
Expanded(
child: PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentSegment = index;
});
},
children: [
DeliveryListView(
deliveries: todoDeliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onAction: (delivery, action) =>
_handleDeliveryAction(context, delivery, action, token),
),
DeliveryListView(
deliveries: completedDeliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onAction: (delivery, action) =>
_handleDeliveryAction(context, delivery, action, token),
),
],
),
),
],
sidebarWidget: UnifiedDeliveryListView(
deliveries: deliveries,
selectedDelivery: _selectedDelivery,
scrollController: _listScrollController,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onItemAction: (delivery, action) {
_handleDeliveryAction(context, delivery, action, token);
_autoScrollToFirstPending(deliveries);
},
),
),
);
@@ -291,7 +196,6 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
endpoint: 'completeDelivery',
command: CompleteDeliveryCommand(
deliveryId: delivery.id,
deliveredAt: DateTime.now().toIso8601String(),
),
);
result.when(
@@ -351,18 +255,20 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
}
}
class DeliveryListView extends StatelessWidget {
class UnifiedDeliveryListView extends StatelessWidget {
final List<Delivery> deliveries;
final Delivery? selectedDelivery;
final ScrollController scrollController;
final ValueChanged<Delivery> onDeliverySelected;
final Function(Delivery, String) onAction;
final Function(Delivery, String) onItemAction;
const DeliveryListView({
const UnifiedDeliveryListView({
super.key,
required this.deliveries,
this.selectedDelivery,
required this.scrollController,
required this.onDeliverySelected,
required this.onAction,
required this.onItemAction,
});
@override
@@ -378,6 +284,7 @@ class DeliveryListView extends StatelessWidget {
// Trigger refresh via provider
},
child: ListView.builder(
controller: scrollController,
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: deliveries.length,
itemBuilder: (context, index) {
@@ -386,7 +293,8 @@ class DeliveryListView extends StatelessWidget {
delivery: delivery,
isSelected: selectedDelivery?.id == delivery.id,
onTap: () => onDeliverySelected(delivery),
onCall: () => onAction(delivery, 'call'),
onCall: () => onItemAction(delivery, 'call'),
onAction: (action) => onItemAction(delivery, action),
animationIndex: index,
);
},
+8 -17
View File
@@ -52,11 +52,8 @@ class _NavigationPageState extends ConsumerState<NavigationPage> {
setState(() {
_hasLocationPermission = true;
_isNavigationInitialized = true;
});
if (mounted) {
_initializeNavigationSession();
}
} catch (e) {
if (mounted) {
_showErrorDialog('Initialization error: ${e.toString()}');
@@ -67,19 +64,10 @@ class _NavigationPageState extends ConsumerState<NavigationPage> {
Future<void> _initializeNavigationSession() async {
try {
await GoogleMapsNavigationViewController.initializeNavigationSession();
if (mounted) {
setState(() {
_isNavigationInitialized = true;
});
// Set destination after session is initialized
await _setDestination();
}
} catch (e) {
if (mounted) {
_showErrorDialog('Failed to initialize navigation: ${e.toString()}');
}
debugPrint('Navigation session initialization error: $e');
// Don't show error dialog, just log it
// The session might already be initialized
}
}
@@ -263,8 +251,11 @@ class _NavigationPageState extends ConsumerState<NavigationPage> {
),
body: _hasLocationPermission && _isNavigationInitialized
? GoogleMapsNavigationView(
onViewCreated: (controller) {
onViewCreated: (controller) async {
_navigationViewController = controller;
await _initializeNavigationSession();
await Future.delayed(const Duration(milliseconds: 500));
await _setDestination();
},
initialCameraPosition: CameraPosition(
target: LatLng(