1+ import 'dart:async' ;
2+
13import 'package:flutter/material.dart' ;
24import 'package:flutter_map/flutter_map.dart' ;
35import 'package:latlong2/latlong.dart' ;
@@ -36,8 +38,16 @@ class _MapPickerScreenState extends State<MapPickerScreen>
3638 LatLng ? _selectedLocation;
3739 bool _isMapMoving = false ;
3840 final ValueNotifier <bool > _isSearchingLocation = ValueNotifier <bool >(false );
41+ final ValueNotifier <bool > _isLoadingAddress = ValueNotifier <bool >(false );
3942 String _addressText = 'Move map to select location' ;
4043
44+ /// Debounce timer for reverse geocoding to avoid excessive API calls
45+ /// during rapid map movements
46+ Timer ? _geocodeDebounceTimer;
47+
48+ /// Debounce duration for reverse geocoding (400ms)
49+ static const Duration _geocodeDebounceDuration = Duration (milliseconds: 400 );
50+
4151 // Animation controller for pin bounce effect
4252 late final AnimationController _pinAnimationController;
4353 late final Animation <double > _pinBounceAnimation;
@@ -83,22 +93,26 @@ class _MapPickerScreenState extends State<MapPickerScreen>
8393
8494 @override
8595 void dispose () {
96+ _geocodeDebounceTimer? .cancel ();
8697 _pinAnimationController.dispose ();
8798 _isSearchingLocation.dispose ();
99+ _isLoadingAddress.dispose ();
88100 super .dispose ();
89101 }
90102
91103 void _handleMapEvent (MapEvent event) {
92104 if (event is MapEventMoveStart ) {
93105 setState (() => _isMapMoving = true );
94106 _pinAnimationController.forward ();
107+ // Cancel any pending geocode request when movement starts
108+ _geocodeDebounceTimer? .cancel ();
95109 } else if (event is MapEventMoveEnd ) {
96110 setState (() {
97111 _isMapMoving = false ;
98112 _selectedLocation = _mapController.camera.center;
99113 });
100114 _pinAnimationController.reverse ();
101- _updateAddress (_mapController.camera.center);
115+ _debouncedUpdateAddress (_mapController.camera.center);
102116 } else if (event is MapEventMove ) {
103117 // Update position during movement
104118 setState (() {
@@ -107,15 +121,48 @@ class _MapPickerScreenState extends State<MapPickerScreen>
107121 }
108122 }
109123
110- void _updateAddress (LatLng location) {
111- // In a real app, this would call a geocoding service
112- // For now, we display the coordinates in a formatted way
113- setState (() {
114- _addressText =
115- '${location .latitude .toStringAsFixed (6 )}, ${location .longitude .toStringAsFixed (6 )}' ;
124+ /// Debounced reverse geocoding to reduce API calls during rapid map movements.
125+ /// Cancels any pending request and schedules a new one after the debounce period.
126+ void _debouncedUpdateAddress (LatLng location) {
127+ // Cancel any existing pending request
128+ _geocodeDebounceTimer? .cancel ();
129+
130+ // Show loading indicator immediately for better UX
131+ _isLoadingAddress.value = true ;
132+
133+ // Schedule the actual geocoding after the debounce period
134+ _geocodeDebounceTimer = Timer (_geocodeDebounceDuration, () {
135+ _updateAddress (location);
116136 });
117137 }
118138
139+ void _updateAddress (LatLng location) async {
140+ // Perform reverse geocoding to get the human-readable address
141+ _isLoadingAddress.value = true ;
142+
143+ try {
144+ final address = await _geocodingService.getAddressFromLatLng (
145+ location.latitude,
146+ location.longitude,
147+ );
148+ if (mounted) {
149+ setState (() {
150+ _addressText = address.name;
151+ });
152+ }
153+ } catch (e) {
154+ // Fallback to coordinates if geocoding fails
155+ if (mounted) {
156+ setState (() {
157+ _addressText =
158+ '${location .latitude .toStringAsFixed (6 )}, ${location .longitude .toStringAsFixed (6 )}' ;
159+ });
160+ }
161+ } finally {
162+ _isLoadingAddress.value = false ;
163+ }
164+ }
165+
119166 void _confirmLocation () {
120167 if (_selectedLocation != null ) {
121168 Navigator .pop (context, _selectedLocation);
@@ -610,19 +657,56 @@ class _MapPickerScreenState extends State<MapPickerScreen>
610657 const SizedBox (height: 4 ),
611658 AnimatedSwitcher (
612659 duration: const Duration (milliseconds: 200 ),
613- child: Text (
614- _isMapMoving ? 'Moving...' : _addressText,
615- key: ValueKey (
616- _isMapMoving ? 'moving' : _addressText,
617- ),
618- style: theme.textTheme.bodyLarge? .copyWith (
619- fontWeight: FontWeight .w600,
620- color: _isMapMoving
621- ? colorScheme.onSurfaceVariant
622- : colorScheme.onSurface,
623- ),
624- maxLines: 2 ,
625- overflow: TextOverflow .ellipsis,
660+ child: ValueListenableBuilder <bool >(
661+ valueListenable: _isLoadingAddress,
662+ builder: (context, isLoading, child) {
663+ if (_isMapMoving) {
664+ return Text (
665+ 'Moving...' ,
666+ key: const ValueKey ('moving' ),
667+ style: theme.textTheme.bodyLarge? .copyWith (
668+ fontWeight: FontWeight .w600,
669+ color: colorScheme.onSurfaceVariant,
670+ ),
671+ maxLines: 2 ,
672+ overflow: TextOverflow .ellipsis,
673+ );
674+ } else if (isLoading) {
675+ return Row (
676+ key: const ValueKey ('loading' ),
677+ mainAxisSize: MainAxisSize .min,
678+ children: [
679+ SizedBox (
680+ width: 16 ,
681+ height: 16 ,
682+ child: CircularProgressIndicator (
683+ strokeWidth: 2 ,
684+ color: colorScheme.primary,
685+ ),
686+ ),
687+ const SizedBox (width: 8 ),
688+ Text (
689+ 'Getting address...' ,
690+ style: theme.textTheme.bodyLarge? .copyWith (
691+ fontWeight: FontWeight .w600,
692+ color: colorScheme.onSurfaceVariant,
693+ ),
694+ ),
695+ ],
696+ );
697+ } else {
698+ return Text (
699+ _addressText,
700+ key: ValueKey (_addressText),
701+ style: theme.textTheme.bodyLarge? .copyWith (
702+ fontWeight: FontWeight .w600,
703+ color: colorScheme.onSurface,
704+ ),
705+ maxLines: 2 ,
706+ overflow: TextOverflow .ellipsis,
707+ );
708+ }
709+ },
626710 ),
627711 ),
628712 ],
0 commit comments