Flutter + Google Maps: A Complete Integration Guide
Integrate Google Maps in Flutter: setup keys, Android/iOS config, markers, directions, clustering, styling, and best practices.
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
- In Google Cloud Console, create/select a project.
- Enable the following APIs as needed:
- Maps SDK for Android
- Maps SDK for iOS
- (Optional) Places API, Geocoding API, Directions API
- 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.
- 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
Flutter Push Notifications with Firebase Cloud Messaging (FCM): A Complete Setup Guide
Step-by-step guide to set up FCM push notifications in Flutter for Android and iOS, with code, permissions, background handling, and testing tips.
Build a Real‑Time Chat App in Flutter with WebSockets
Build a robust Flutter WebSocket real-time chat app: minimal server, resilient client with reconnection and heartbeats, security, scaling, and deployment.
Flutter Plugin Development with Native Code: Channels, Pigeon, and FFI
A practical, end-to-end guide to building robust Flutter plugins with native code, Pigeon, FFI, testing, and multi-platform best practices.