Google Maps Marker & Bubble Misaligned on Android

# 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