Add geotagging in FlutterFlow by auto-capturing device coordinates with geolocator when users create content, storing latitude/longitude and a geohash string in Firestore. For proximity search, use a Cloud Function that queries geohash prefixes to find nearby documents efficiently. Never call Google Geocoding API on every list item render — it will exhaust your API quota within hours.
Building Location Awareness into a FlutterFlow App
Location-based features — auto-tagging photos, finding nearby businesses, showing 'within 5km' results — require two distinct capabilities: capturing a device's GPS coordinates and querying a database for documents near a given location. Firestore does not have native geospatial query support, so proximity search requires a technique called geohashing. A geohash is a string that encodes latitude and longitude into a compact form where geographically close points share a common prefix. By querying for documents where the geohash starts with a given prefix, you efficiently retrieve all items within a geographic region. This tutorial covers the complete flow from GPS capture to map display.
Prerequisites
- FlutterFlow project connected to Firebase/Firestore
- Google Maps API key with Maps SDK for Android, Maps SDK for iOS, and Places API enabled
- geolocator package added to pubspec.yaml (FlutterFlow Pro required)
- Location permission configured in iOS Info.plist and Android AndroidManifest.xml
Step-by-step guide
Capture GPS coordinates with geolocator on content creation
Capture GPS coordinates with geolocator on content creation
Create a Custom Action named getCurrentLocation that returns a Map containing latitude (double) and longitude (double). Add the geolocator: ^10.1.0 package to pubspec.yaml. In the action, call Geolocator.checkPermission() — if denied, call Geolocator.requestPermission(). Then call Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.medium) and return the position coordinates. In FlutterFlow, add this Custom Action to the beginning of your content creation Action Flow (before the Firestore Create Document action). Store the returned coordinates in Page State variables currentLat and currentLng. Use LocationAccuracy.medium rather than high — medium uses cell tower triangulation which is fast and sufficient for location tagging, while high accuracy GPS takes 5-10 seconds and drains battery.
1// Custom Action: getCurrentLocation2// No parameters3// Returns: String in format "lat,lng" (e.g. "51.5074,-0.1278")4import 'package:geolocator/geolocator.dart';56Future<String> getCurrentLocation() async {7 LocationPermission permission = await Geolocator.checkPermission();8 if (permission == LocationPermission.denied) {9 permission = await Geolocator.requestPermission();10 if (permission == LocationPermission.denied) return '';11 }12 if (permission == LocationPermission.deniedForever) return '';1314 final position = await Geolocator.getCurrentPosition(15 desiredAccuracy: LocationAccuracy.medium,16 );17 return '${position.latitude},${position.longitude}';18}Expected result: When the user opens the content creation page, the action captures their current coordinates within 2-3 seconds and stores them for use in the Firestore write.
Store coordinates and a geohash in Firestore
Store coordinates and a geohash in Firestore
Add three fields to your Firestore content collection documents: latitude (Number), longitude (Number), and geohash (String). The geohash is a string like 'gcpvj' that encodes the location. Generate it in a Custom Action using the dart_geohash package or by implementing the algorithm directly. A geohash of length 5 covers approximately a 5km x 5km area — appropriate for most location-based apps. When writing the content document, include all three fields. The latitude and longitude are used for display (showing the exact pin on a map) while the geohash is used for querying (finding documents in a geographic area efficiently without loading every document).
1// Custom Action: encodeGeohash2// Parameters: latLng (String, format: "lat,lng")3// Returns: String (geohash of length 5)4// Simple geohash encoding — for production, use dart_geohash package5Future<String> encodeGeohash(String latLng) async {6 if (latLng.isEmpty) return '';7 final parts = latLng.split(',');8 if (parts.length != 2) return '';9 final lat = double.tryParse(parts[0]) ?? 0.0;10 final lng = double.tryParse(parts[1]) ?? 0.0;1112 // Use dart_geohash package in production:13 // import 'package:dart_geohash/dart_geohash.dart';14 // return GeoHasher().encode(lng, lat, precision: 5);1516 // Simplified placeholder — add dart_geohash to pubspec.yaml17 return _encodeSimple(lat, lng, 5);18}1920String _encodeSimple(double lat, double lng, int precision) {21 const base32 = '0123456789bcdefghjkmnpqrstuvwxyz';22 var minLat = -90.0, maxLat = 90.0;23 var minLng = -180.0, maxLng = 180.0;24 var isEven = true;25 var bit = 0, ch = 0;26 final result = StringBuffer();2728 while (result.length < precision) {29 if (isEven) {30 final mid = (minLng + maxLng) / 2;31 if (lng >= mid) { ch |= (1 << (4 - bit)); minLng = mid; } else { maxLng = mid; }32 } else {33 final mid = (minLat + maxLat) / 2;34 if (lat >= mid) { ch |= (1 << (4 - bit)); minLat = mid; } else { maxLat = mid; }35 }36 isEven = !isEven;37 if (bit < 4) { bit++; } else { result.write(base32[ch]); bit = 0; ch = 0; }38 }39 return result.toString();40}Expected result: Content documents in Firestore include latitude, longitude, and a 5-character geohash string alongside the regular content fields.
Use a Cloud Function for proximity search by geohash prefix
Use a Cloud Function for proximity search by geohash prefix
Create an HTTP Cloud Function named searchNearby that accepts lat, lng, and radiusKm as query parameters. In the function, compute the set of geohash prefixes that cover the search radius around the given point. For a 5km radius, a 4-character prefix covers the area. Query Firestore for documents where geohash starts with each of the computed prefixes using a range query (.where('geohash', isGreaterThanOrEqualTo: prefix).where('geohash', isLessThan: prefix + '~')). Merge the results, filter by exact distance using the Haversine formula to remove false positives from the rectangular geohash grid, and return the filtered results as JSON. Call this Cloud Function from FlutterFlow using an API call action.
1// Cloud Function: searchNearby2// Query params: lat, lng, radiusKm3const functions = require('firebase-functions');4const admin = require('firebase-admin');56exports.searchNearby = functions.https.onRequest(async (req, res) => {7 res.set('Access-Control-Allow-Origin', '*');8 const { lat, lng, radiusKm = 5 } = req.query;9 if (!lat || !lng) return res.status(400).json({ error: 'lat and lng required' });1011 const center = [parseFloat(lat), parseFloat(lng)];12 const radius = parseFloat(radiusKm);1314 // Compute geohash prefix length based on radius15 // 5km radius -> prefix length 4, 1km -> length 516 const prefixLen = radius <= 1 ? 5 : radius <= 10 ? 4 : 3;17 const centerHash = encodeGeohash(center[0], center[1], prefixLen);1819 // Query Firestore for documents near the center hash20 const db = admin.firestore();21 const snap = await db.collection('content')22 .where('geohash', '>=', centerHash)23 .where('geohash', '<', centerHash + '~')24 .limit(50)25 .get();2627 // Filter by exact distance (Haversine)28 const results = snap.docs29 .map(d => ({ id: d.id, ...d.data() }))30 .filter(doc => {31 const dist = haversineKm(center[0], center[1], doc.latitude, doc.longitude);32 return dist <= radius;33 });3435 res.json({ results });36});3738function haversineKm(lat1, lng1, lat2, lng2) {39 const R = 6371;40 const dLat = (lat2 - lat1) * Math.PI / 180;41 const dLng = (lng2 - lng1) * Math.PI / 180;42 const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)*Math.sin(dLng/2)**2;43 return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));44}Expected result: The Cloud Function returns a JSON array of documents within the specified radius, sorted by distance, with no false positives from rectangular grid edges.
Add Google Places Autocomplete for address-based search
Add Google Places Autocomplete for address-based search
For users who want to search by address rather than their current location, add Google Places Autocomplete. In FlutterFlow, go to the Google Maps settings and enable the Places API key. Add a TextField to your search page and an API call action on text change that calls the Google Places Autocomplete API (maps.googleapis.com/maps/api/place/autocomplete/json) with the typed text as the input parameter and your API key. Display the returned predictions in a ListView below the search field. When the user taps a prediction, make a second API call to Places Details to get the exact latitude and longitude of the selected place. Then use these coordinates as the search center for your searchNearby Cloud Function call.
Expected result: Typing an address in the search field shows a dropdown of matching places. Selecting a place fetches its coordinates and triggers the proximity search centered on that address.
Display results on a Google Maps widget with markers
Display results on a Google Maps widget with markers
In FlutterFlow, add a Google Maps widget to your search results page. Set the widget to receive a list of map markers generated from the searchNearby results. Each result document becomes a marker with its latitude and longitude. For the marker info window, show the content title and a thumbnail image. Add a Custom Marker Icon action to differentiate content types with different coloured pins. When the map has more than 15-20 markers in view, enable marker clustering by adding the google_maps_cluster_manager package as a Custom Widget — clustering groups nearby markers into numbered circles, preventing visual clutter on dense datasets.
Expected result: Search results appear as pins on the Google Maps widget. Tapping a pin shows the content title. Nearby pins are grouped into numbered cluster markers at low zoom levels.
Complete working example
1// Cloud Function: searchNearby2// Deploy with: firebase deploy --only functions:searchNearby3// Requires firebase-admin and firebase-functions45const functions = require('firebase-functions');6const admin = require('firebase-admin');78if (!admin.apps.length) admin.initializeApp();910// Geohash encoding (simplified — use geofire-common package in production)11function encodeGeohash(lat, lng, precision) {12 const BASE32 = '0123456789bcdefghjkmnpqrstuvwxyz';13 let minLat = -90, maxLat = 90, minLng = -180, maxLng = 180;14 let isEven = true, bit = 0, ch = 0;15 let result = '';16 while (result.length < precision) {17 if (isEven) {18 const mid = (minLng + maxLng) / 2;19 if (lng >= mid) { ch |= 1 << (4 - bit); minLng = mid; } else { maxLng = mid; }20 } else {21 const mid = (minLat + maxLat) / 2;22 if (lat >= mid) { ch |= 1 << (4 - bit); minLat = mid; } else { maxLat = mid; }23 }24 isEven = !isEven;25 if (bit < 4) { bit++; } else { result += BASE32[ch]; bit = 0; ch = 0; }26 }27 return result;28}2930function haversineKm(lat1, lng1, lat2, lng2) {31 const R = 6371;32 const dLat = (lat2 - lat1) * Math.PI / 180;33 const dLng = (lng2 - lng1) * Math.PI / 180;34 const a = Math.sin(dLat / 2) ** 2 +35 Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *36 Math.sin(dLng / 2) ** 2;37 return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));38}3940exports.searchNearby = functions.https.onRequest(async (req, res) => {41 res.set('Access-Control-Allow-Origin', '*');42 res.set('Access-Control-Allow-Methods', 'GET');4344 const lat = parseFloat(req.query.lat);45 const lng = parseFloat(req.query.lng);46 const radiusKm = parseFloat(req.query.radiusKm || '5');47 const collection = req.query.collection || 'content';4849 if (isNaN(lat) || isNaN(lng)) {50 return res.status(400).json({ error: 'lat and lng must be valid numbers' });51 }5253 // Choose prefix length based on radius54 const prefixLen = radiusKm <= 1 ? 5 : radiusKm <= 10 ? 4 : 3;55 const centerHash = encodeGeohash(lat, lng, prefixLen);5657 const db = admin.firestore();58 const snapshot = await db.collection(collection)59 .where('geohash', '>=', centerHash)60 .where('geohash', '<', centerHash + '~')61 .limit(100)62 .get();6364 const results = snapshot.docs65 .map(doc => ({ id: doc.id, ...doc.data() }))66 .filter(doc => {67 if (typeof doc.latitude !== 'number' || typeof doc.longitude !== 'number') return false;68 return haversineKm(lat, lng, doc.latitude, doc.longitude) <= radiusKm;69 })70 .map(doc => ({71 id: doc.id,72 title: doc.title || '',73 latitude: doc.latitude,74 longitude: doc.longitude,75 distanceKm: Math.round(haversineKm(lat, lng, doc.latitude, doc.longitude) * 10) / 10,76 }))77 .sort((a, b) => a.distanceKm - b.distanceKm);7879 res.json({ results, count: results.length });80});Common mistakes
Why it's a problem: Calling Google Geocoding API for reverse geocode on every list item render
How to avoid: Reverse geocode once when the user creates content and store the resulting city/neighbourhood string in Firestore. Bind the pre-stored address string directly to the list item widget instead of calling the API on every render.
Why it's a problem: Storing only latitude and longitude without a geohash in Firestore
How to avoid: Always store a geohash string alongside lat/lng when writing location-tagged documents. The geohash enables efficient range queries that reduce the candidate set to only documents in the target geographic area.
Why it's a problem: Not filtering geohash query results by exact Haversine distance
How to avoid: Always apply a Haversine distance filter after the Firestore geohash query to remove documents that fall in the rectangular grid corners but outside the actual circular search radius.
Best practices
- Store latitude, longitude, AND geohash on every location-tagged document — all three are needed for different operations
- Choose geohash precision based on your search radius: precision 5 for 1-5km, precision 4 for 5-20km
- Always apply Haversine distance filtering after geohash queries to remove rectangular boundary false positives
- Reverse geocode at write time and store the address string — never call Geocoding API on list render
- Use LocationAccuracy.medium for content tagging and location search — GPS-level accuracy is overkill and slow
- Cache the user's current location in App State and refresh it on page load, not on every action
- Enable marker clustering on Google Maps when displaying more than 15 location results simultaneously
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I am building a location-based search feature in FlutterFlow using Firestore. I store latitude, longitude, and a geohash string on each document. How do I write a Firebase Cloud Function that takes a latitude, longitude, and radius in km, queries Firestore using geohash prefix matching, and filters the results by exact Haversine distance before returning them?
Create a FlutterFlow Custom Action called getCurrentLocation that uses the geolocator package to get the device's current GPS position. It should check and request location permission, get the position with LocationAccuracy.medium, and return the coordinates as a String in 'latitude,longitude' format (e.g. '51.5074,-0.1278'). Return an empty String if permission is denied.
Frequently asked questions
Do I need a paid Google Maps API plan for geotagging and proximity search?
Google Maps Platform has a free monthly credit of $200, which covers approximately 28,000 map loads or 40,000 Geocoding API calls. For most apps with under a few thousand daily active users, the free tier is sufficient. GPS coordinate capture via geolocator does not use the Google Maps API and is completely free.
What geohash precision should I use for my search radius?
As a rule of thumb: precision 6 covers roughly 1km x 0.6km (city block level), precision 5 covers 5km x 5km (neighbourhood level), precision 4 covers 40km x 20km (city level), precision 3 covers 160km x 160km (regional level). For a 5km search radius, precision 4 or 5 both work. Precision 5 will return fewer false positives but may miss documents at the edges of the radius.
Can I use Firestore's native GeoPoint type instead of storing lat/lng as separate numbers?
Yes. Firestore has a GeoPoint type that stores lat/lng as a single field. However, Firestore still cannot query by distance natively — you still need the geohash field for proximity queries. You can store both: a GeoPoint for display purposes and a geohash string for search. FlutterFlow supports GeoPoint field types in its Firestore panel.
How do I update a document's geotag if the user edits the content and is in a different location?
In your content edit Action Flow, call getCurrentLocation again to get the new coordinates and encodeGeohash to compute the new geohash. Include latitude, longitude, and geohash in the Firestore Update Document action alongside the other updated fields. The old geohash will be replaced automatically.
Is there a simpler way to do location search without writing a Cloud Function?
GeoFlutterFire2 is a Flutter package that handles geohash querying client-side, eliminating the need for a Cloud Function. Add it to pubspec.yaml (requires FlutterFlow Pro for code export) and use its query method with a GeoFirePoint center and radius. It handles the geohash prefix computation and Haversine filtering internally.
How accurate is geohash-based proximity search?
Geohash queries are accurate for finding documents within a geographic area, but the rectangular bounding box means results can include documents slightly outside your radius. The Haversine filter removes these. The core GPS coordinates are accurate to within 3-5 metres using LocationAccuracy.medium, and within 1-2 metres using LocationAccuracy.best.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation