Flutter + Google Maps: A Complete Integration Guide

Integrate Google Maps in Flutter: setup keys, Android/iOS config, markers, directions, clustering, styling, and best practices.

ASOasis
7 min read
Flutter + Google Maps: A Complete Integration Guide

Image used for representation purposes only.

Overview

Google Maps is one of the most requested features in mobile apps. With Flutter, you can render highly interactive maps, place markers, draw routes, and style the map to match your brand—all from a single codebase. This guide walks you through a production-ready integration using the official google_maps_flutter plugin, then layers on advanced features such as clustering, directions, custom markers, and performance tuning.

What You’ll Build

  • A Flutter screen that displays a Google Map widget
  • Interactive markers and info windows
  • User location and camera controls
  • Polylines for routes (Directions API)
  • Optional: marker clustering, custom map styling, and web support

Prerequisites

  • Flutter SDK installed and a working emulator/device
  • A Google Cloud project with billing enabled
  • Familiarity with Dart, Android Studio/Xcode basics

Step 1: Create and Secure Your Google Maps API Key

  1. In Google Cloud Console, create/select a project.
  2. Enable the following APIs as needed:
    • Maps SDK for Android
    • Maps SDK for iOS
    • (Optional) Places API, Geocoding API, Directions API
  3. Create a restricted API key:
    • Android: restrict by package name and SHA-1/256 signing certificate.
    • iOS: restrict by bundle identifier.
    • Web (if targeting Flutter Web): restrict by HTTP referrers.
  4. Keep the key out of source control; use per-environment keys and CI secrets. Even with native SDKs, the key is embedded at build time—so rely on platform restrictions and API quotas.

Step 2: Add Dependencies

In pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  google_maps_flutter: ^2.7.0  # or latest
  http: ^1.2.0                 # for Directions API calls
  geolocator: ^11.0.0          # for location/permissions (optional)
  # Optional add-ons
  google_maps_cluster_manager: ^3.1.0  # community clustering
  flutter_polyline_points: ^2.0.0      # decode polylines

(Optional) Assets for custom markers and map styles:

flutter:
  assets:
    - assets/markers/pin.png
    - assets/map_styles/dark.json

Run:

flutter pub get

Step 3: Platform Configuration

Android

  • android/app/src/main/AndroidManifest.xml:
<manifest ...>
  <application ...>
    <meta-data
      android:name="com.google.android.geo.API_KEY"
      android:value="YOUR_ANDROID_API_KEY" />
  </application>

  <!-- If you need user location -->
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
</manifest>
  • Ensure minSdkVersion meets the plugin’s requirement (commonly 20+). In android/app/build.gradle:
android {
  defaultConfig {
    minSdkVersion 20
  }
}

Tips:

  • Use a recent Android Gradle Plugin and Kotlin plugin.
  • On emulators, update Google Play services if the map doesn’t render.

iOS

  • iOS platform version (ios/Podfile):
platform :ios, '12.0'
  • Provide the API key in AppDelegate (Swift):
import UIKit
import Flutter
import GoogleMaps

@UIApplicationMain
class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GMSServices.provideAPIKey("YOUR_IOS_API_KEY")
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}
  • Info.plist (for location and permissions if you show user location):
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app uses your location to show your position on the map.</string>

Run:

cd ios && pod install && cd ..

Web (Optional)

For Flutter Web, add the web implementation and ensure your key is restricted to HTTP referrers:

dependencies:
  google_maps_flutter_web: ^0.5.0  # pairs with google_maps_flutter

Flutter will use the web implementation automatically when building for web. If needed, load the Maps JavaScript API via index.html or rely on the plugin’s loader. Restrict the key to your domains.

Step 4: Render Your First Map

Create a basic map screen.

import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

class MapScreen extends StatefulWidget {
  const MapScreen({super.key});

  @override
  State<MapScreen> createState() => _MapScreenState();
}

class _MapScreenState extends State<MapScreen> {
  late GoogleMapController _controller;
  static const _initialCamera = CameraPosition(
    target: LatLng(37.7749, -122.4194), // San Francisco
    zoom: 12,
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Google Map in Flutter')),
      body: GoogleMap(
        initialCameraPosition: _initialCamera,
        onMapCreated: (c) => _controller = c,
        myLocationButtonEnabled: true,
        zoomControlsEnabled: false,
        compassEnabled: true,
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () => _controller.animateCamera(
          CameraUpdate.newCameraPosition(
            const CameraPosition(target: LatLng(34.0522, -118.2437), zoom: 12),
          ),
        ),
        label: const Text('Go to LA'),
        icon: const Icon(Icons.explore),
      ),
    );
  }
}

Step 5: Markers and Info Windows

Add custom markers with tappable info windows.

class _MapScreenState extends State<MapScreen> {
  late GoogleMapController _controller;
  final Set<Marker> _markers = {};

  @override
  void initState() {
    super.initState();
    _addMarkers();
  }

  void _addMarkers() async {
    final customIcon = await BitmapDescriptor.fromAssetImage(
      const ImageConfiguration(size: Size(48, 48)),
      'assets/markers/pin.png',
    );

    _markers.addAll([
      Marker(
        markerId: const MarkerId('sf_pier'),
        position: const LatLng(37.8086, -122.4098),
        icon: customIcon,
        infoWindow: const InfoWindow(title: 'Pier 39', snippet: 'Sea lions & views'),
        onTap: () {},
      ),
      const Marker(
        markerId: MarkerId('gg_bridge'),
        position: LatLng(37.8199, -122.4783),
        infoWindow: InfoWindow(title: 'Golden Gate Bridge'),
      ),
    ]);
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Markers')),
      body: GoogleMap(
        initialCameraPosition: const CameraPosition(
          target: LatLng(37.7749, -122.4194),
          zoom: 12,
        ),
        markers: _markers,
        onMapCreated: (c) => _controller = c,
      ),
    );
  }
}

Tips:

  • Reuse MarkerId strings consistently for updates.
  • For dynamic icons (e.g., network images), render to bytes (ui.Codec) and use BitmapDescriptor.fromBytes.

Step 6: User Location and Permissions

  • Android/iOS require runtime permission for fine or when-in-use location.
  • With geolocator:
import 'package:geolocator/geolocator.dart';

Future<Position> _getPosition() async {
  bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
  if (!serviceEnabled) {
    return Future.error('Location services are disabled.');
  }
  LocationPermission permission = await Geolocator.checkPermission();
  if (permission == LocationPermission.denied) {
    permission = await Geolocator.requestPermission();
  }
  if (permission == LocationPermission.deniedForever) {
    return Future.error('Location permissions are permanently denied.');
  }
  return Geolocator.getCurrentPosition();
}
  • Show the native “blue dot” by enabling:
GoogleMap(
  myLocationEnabled: true,
  myLocationButtonEnabled: true,
  ...
)

Step 7: Drawing Routes with the Directions API

Query the Directions API, decode the polyline, and draw it on the map.

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_polyline_points/flutter_polyline_points.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

class DirectionsExample extends StatefulWidget {
  const DirectionsExample({super.key});
  @override
  State<DirectionsExample> createState() => _DirectionsExampleState();
}

class _DirectionsExampleState extends State<DirectionsExample> {
  final Set<Polyline> _polylines = {};
  GoogleMapController? _controller;

  Future<void> _fetchRoute() async {
    const origin = '37.7749,-122.4194'; // SF
    const dest = '37.8199,-122.4783';  // Golden Gate Bridge
    final url = Uri.parse(
      'https://maps.googleapis.com/maps/api/directions/json?origin=$origin&destination=$dest&mode=driving&key=YOUR_SERVER_OR_RESTRICTED_KEY',
    );
    final res = await http.get(url);
    final data = jsonDecode(res.body);
    final points = data['routes'][0]['overview_polyline']['points'];

    final decoded = PolylinePoints().decodePolyline(points);
    final polylineCoords = decoded
        .map((p) => LatLng(p.latitude, p.longitude))
        .toList(growable: false);

    setState(() {
      _polylines.clear();
      _polylines.add(Polyline(
        polylineId: const PolylineId('route'),
        width: 5,
        color: Colors.blueAccent,
        points: polylineCoords,
      ));
    });

    // Fit camera to route
    final bounds = _boundsFromLatLngList(polylineCoords);
    await _controller?.animateCamera(CameraUpdate.newLatLngBounds(bounds, 48));
  }

