Build location-based discovery in FlutterFlow by storing each item's location as a Firestore GeoPoint plus a geohash string. When the user searches, get their position with the geolocator package, convert radius to geohash bounds, query Firestore using geohash range filters, then sort results by exact distance using the Haversine formula. Never query all documents and filter on the client — that approach fails at scale.
Efficient Nearby Search in Firestore for FlutterFlow Apps
Location-based features — find nearby restaurants, connect with people in your area, show local listings — are among the most-requested app features. The naive implementation downloads every document and calculates distance on the client, which works fine for 50 documents but fails catastrophically at 50,000. The correct approach uses geohashing: encode each location's latitude and longitude into a short string that preserves geographic proximity. Firestore can then range-query these strings to return only documents within a bounding box, dramatically reducing both query cost and latency.
Prerequisites
- FlutterFlow Pro plan with code export enabled
- Firebase project with Firestore and Google Maps API key configured
- geolocator and geoflutterfire_plus packages added to Custom Dependencies
- Google Maps widget enabled in FlutterFlow (Settings → Integrations → Google Maps)
Step-by-step guide
Design the Firestore schema with GeoPoint and geohash
Design the Firestore schema with GeoPoint and geohash
Every document representing a physical location needs two fields stored together. In FlutterFlow's Firestore panel, add to your locations or listings collection: a location field of type LatLng (FlutterFlow's built-in type, which maps to Firestore GeoPoint) and a geohash field of type String. The geohash is a Base32 encoded string like 'u4pruydqqvj' where longer strings represent more precise locations. Store geohash precision 9 (9-character strings) for most use cases — this gives approximately 5-meter accuracy. The geohash is what you query; the GeoPoint is what you use to calculate exact distance after fetching results.
Expected result: Your Firestore collection has both a location (LatLng/GeoPoint) field and a geohash (String) field visible in the schema editor.
Add Custom Dependencies for geolocation and geohash
Add Custom Dependencies for geolocation and geohash
In FlutterFlow Settings → Custom Code → Dependencies, add three packages: geolocator (^13.0.0) for getting the user's GPS position with proper permission handling, geoflutterfire_plus (^0.0.30) for generating geohash values and creating query bounds, and latlong2 (^0.9.0) for the Haversine distance calculation. These three packages form the complete location services stack. After adding dependencies, create a Custom Action named requestLocationPermission that calls Geolocator.requestPermission() and stores the result. Call this action early in your app flow — on the discovery page's first load — before attempting any location queries.
Expected result: Three dependencies appear in your FlutterFlow Dependencies list. A location permission action exists in Custom Code.
Get the user's current position and store it in App State
Get the user's current position and store it in App State
Create a Custom Action named getUserLocation. This action calls Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.medium) with a 10-second timeout. If it succeeds, store the latitude and longitude in App State variables named userLatitude and userLongitude (both Double type). If location permissions are denied, show a Snackbar explaining that the feature requires location access and provide a button to open Settings. Use LocationAccuracy.medium rather than .best — medium accuracy is sufficient for proximity search and uses significantly less battery. Cache the position for 5 minutes using a timestamp App State variable to avoid re-requesting GPS on every page render.
1// Custom Action: getUserLocation2import 'package:geolocator/geolocator.dart';34Future<Map<String, dynamic>> getUserLocation() async {5 LocationPermission permission = await Geolocator.checkPermission();67 if (permission == LocationPermission.denied) {8 permission = await Geolocator.requestPermission();9 if (permission == LocationPermission.denied) {10 return {'success': false, 'error': 'Location permission denied'};11 }12 }1314 if (permission == LocationPermission.deniedForever) {15 return {'success': false, 'error': 'Location permission permanently denied'};16 }1718 try {19 final position = await Geolocator.getCurrentPosition(20 desiredAccuracy: LocationAccuracy.medium,21 timeLimit: const Duration(seconds: 10),22 );23 return {24 'success': true,25 'latitude': position.latitude,26 'longitude': position.longitude,27 };28 } catch (e) {29 return {'success': false, 'error': 'Could not get location: $e'};30 }31}Expected result: Calling getUserLocation returns the device's current GPS coordinates and stores them in App State. If permission is denied, an error message explains the issue.
Query nearby documents using geohash bounds
Query nearby documents using geohash bounds
Create a Custom Action named queryNearbyItems. This action generates geohash bounds for the search radius using GeoFirePoint from geoflutterfire_plus, then queries Firestore using a range filter on the geohash field. The bounds are the minimum and maximum geohash strings that fall within your radius. A range query returns all documents whose geohash falls between those bounds — this gives you approximately the right bounding box. After fetching, calculate the exact distance using Geolocator.distanceBetween() and filter out any documents outside the precise radius (geohash bounding boxes are squares, not circles). Sort the results by distance ascending.
1// Custom Action: queryNearbyItems2import 'package:geoflutterfire_plus/geoflutterfire_plus.dart';3import 'package:cloud_firestore/cloud_firestore.dart';4import 'package:geolocator/geolocator.dart';56Future<List<Map<String, dynamic>>> queryNearbyItems({7 required double userLat,8 required double userLng,9 required double radiusKm,10}) async {11 // Generate geohash bounds for the search radius12 final center = GeoFirePoint(GeoPoint(userLat, userLng));13 final bounds = GeoFirePoint.queryBounds(14 center: center.geopoint,15 radiusInKm: radiusKm,16 );1718 final db = FirebaseFirestore.instance;19 final futures = bounds.map((bound) {20 return db.collection('listings')21 .orderBy('geohash')22 .startAt([bound.startValue])23 .endAt([bound.endValue])24 .limit(50)25 .get();26 });2728 final snapshots = await Future.wait(futures);29 final allDocs = snapshots.expand((s) => s.docs).toList();3031 // Calculate exact distance and filter/sort32 final results = <Map<String, dynamic>>[];33 for (final doc in allDocs) {34 final data = doc.data();35 final geoPoint = data['location'] as GeoPoint;36 final distanceMeters = Geolocator.distanceBetween(37 userLat, userLng,38 geoPoint.latitude, geoPoint.longitude,39 );40 final distanceKm = distanceMeters / 1000;4142 if (distanceKm <= radiusKm) {43 results.add({44 ...data,45 'id': doc.id,46 'distanceKm': distanceKm,47 'distanceLabel': distanceKm < 148 ? '${(distanceKm * 1000).round()}m away'49 : '${distanceKm.toStringAsFixed(1)}km away',50 });51 }52 }5354 results.sort((a, b) =>55 (a['distanceKm'] as double).compareTo(b['distanceKm'] as double));56 return results;57}Expected result: Calling queryNearbyItems returns a sorted list of documents within the specified radius, each with a distanceKm and human-readable distanceLabel field.
Display results on Google Maps with distance-sorted ListView
Display results on Google Maps with distance-sorted ListView
On your discovery page, add a Google Maps widget and a ListView below it (or use a DraggableScrollableSheet for a bottom-sheet list overlaying the map). Store the query results in a Page State variable named nearbyItems (JSON list). When the page loads, call getUserLocation then queryNearbyItems, and store results in nearbyItems. In the Google Maps widget, add markers for each item using its GeoPoint coordinates. In the ListView, display each item as a card showing the item name, category, distance label, and a thumbnail image. Make list items tappable to navigate to the detail page and animate the map to center on that item's marker.
Expected result: The discovery page shows a map with pins for nearby items and a scrollable list below it sorted by distance. Each item card shows the exact distance in meters or kilometers.
Complete working example
1// Custom Action: saveListingWithGeohash2// Call this when creating or updating a listing with a location3import 'package:cloud_firestore/cloud_firestore.dart';4import 'package:geoflutterfire_plus/geoflutterfire_plus.dart';5import 'package:firebase_auth/firebase_auth.dart';67Future<String> saveListingWithGeohash({8 required String title,9 required String description,10 required double latitude,11 required double longitude,12 String? existingId,13}) async {14 final user = FirebaseAuth.instance.currentUser;15 if (user == null) throw Exception('Not authenticated');1617 // Generate geohash from coordinates18 final geoPoint = GeoPoint(latitude, longitude);19 final geoFirePoint = GeoFirePoint(geoPoint);20 final geohash = geoFirePoint.geohash;2122 final db = FirebaseFirestore.instance;23 final ref = existingId != null24 ? db.collection('listings').doc(existingId)25 : db.collection('listings').doc();2627 await ref.set(28 {29 'title': title,30 'description': description,31 'location': geoPoint, // Stored as Firestore GeoPoint32 'geohash': geohash, // 9-char string for proximity queries33 'owner_id': user.uid,34 'updated_at': FieldValue.serverTimestamp(),35 if (existingId == null)36 'created_at': FieldValue.serverTimestamp(),37 },38 SetOptions(merge: true),39 );4041 return ref.id;42}Common mistakes
Why it's a problem: Querying all documents from Firestore and filtering by distance on the client
How to avoid: Store a geohash string on every location document and use GeoFirePoint.queryBounds() to generate range queries that only fetch documents within the approximate search radius.
Why it's a problem: Storing only a GeoPoint without the geohash string
How to avoid: When creating or updating any document with a location, generate and store both a GeoPoint (location field) and a geohash string (geohash field) from the same coordinates.
Why it's a problem: Using LocationAccuracy.best for every location request
How to avoid: Use LocationAccuracy.medium for typical proximity searches. Cache the result for 5 minutes. Only use LocationAccuracy.best for turn-by-turn navigation or precise check-in features.
Why it's a problem: Not creating a Firestore composite index on the geohash field
How to avoid: Create a Firestore index on the geohash field with ascending order. Either follow the link in the error message or add it in the Firebase console under Firestore → Indexes before going to production.
Best practices
- Always store both a GeoPoint and a geohash string when saving location data — the GeoPoint is for exact distance calculation, the geohash is for database queries.
- Cache the user's location in App State for 5 minutes to avoid redundant GPS requests on every page render.
- Use LocationAccuracy.medium for proximity searches to balance speed and battery life.
- Run multiple geohash bound queries in parallel with Future.wait() rather than sequentially to minimize total query time.
- Add a Firestore ascending index on the geohash field before going to production to avoid query errors.
- Show a pre-permission explanation dialog before requesting location access — this significantly improves the permission grant rate.
- Implement a fallback for when GPS is unavailable: allow manual address entry that geocodes via Google Maps Geocoding API and uses those coordinates for proximity queries.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I am building a FlutterFlow app with location-based search. Explain how to implement efficient nearby search using Firestore GeoPoints and geohash strings so I don't have to download all documents to the client. Include how to generate geohash values and create range queries.
Add nearby listing search to my FlutterFlow app. Store each listing's location as a Firestore GeoPoint and geohash string. When the user opens the discovery page, get their GPS position with geolocator, query Firestore using geohash bounds for a 10km radius, sort results by distance, and display them in a ListView with a Google Maps widget showing pins.
Frequently asked questions
What is a geohash and why do I need it for Firestore location queries?
A geohash is a Base32 string that encodes latitude and longitude into a format where nearby locations have similar string prefixes. Firestore can range-query strings but cannot do radius queries on GeoPoints. By storing a geohash, you can use Firestore's startAt/endAt operators to retrieve documents within a geographic bounding box.
Why can I not just query Firestore by latitude range and longitude range?
You can query one range at a time in Firestore. A latitude range query would return all documents with the right latitude but any longitude — a horizontal band across the globe, not a nearby circle. Geohash encodes both dimensions into a single string that can be range-queried to approximate a geographic area.
How accurate are geohash-based proximity queries?
Geohash queries return a bounding box, not an exact circle. You get all documents within a rectangular area roughly matching your radius, plus a few outside the radius near the corners. This is why you must apply a second exact distance filter using Geolocator.distanceBetween() after fetching the geohash results.
Does this work with FlutterFlow's free plan?
No. Adding the geolocator and geoflutterfire_plus packages requires custom Dart code via FlutterFlow's Custom Actions feature, which requires code export — a Pro plan feature.
How do I handle the case where the user denies location permission?
Check the permission result from Geolocator.checkPermission(). If it returns LocationPermission.deniedForever, show a dialog explaining that location access was permanently denied and provide a button that calls Geolocator.openAppSettings() to open the device settings where the user can re-enable it.
What search radius should I default to?
For urban areas (restaurants, shops), 1-5 km works well. For rural or specialized searches (real estate, events), 20-50 km is more appropriate. Offer a slider and remember the user's last-used radius in App State.
How do I update a listing's geohash when its location changes?
When updating a listing, regenerate the geohash from the new coordinates using GeoFirePoint(GeoPoint(lat, lng)).geohash and write both the new location GeoPoint and new geohash to Firestore in the same document update. Old geohash values must be overwritten — if only the GeoPoint is updated the geohash remains stale and proximity queries return wrong results.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation