# Google Maps Marker & Bubble Misaligned on Android: A Production Postmortem
## Summary
- Custom marker bubbles appeared misaligned on Android while working correctly on iOS
- Bubble positioning was being calculated synchronously after marker creation
- Android required additional time for marker layout completion before reliable coordinate fetching
- Debugging revealed `getScreenCoordinate()` returned premature values during map transitions
## Root Cause
- The Android implementation of `google_maps_flutter` requires additional render passes to finalize marker positions
- `getScreenCoordinate()` was returning position values before marker rendering completed
- The 30ms delay solution was insufficient due to device performance variability
- Missing synchronization mechanism between marker rendering completion and coordinate requests
## Why This Happens in Real Systems
- Asynchronous rendering pipelines differ across platforms (Android vs iOS)
- Resource contention during map tile loading delays UI layout calculations
- Flutter plugin bridges don't expose low-level rendering milestones
- Real-world variables:
- Varying device GPU/CPU capabilities
- Map tile loading delays
- Concurrent gesture interactions
- Animation mid-execution when coordinates are requested
## Real-World Impact
- User experience damage: Bubble displaced downward from marker
- Content misassociation: Critical UI elements pointed to wrong locations
- Platform inconsistency: Core feature worked on iOS but failed on Android
- User frustration: Increased support tickets mentioning "broken pin placement"
- Conversion impact: Discoverability failure for location-based features
## Example Code
Original problematic implementation for Android:
```dart
if (defaultTargetPlatform == TargetPlatform.android) {
await Future.delayed(const Duration(milliseconds: 30));
sc = await ctl.getScreenCoordinate(anchor);
}
Improved solution:
void _scheduleBubbleUpdate() {
// Cancel existing timers
_positionTimer?.cancel();
// Progressive backoff: 30ms → 50ms → 100ms → 150ms
final delay = _updateAttempt < 3
? Duration(milliseconds: 30 + _updateAttempt * 20)
: const Duration(milliseconds: 150);
_positionTimer = Timer(delay, () async {
final coordinates = await _getValidatedCoordinates();
if (coordinates != null) updateBubbleUI(coordinates);
else _scheduleBubbleUpdate(); // Retry recursively
_updateAttempt++;
});
}
Future<Offset?> _getValidatedCoordinates() async {
final ctl = await _mapController.future;
final sc = await ctl.getScreenCoordinate(_currentBubbleAnchor!);
// Validation checks
if (sc.x < 0 || sc.y < 0) return null;
if (MediaQuery.of(context).size.height < sc.y) return null;
return Offset(sc.x.toDouble(), sc.y.toDouble());
}
How Senior Engineers Fix It
- Implement recursive validation with exponential backoff
- Add rendering lifecycle listeners:
WidgetsBinding