  LatLngBounds _boundsFromLatLngList(List<LatLng> list) {
    double x0 = list.first.latitude, x1 = list.first.latitude;
    double y0 = list.first.longitude, y1 = list.first.longitude;
    for (final latLng in list) {
      if (latLng.latitude < x0) x0 = latLng.latitude;
      if (latLng.latitude > x1) x1 = latLng.latitude;
      if (latLng.longitude < y0) y0 = latLng.longitude;
      if (latLng.longitude > y1) y1 = latLng.longitude;
    }
    return LatLngBounds(
      southwest: LatLng(x0, y0),
      northeast: LatLng(x1, y1),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Directions')),
      body: GoogleMap(
        initialCameraPosition: const CameraPosition(
          target: LatLng(37.7749, -122.4194),
          zoom: 12,
        ),
        polylines: _polylines,
        onMapCreated: (c) => _controller = c,
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _fetchRoute,
        child: const Icon(Icons.directions),
      ),
    );
  }
}

Notes:

  • Use a server-restricted key for Directions/Places calls from servers; for in-app calls, ensure domain/app restrictions and quota monitoring.
  • Handle zero results and error statuses gracefully.

Step 8: Marker Clustering (Optional)

For large datasets, aggregate nearby markers into clusters.

import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart';

class Place with ClusterItem {
  final LatLng position;
  final String name;
  Place(this.position, this.name);
  @override
  LatLng get location => position;
}

late ClusterManager<Place> _clusterManager;
final List<Place> _items = [
  Place(const LatLng(37.78, -122.42), 'A'),
  Place(const LatLng(37.79, -122.43), 'B'),
  // ... thousands more
];

void _initCluster() {
  _clusterManager = ClusterManager<Place>(
    _items,
    _updateMarkers,
    levels: const [1, 5, 10, 15, 20],
  );
}

void _updateMarkers(Set<Marker> markers) {
  setState(() => _markers = markers);
}

// In GoogleMap:
GoogleMap(
  onCameraMove: _clusterManager.onCameraMove,
  onCameraIdle: _clusterManager.updateMap,
  markers: _markers,
)

Step 9: Custom Map Styling

Load a JSON style (from Google Maps Style Wizard) and apply it at runtime.

Future<void> _applyMapStyle() async {
  final style = await DefaultAssetBundle.of(context)
      .loadString('assets/map_styles/dark.json');
  await _controller.setMapStyle(style);
}

Call _applyMapStyle() after onMapCreated. Keep a light/dark pair and switch with the theme.

Step 10: Performance and UX Tips

  • Prefer immutable Sets for markers/polylines; update only what changed.
  • Debounce camera move events if you query APIs during pan/zoom.
  • Use liteModeEnabled on Android for list previews or background maps.
  • Avoid rebuilding GoogleMap unnecessarily—lift it up the widget tree and control via controller calls.
  • Convert large image markers to small, cached bitmaps; avoid large PNGs.
  • On iOS, test on real devices to validate rendering and memory.

Testing and Debugging

  • Integration tests: drive the map screen, mock location with geolocator’s testing utilities.
  • Log MapController states (camera position, visible region) to verify boundaries and clustering logic.
  • Common issues:
    • Blank/gray map: invalid/blocked key, missing API enablement, or Play services outdated.
    • “For development purposes only” watermark: using a key without billing or improper restrictions.
    • Crashes on rotate/resume: ensure controller null checks and dispose patterns.

Production Checklist

  • Separate API keys per environment (dev/stage/prod) with proper restrictions.
  • Monitor quota and errors in Google Cloud console; set alerts.
  • Gracefully handle permission denial; provide fallbacks and explanations.
  • Cache geocoding/directions where appropriate to reduce API calls.
  • Review legal requirements for location usage disclosures and privacy policy.

Putting It Together

You now have a solid, extensible Google Maps integration in Flutter. Start with a clean map, add markers and user location, then expand with directions, clustering, and custom styles. With careful key restrictions and performance-minded updates, your map will feel fast, reliable, and production-ready across Android, iOS, and (optionally) the web.

Related Posts