Integrating Maps in Flutter

FlutterDartMapsMobile
Share on LinkedIn

The delivery tracker showed a pin three blocks away from the actual driver because someone passed raw latitude strings into double.parse without validating sign, and the map froze whenever 400 markers loaded at once because each one was a full-resolution bitmap. Maps in Flutter are straightforward until they aren't — API keys fail silently on release builds, camera animations fight gesture handlers, and marker density turns a smooth scroll into a slideshow. This guide covers the integration patterns that survive production traffic, not just the getting-started tutorial.

Choose your map stack

Flutter has two mature options:

Need google_maps_flutter Mapbox Maps Flutter
Familiar Google styling Yes Custom styles
Places / Geocoding tie-in Native Separate APIs
Offline tiles Limited Strong
Custom vector styling Limited Excellent
Web support Via plugin Via plugin

For most consumer apps — ride-hailing, store locators, field service — google_maps_flutter is the path of least resistance. For logistics dashboards, custom-branded maps, or offline-first field apps, Mapbox earns its license fee.

Platform setup that actually works

Google Maps requires keys before the first frame:

// main.dart — Mapbox token early
void main() {
  WidgetsFlutterBinding.ensureInitialized();
  // Mapbox: MapboxOptions.setAccessToken(String.fromEnvironment('MAPBOX_TOKEN'));
  runApp(const App());
}

Android (android/app/src/main/AndroidManifest.xml):

<meta-data
    android:name="com.google.android.geo.API_KEY"
    android:value="${MAPS_API_KEY}"/>

Pass the key via --dart-define=MAPS_API_KEY=... in CI and inject into Gradle. Never commit keys. On iOS, add GMSApiKey to AppDelegate.swift and include the key in ios/Runner/Info.plist if your setup requires it.

Release builds fail more often than debug because SHA-1 restrictions differ. Register both debug and release signing certificates in Google Cloud Console, or use separate restricted keys per build type.

Basic map widget and camera control

class DeliveryMap extends StatefulWidget {
  const DeliveryMap({super.key, required this.driverLocation});
  final LatLng driverLocation;

  @override
  State<DeliveryMap> createState() => _DeliveryMapState();
}

class _DeliveryMapState extends State<DeliveryMap> {
  GoogleMapController? _controller;
  final Set<Marker> _markers = {};

  @override
  void initState() {
    super.initState();
    _markers.add(
      Marker(
        markerId: const MarkerId('driver'),
        position: widget.driverLocation,
        icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueAzure),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return GoogleMap(
      initialCameraPosition: CameraPosition(
        target: widget.driverLocation,
        zoom: 15,
      ),
      markers: _markers,
      myLocationEnabled: true,
      onMapCreated: (c) => _controller = c,
    );
  }

  Future<void> followDriver(LatLng next) async {
    setState(() {
      _markers.removeWhere((m) => m.markerId.value == 'driver');
      _markers.add(Marker(
        markerId: const MarkerId('driver'),
        position: next,
      ));
    });
    await _controller?.animateCamera(
      CameraUpdate.newLatLng(next),
    );
  }
}

Keep a single GoogleMapController reference. Dispose it when the widget leaves the tree on older plugin versions that leaked platform views.

Markers, clustering, and performance

Individual Marker objects work up to roughly 50–100 points on mid-range phones. Beyond that:

  1. Cluster manager — group nearby markers into count bubbles at low zoom, expand on tap.
  2. Custom small icons — 32×32 or 48×48 PNGs, not full-resolution photos.
  3. Rebuild discipline — store markers in a Set outside build() and mutate in response to events, not every frame.
  4. Heatmap layer — for density visualization without per-point widgets.

For live tracking, update marker position with animateCamera throttled to once per second, not on every GPS callback at 10 Hz.

User location and permissions

Request location in context, not at app launch:

Future<Position?> currentPosition() async {
  var status = await Permission.locationWhenInUse.status;
  if (!status.isGranted) {
    status = await Permission.locationWhenInUse.request();
  }
  if (!status.isGranted) return null;
  return Geolocator.getCurrentPosition(
    locationSettings: const LocationSettings(
      accuracy: LocationAccuracy.high,
    ),
  );
}

Enable myLocationEnabled: true only after permission is granted. On iOS, add NSLocationWhenInUseUsageDescription with a specific reason ("Show your position relative to the delivery driver"). Vague strings get App Store rejections.

Polylines, geofencing, and tap handling

Route display uses Polyline with decoded coordinates from your routing API:

Polyline(
  polylineId: const PolylineId('route'),
  points: decodedRoute,
  color: Colors.blue,
  width: 5,
)

For tap-to-select, use onTap on the map to reverse-geocode or hit-test against your marker set. Debounce taps during camera animation — users tapping while the map moves produce coordinates that miss intended targets.

Geofencing belongs in a background location service (geolocator + platform geofence APIs), not in the map widget. The map displays geofence circles; the OS triggers entry/exit events.

Web and desktop considerations

google_maps_flutter_web renders via JavaScript interop. Performance is acceptable for locator pages but poor for heavy marker apps. Prefer a static map image or simplified list view on web if marker count exceeds 100. Mapbox Flutter web support is stronger for custom styling but adds bundle weight.

Test map widgets inside ListView and PageView carefully — nested scrollables conflict with map pan gestures. Wrap the map in GestureDetector with behavior: HitTestBehavior.opaque or use InteractiveViewer only when necessary.

Platform view performance

Hybrid composition mode on Android affects map scroll performance inside complex layouts—test Hybrid Composition vs Virtual Display settings in platform view documentation for your target devices. Impeller on iOS may change platform view behavior; re-test map overlays after Flutter major upgrades.

Offline maps require downloaded tile regions—Google Maps SDK offline limited; Mapbox better for offline expedition apps. Document offline capability in app store description if ships without network dependency for core map features to set user expectations accurately.

Production teams should treat this guidance as living documentation: revisit assumptions after major platform upgrades, measure outcomes with real metrics rather than checklist compliance, and pair written standards with automated checks in CI. The patterns here reflect what held up in shipped apps—not theoretical perfection. Adapt thresholds, timeouts, and tooling to your stack, but keep the underlying principles: explicit configuration, testable behavior, and failure modes users can understand.

Resources

Frequently asked questions

Should I use google_maps_flutter or Mapbox in Flutter?

Use google_maps_flutter when your users expect Google Maps styling, you rely on Places/Geocoding APIs, or Android/iOS parity with minimal custom tile work. Choose Mapbox when you need custom map styles, offline tile packs, vector tile performance at scale, or tighter control over rendering. Both work in production; the decision is usually licensing cost and styling requirements, not Flutter limitations.

How do I handle hundreds of markers without jank?

Do not render hundreds of individual Marker widgets. Use marker clustering (google_maps_cluster_manager or Mapbox clustering layers), reduce marker bitmap size, debounce camera updates, and move heavy geospatial math off the UI isolate. For very large datasets, render markers server-side into tile layers or use a heatmap layer instead of point markers.

Why does my Flutter map show a blank gray screen?

Almost always an API key or platform configuration issue. On Android, verify the Maps SDK key in AndroidManifest and restrict it correctly. On iOS, set GMSApiKey in AppDelegate and enable Maps SDK in Google Cloud Console. For Mapbox, confirm the access token in main() before runApp. Also check that billing is enabled for Google Maps and that the emulator has Google Play Services.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →