Build an interactive map feature using FlutterFlowGoogleMap with markers loaded from a Firestore locations collection. Add address search via the Google Geocoding API, filter markers by category with ChoiceChips, implement marker clustering with a Custom Widget using google_maps_cluster_manager for large datasets, and show location details in a Bottom Sheet when a marker is tapped. Geospatial queries load only markers within the visible map bounds to keep performance smooth.
Building an Interactive Map with Search and Clustering in FlutterFlow
Interactive maps are essential for location-based apps like store locators, property finders, and city guides. This tutorial goes beyond basic map setup to add real-world features: search by address, category filtering, marker clustering for large datasets, and tappable markers that open a detail sheet with a directions button. It is designed for founders building any location-aware application.
Prerequisites
- A FlutterFlow project with Google Maps enabled in Settings
- Google Maps API key with Maps SDK, Geocoding API, and Directions API enabled
- Firestore database with a locations collection containing lat/lng coordinates
- Basic familiarity with FlutterFlow Backend Queries and Custom Widgets
Step-by-step guide
Set up the locations collection and load markers on the map
Set up the locations collection and load markers on the map
Create a Firestore locations collection with fields: name (String), lat (Double), lng (Double), category (String), description (String), imageUrl (String), and address (String). Add several sample locations. On your Map page, add a FlutterFlowGoogleMap widget. Create a Backend Query that fetches all locations (or filter by visible bounds for large datasets). For each location document, create a marker using the lat and lng fields. Set the marker title to the location name. Bind the list of markers to the map's markers property.
Expected result: The map displays markers for all locations stored in Firestore. Each marker shows the location name when tapped.
Add address search with geocoding and map animation
Add address search with geocoding and map animation
Add a TextField at the top of the page with a search icon and placeholder text 'Search address or place'. Below the TextField, add a search Button. On tap, call the Google Geocoding API via an API Call action: GET https://maps.googleapis.com/maps/api/geocode/json?address={searchText}&key={API_KEY}. Parse the response to extract results[0].geometry.location.lat and lng. Animate the map to the geocoded coordinates using the Animate Map action with a zoom level of 15. Store the search result in Page State so you can display a marker at the searched location.
Expected result: Users type an address, tap search, and the map smoothly animates to the geocoded location with a marker at the searched position.
Filter markers by category using ChoiceChips
Filter markers by category using ChoiceChips
Add ChoiceChips below the search bar with category options matching your locations data (e.g., Restaurant, Shopping, Park, Hotel). Bind the selected chip to Page State selectedCategory. Modify your Backend Query to add a where clause filtering by category when selectedCategory is not null. Add an 'All' chip option that clears the filter and shows every marker. When the category changes, the Backend Query re-fires and the map updates with only the matching markers.
Expected result: Selecting a category chip filters the map to show only matching markers. The All chip restores all markers.
Implement marker clustering for large datasets with a Custom Widget
Implement marker clustering for large datasets with a Custom Widget
For maps with hundreds of markers, create a Custom Widget using the google_maps_cluster_manager package. Add google_maps_cluster_manager: ^0.4.0 and google_maps_flutter: ^2.5.0 to Pubspec Dependencies. In the Custom Widget, initialize a ClusterManager with the list of locations as ClusterItem objects. The ClusterManager groups nearby markers into cluster circles at low zoom levels that break apart as the user zooms in. Pass the locations list as a Component Parameter (JSON list). Configure the cluster appearance to show a circle with the count number inside.
1// Custom Widget: ClusteredMap2import 'package:flutter/material.dart';3import 'package:google_maps_flutter/google_maps_flutter.dart';4import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart';56class Place with ClusterItem {7 final String name;8 final LatLng latLng;9 Place({required this.name, required this.latLng});10 @override11 LatLng get location => latLng;12}1314class ClusteredMapWidget extends StatefulWidget {15 final double width;16 final double height;17 final List<dynamic> locations;18 final Future Function(String name, double lat, double lng)? onMarkerTap;1920 const ClusteredMapWidget({21 Key? key,22 required this.width,23 required this.height,24 required this.locations,25 this.onMarkerTap,26 }) : super(key: key);2728 @override29 State<ClusteredMapWidget> createState() => _ClusteredMapWidgetState();30}3132class _ClusteredMapWidgetState extends State<ClusteredMapWidget> {33 late ClusterManager<Place> _clusterManager;34 Set<Marker> _markers = {};35 GoogleMapController? _controller;3637 @override38 void initState() {39 super.initState();40 final places = widget.locations.map((loc) => Place(41 name: loc['name'] ?? '',42 latLng: LatLng(loc['lat'] ?? 0, loc['lng'] ?? 0),43 )).toList();44 _clusterManager = ClusterManager<Place>(45 places,46 _updateMarkers,47 markerBuilder: _markerBuilder,48 );49 }5051 void _updateMarkers(Set<Marker> markers) {52 setState(() => _markers = markers);53 }5455 Future<Marker> _markerBuilder(Cluster<Place> cluster) async {56 return Marker(57 markerId: MarkerId(cluster.getId()),58 position: cluster.location,59 onTap: () {60 if (!cluster.isMultiple) {61 widget.onMarkerTap?.call(62 cluster.items.first.name,63 cluster.location.latitude,64 cluster.location.longitude,65 );66 }67 },68 icon: cluster.isMultiple69 ? await _getClusterBitmap(cluster.count)70 : BitmapDescriptor.defaultMarker,71 );72 }7374 Future<BitmapDescriptor> _getClusterBitmap(int count) async {75 return BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueAzure);76 }7778 @override79 Widget build(BuildContext context) {80 return SizedBox(81 width: widget.width,82 height: widget.height,83 child: GoogleMap(84 initialCameraPosition: const CameraPosition(85 target: LatLng(37.7749, -122.4194),86 zoom: 12,87 ),88 markers: _markers,89 onMapCreated: (controller) {90 _controller = controller;91 _clusterManager.setMapId(controller.mapId);92 },93 onCameraMove: _clusterManager.onCameraMove,94 onCameraIdle: _clusterManager.updateMap,95 ),96 );97 }98}Expected result: At low zoom levels, nearby markers merge into cluster circles showing a count. Zooming in splits them into individual markers.
Show a detail Bottom Sheet on marker tap with directions button
Show a detail Bottom Sheet on marker tap with directions button
When a marker is tapped (via the onMarkerTap Action Parameter callback from the Custom Widget, or the built-in FlutterFlowGoogleMap marker tap action), trigger a Show Bottom Sheet action. The Bottom Sheet contains a Component with: location Image, name Text, description Text, address Text, and a 'Get Directions' Button. The Get Directions button uses Launch URL with the value 'https://www.google.com/maps/dir/?api=1&destination={lat},{lng}' to open Google Maps with turn-by-turn directions from the user's current location.
Expected result: Tapping a marker slides up a Bottom Sheet showing the location image, name, description, and a Get Directions button that opens Google Maps navigation.
Complete working example
1// Custom Widget: ClusteredMap with Marker Tap Callback2import 'package:flutter/material.dart';3import 'package:google_maps_flutter/google_maps_flutter.dart';4import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart';56class Place with ClusterItem {7 final String id;8 final String name;9 final LatLng latLng;10 Place({required this.id, required this.name, required this.latLng});11 @override12 LatLng get location => latLng;13}1415class ClusteredMapWidget extends StatefulWidget {16 final double width;17 final double height;18 final List<dynamic> locations;19 final Future Function(String id, String name, double lat, double lng)? onMarkerTap;2021 const ClusteredMapWidget({22 Key? key,23 required this.width,24 required this.height,25 required this.locations,26 this.onMarkerTap,27 }) : super(key: key);2829 @override30 State<ClusteredMapWidget> createState() => _ClusteredMapWidgetState();31}3233class _ClusteredMapWidgetState extends State<ClusteredMapWidget> {34 late ClusterManager<Place> _manager;35 Set<Marker> _markers = {};36 GoogleMapController? _mapCtrl;3738 @override39 void initState() {40 super.initState();41 final places = widget.locations.map((l) => Place(42 id: l['id'] ?? '',43 name: l['name'] ?? '',44 latLng: LatLng((l['lat'] ?? 0).toDouble(), (l['lng'] ?? 0).toDouble()),45 )).toList();4647 _manager = ClusterManager<Place>(48 places,49 (markers) => setState(() => _markers = markers),50 markerBuilder: _buildMarker,51 );52 }5354 Future<Marker> _buildMarker(Cluster<Place> cluster) async {55 return Marker(56 markerId: MarkerId(cluster.getId()),57 position: cluster.location,58 onTap: () {59 if (!cluster.isMultiple) {60 final p = cluster.items.first;61 widget.onMarkerTap?.call(p.id, p.name, p.latLng.latitude, p.latLng.longitude);62 }63 },64 icon: cluster.isMultiple65 ? BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueAzure)66 : BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed),67 );68 }6970 void animateTo(double lat, double lng, double zoom) {71 _mapCtrl?.animateCamera(CameraUpdate.newLatLngZoom(LatLng(lat, lng), zoom));72 }7374 @override75 Widget build(BuildContext context) {76 return SizedBox(77 width: widget.width,78 height: widget.height,79 child: GoogleMap(80 initialCameraPosition: const CameraPosition(81 target: LatLng(37.7749, -122.4194),82 zoom: 11,83 ),84 markers: _markers,85 onMapCreated: (ctrl) {86 _mapCtrl = ctrl;87 _manager.setMapId(ctrl.mapId);88 },89 onCameraMove: _manager.onCameraMove,90 onCameraIdle: _manager.updateMap,91 myLocationEnabled: true,92 myLocationButtonEnabled: true,93 zoomControlsEnabled: false,94 ),95 );96 }97}Common mistakes when building a Custom Interactive Map Feature in FlutterFlow
Why it's a problem: Loading all location markers at once for a dataset with thousands of entries
How to avoid: Use marker clustering to group nearby markers visually, and add geospatial filtering to only load markers within the visible map bounds.
Why it's a problem: Hardcoding the Google Maps API key in client-side code
How to avoid: Restrict your API key in the Google Cloud Console to your app's bundle ID (mobile) and domain (web). Use separate keys for each platform.
Why it's a problem: Not handling the case when geocoding returns zero results
How to avoid: Check that the geocoding response status is 'OK' and results array is not empty before accessing coordinates. Show a 'No results found' SnackBar on failure.
Best practices
- Restrict your Google Maps API key by platform (Android, iOS, web) in the Cloud Console
- Use marker clustering when you have more than 50 markers to keep the map readable
- Filter locations by visible map bounds using geospatial queries to reduce Firestore reads
- Add a 'My Location' button so users can quickly center the map on their current position
- Cache geocoding results in Page State to avoid re-querying the same address
- Use distinct marker colors or custom icons per category for visual clarity
- Pre-calculate and store the directions URL template on each location document for faster Bottom Sheet loading
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I need to build an interactive map feature in FlutterFlow with markers from Firestore, address search using Google Geocoding API, category filtering with ChoiceChips, and marker clustering for large datasets. Show me the Custom Widget code for the clustered map and the FlutterFlow action flow for search.
Create a map page with Google Maps showing markers from a Firestore locations collection. Add a search bar at the top that geocodes addresses and animates the map. Add ChoiceChips for category filtering. Show a Bottom Sheet with location details and a Get Directions button when a marker is tapped.
Frequently asked questions
Can I use Mapbox instead of Google Maps in FlutterFlow?
FlutterFlow's built-in map widget uses Google Maps. For Mapbox, you would need to create a Custom Widget using the flutter_map or mapbox_maps_flutter package and handle markers and interactions manually.
How do I show the user's current location on the map?
Enable myLocationEnabled on the Google Map widget. In FlutterFlow, this is a toggle in the map properties. Make sure to request location permissions in your app settings.
How many markers can FlutterFlowGoogleMap handle before performance degrades?
Performance starts degrading around 100-200 individual markers on mobile. Use marker clustering to handle larger datasets efficiently. With clustering, you can support thousands of locations.
Can I draw routes between two markers on the map?
Yes. Use the Google Directions API to get route polyline data, then decode the polyline string and overlay it on the map using a Custom Widget that draws Polyline objects on GoogleMap.
How do I make markers update in real time as locations change?
Set Single Time Query to OFF on your locations Backend Query. When a location document is updated in Firestore, the query re-fires and the map markers update automatically.
Can RapidDev build a custom location-based app with advanced map features?
Yes. RapidDev can implement store locators, delivery tracking maps, geofencing, route optimization, and custom map styling tailored to your brand and use case.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation