Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Implement Augmented Reality Navigation Within an App in FlutterFlow

Build AR navigation in FlutterFlow by combining geolocator for GPS position, flutter_compass for device heading, and a camera preview overlay. Calculate the bearing from current position to each point of interest using the haversine formula, subtract the device heading to get relative angle, and render directional arrows and distance labels over the live camera feed using a Stack widget.

What you'll learn

  • How to get GPS position and compass heading using geolocator and flutter_compass
  • How to calculate bearing between two GPS coordinates using the haversine formula
  • How to overlay directional arrows over a live camera preview using a Stack widget
  • Why GPS-only AR fails indoors and what alternatives exist
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner11 min read50-65 minFlutterFlow Pro+ (code export required)March 2026RapidDev Engineering Team
TL;DR

Build AR navigation in FlutterFlow by combining geolocator for GPS position, flutter_compass for device heading, and a camera preview overlay. Calculate the bearing from current position to each point of interest using the haversine formula, subtract the device heading to get relative angle, and render directional arrows and distance labels over the live camera feed using a Stack widget.

AR Wayfinding: Camera Overlay with GPS and Compass

Augmented reality navigation overlays directional guidance on a live camera feed. In FlutterFlow, this is built as a Custom Widget that combines three data streams: the device's GPS position (geolocator package), the compass heading (flutter_compass package), and a camera preview (camera package). The bearing from the user's current GPS position to each point of interest is calculated server-side using the haversine formula. Subtracting the device's compass heading from the bearing gives the angle at which to render the directional arrow relative to screen center. The most important limitation to communicate clearly is that GPS accuracy is 5-15 metres outdoors and essentially useless indoors — this approach works well for outdoor landmarks, parking spots, or campus navigation, but not for indoor navigation like mall stores or hospital wards.

Prerequisites

  • FlutterFlow Pro plan with code export enabled
  • geolocator, flutter_compass, and camera packages in pubspec.yaml
  • Firestore collection with POI (point of interest) documents containing lat/lng fields
  • iOS: NSLocationWhenInUseUsageDescription, NSCameraUsageDescription in Info.plist
  • Android: ACCESS_FINE_LOCATION and CAMERA permissions in AndroidManifest.xml

Step-by-step guide

1

Store points of interest in Firestore with GPS coordinates

Create a Firestore collection named poi (points of interest). Each document contains: name (String), description (String), latitude (Number), longitude (Number), category (String), thumbnail_url (String), and distance_meters (Number — calculated and updated by the app). In FlutterFlow, create this collection schema in the Firestore panel. Populate it with test points in your area using their precise GPS coordinates (find these in Google Maps by right-clicking a location). Add a GeoPoint field type for the coordinates so Firestore can do geo-proximity queries in the future. For the AR overlay, the app queries all POIs or filters by category, then calculates bearing and distance to each one client-side using the user's current GPS position.

Expected result: Firestore shows a poi collection with at least 3 test documents with valid latitude and longitude values near your test location.

2

Request location and camera permissions on app launch

Create a Custom Action named requestPermissions that calls Geolocator.requestPermission() and permission_handler's Permission.camera.request(). Check the result — if either permission is denied, set a page state variable permissionsDenied to true and show a permissions explanation screen instead of the AR view. For iOS, add the required permission descriptions to Info.plist via FlutterFlow's Custom Info Plist: NSLocationWhenInUseUsageDescription ('This app uses your location to show nearby points of interest'), NSCameraUsageDescription ('Camera is used to show AR navigation overlays'). Call requestPermissions in the AR page's On Page Load action before initialising the camera.

custom_actions/request_permissions.dart
1import 'package:geolocator/geolocator.dart';
2import 'package:permission_handler/permission_handler.dart';
3
4Future<bool> requestPermissions() async {
5 // Check if location services are enabled
6 final serviceEnabled = await Geolocator.isLocationServiceEnabled();
7 if (!serviceEnabled) return false;
8
9 var locationStatus = await Geolocator.checkPermission();
10 if (locationStatus == LocationPermission.denied) {
11 locationStatus = await Geolocator.requestPermission();
12 }
13 if (locationStatus == LocationPermission.deniedForever) return false;
14
15 final cameraStatus = await Permission.camera.request();
16 return locationStatus != LocationPermission.denied &&
17 cameraStatus.isGranted;
18}

Expected result: On first launch of the AR page, iOS shows location and camera permission dialogs; granting both proceeds to the camera view.

3

Build the GPS and compass data stream custom widget

Create a Custom Widget named ARNavigationWidget that combines three streams: Geolocator.getPositionStream() for real-time GPS, FlutterCompass.events for compass heading, and the camera package's CameraController for the live preview. In the widget's build method, use a Stack with the camera preview filling the entire frame as the bottom layer. The top layer is a CustomPaint widget that receives the current position, compass heading, and a list of POI objects, and draws bearing arrows and distance labels for each POI relative to the compass heading. This widget is the entire AR experience — embed it on a full-screen FlutterFlow page.

Expected result: The AR widget shows a live camera feed; the CustomPaint layer renders (initially static) overlays that will be animated in the next step.

4

Implement bearing calculation and directional arrows

The core math converts GPS coordinates and compass heading into screen positions for each POI. The haversine bearing formula gives the compass direction from point A to point B. Subtract the device's current compass heading from each POI's bearing to get the angle relative to the screen's forward direction. Convert this relative angle to an x-position on screen: x = screenWidth/2 + sin(relativeAngle) * screenWidth/2. Cap x to the screen edges so off-screen POIs show arrows pointing left or right. Draw a triangle arrow at position (x, screenHeight * 0.6) pointing up, with the POI name and distance label below it. Update the CustomPaint on every GPS and compass event by calling setState.

custom_functions/bearing_calculator.dart
1import 'dart:math';
2
3/// Calculate bearing from point A to point B in degrees (0-360).
4double calculateBearing(double lat1, double lon1, double lat2, double lon2) {
5 final dLon = _toRad(lon2 - lon1);
6 final y = sin(dLon) * cos(_toRad(lat2));
7 final x = cos(_toRad(lat1)) * sin(_toRad(lat2)) -
8 sin(_toRad(lat1)) * cos(_toRad(lat2)) * cos(dLon);
9 final bearing = atan2(y, x) * 180 / pi;
10 return (bearing + 360) % 360;
11}
12
13/// Calculate distance in metres between two GPS coordinates.
14double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
15 const R = 6371000.0; // Earth radius in metres
16 final dLat = _toRad(lat2 - lat1);
17 final dLon = _toRad(lon2 - lon1);
18 final a = sin(dLat / 2) * sin(dLat / 2) +
19 cos(_toRad(lat1)) * cos(_toRad(lat2)) * sin(dLon / 2) * sin(dLon / 2);
20 return R * 2 * atan2(sqrt(a), sqrt(1 - a));
21}
22
23double _toRad(double deg) => deg * pi / 180;

Expected result: Rotating the device changes the angles of the POI arrows — arrows point in the correct compass direction toward each POI.

5

Add distance filtering and POI detail cards

Add a distance filter slider to the AR page (a Slider widget in a bottom panel) that controls maximum display distance (100m to 5km). When the slider changes, update a page state variable maxDistance and rebuild the overlay with only POIs within that distance. Add a tap gesture on each POI arrow that shows a bottom sheet with the POI's full details: name, description, thumbnail image, exact distance, and a 'Get Directions' button that launches Google Maps with the POI coordinates. To avoid cluttering the AR view with too many overlapping arrows, sort POIs by distance and limit display to the 5 nearest. For POIs farther than 1km, show a compact distance label instead of the full arrow widget.

Expected result: Adjusting the distance slider shows or hides POIs; tapping an arrow opens a detail sheet with a directions button.

Complete working example

custom_widgets/ar_navigation_widget.dart
1import 'dart:math';
2import 'package:flutter/material.dart';
3import 'package:camera/camera.dart';
4import 'package:geolocator/geolocator.dart';
5import 'package:flutter_compass/flutter_compass.dart';
6
7class POI {
8 final String name;
9 final double latitude;
10 final double longitude;
11 const POI({required this.name, required this.latitude, required this.longitude});
12}
13
14class ARNavigationWidget extends StatefulWidget {
15 final List<POI> pois;
16 const ARNavigationWidget({super.key, required this.pois});
17 @override
18 State<ARNavigationWidget> createState() => _ARNavigationWidgetState();
19}
20
21class _ARNavigationWidgetState extends State<ARNavigationWidget> {
22 CameraController? _cameraController;
23 double _heading = 0;
24 double _userLat = 0;
25 double _userLon = 0;
26 final List<double> _headingBuffer = [];
27
28 @override
29 void initState() {
30 super.initState();
31 _initCamera();
32 _startStreams();
33 }
34
35 Future<void> _initCamera() async {
36 final cameras = await availableCameras();
37 if (cameras.isEmpty) return;
38 _cameraController = CameraController(cameras.first, ResolutionPreset.medium);
39 await _cameraController!.initialize();
40 if (mounted) setState(() {});
41 }
42
43 void _startStreams() {
44 Geolocator.getPositionStream(
45 locationSettings: const LocationSettings(accuracy: LocationAccuracy.high),
46 ).listen((pos) => setState(() { _userLat = pos.latitude; _userLon = pos.longitude; }));
47
48 FlutterCompass.events?.listen((event) {
49 if (event.heading == null) return;
50 _headingBuffer.add(event.heading!);
51 if (_headingBuffer.length > 5) _headingBuffer.removeAt(0);
52 setState(() => _heading = _headingBuffer.reduce((a, b) => a + b) / _headingBuffer.length);
53 });
54 }
55
56 double _bearing(double lat2, double lon2) {
57 final dLon = (lon2 - _userLon) * pi / 180;
58 final y = sin(dLon) * cos(lat2 * pi / 180);
59 final x = cos(_userLat * pi / 180) * sin(lat2 * pi / 180) -
60 sin(_userLat * pi / 180) * cos(lat2 * pi / 180) * cos(dLon);
61 return (atan2(y, x) * 180 / pi + 360) % 360;
62 }
63
64 double _distance(double lat2, double lon2) {
65 final dLat = (lat2 - _userLat) * pi / 180;
66 final dLon = (lon2 - _userLon) * pi / 180;
67 final a = sin(dLat/2)*sin(dLat/2) +
68 cos(_userLat*pi/180)*cos(lat2*pi/180)*sin(dLon/2)*sin(dLon/2);
69 return 6371000 * 2 * atan2(sqrt(a), sqrt(1 - a));
70 }
71
72 @override
73 Widget build(BuildContext context) {
74 if (_cameraController == null || !_cameraController!.value.isInitialized) {
75 return const Center(child: CircularProgressIndicator());
76 }
77 final size = MediaQuery.of(context).size;
78 return Stack(children: [
79 SizedBox.expand(child: CameraPreview(_cameraController!)),
80 CustomPaint(
81 size: size,
82 painter: _AROverlayPainter(
83 pois: widget.pois, heading: _heading,
84 bearingFn: _bearing, distanceFn: _distance,
85 ),
86 ),
87 ]);
88 }
89
90 @override
91 void dispose() {
92 _cameraController?.dispose();
93 super.dispose();
94 }
95}
96
97class _AROverlayPainter extends CustomPainter {
98 final List<POI> pois;
99 final double heading;
100 final double Function(double, double) bearingFn;
101 final double Function(double, double) distanceFn;
102 _AROverlayPainter({required this.pois, required this.heading,
103 required this.bearingFn, required this.distanceFn});
104 @override
105 void paint(Canvas canvas, Size size) {
106 final paint = Paint()..color = Colors.white..strokeWidth = 2;
107 for (final poi in pois) {
108 final dist = distanceFn(poi.latitude, poi.longitude);
109 if (dist > 500) continue;
110 final bearing = bearingFn(poi.latitude, poi.longitude);
111 final rel = ((bearing - heading + 360) % 360);
112 final angle = rel > 180 ? rel - 360 : rel;
113 final x = size.width / 2 + sin(angle * pi / 180) * size.width / 2;
114 final clampedX = x.clamp(20.0, size.width - 20);
115 final y = size.height * 0.6;
116 final path = Path()
117 ..moveTo(clampedX, y - 20)
118 ..lineTo(clampedX - 10, y)
119 ..lineTo(clampedX + 10, y)
120 ..close();
121 canvas.drawPath(path, paint);
122 final tp = TextPainter(
123 text: TextSpan(text: '${poi.name}\n${dist.toInt()}m',
124 style: const TextStyle(color: Colors.white, fontSize: 12)),
125 textDirection: TextDirection.ltr,
126 )..layout();
127 tp.paint(canvas, Offset(clampedX - tp.width / 2, y + 5));
128 }
129 }
130 @override
131 bool shouldRepaint(_AROverlayPainter old) => true;
132}

Common mistakes when implementing Augmented Reality Navigation Within an App in FlutterFlow

Why it's a problem: Relying solely on GPS for indoor AR navigation

How to avoid: For outdoor navigation (parks, campuses, city landmarks), GPS works well. For indoor navigation, use BLE beacons (with flutter_blue_plus package) or Wi-Fi fingerprinting for positioning. Accept the limitation and document it clearly in your app's onboarding.

Why it's a problem: Using raw compass readings directly without smoothing

How to avoid: Apply a simple low-pass filter: average the last 5 compass readings before using the heading value. This smooths the jitter while still responding to genuine direction changes.

Why it's a problem: Not handling the case where GPS permission is denied

How to avoid: Check permission status before starting location streams. Show a permissions explanation screen with a button to open app settings if permission is permanently denied.

Best practices

  • Always check and request location and camera permissions before initialising the AR widget.
  • Apply low-pass filtering to compass readings (average last 5 values) to prevent jittery arrow rendering.
  • Limit the number of visible POI overlays to 5-8 at once to avoid a cluttered AR view.
  • Show distance labels prominently — users want to know how far away a POI is more than its exact direction.
  • Include a 2D map fallback view for users who prefer traditional navigation.
  • Clearly document GPS-only limitations: this works outdoors, not indoors.
  • Cache the POI list in App State to avoid re-fetching Firestore on every AR page visit.
  • Test on a real device — the camera and compass cannot be tested in the FlutterFlow web preview or iOS Simulator.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I am building an AR navigation feature in FlutterFlow using a Custom Widget. The widget uses the geolocator package for GPS and flutter_compass for heading. It needs to render directional arrows over a camera preview for nearby points of interest stored in Firestore with latitude and longitude fields. Write the complete Dart code for: (1) the haversine bearing and distance calculation functions, (2) a CustomPainter that draws arrows and distance labels at the correct screen positions based on GPS bearing and compass heading, and (3) a low-pass filter for compass heading values.

FlutterFlow Prompt

In FlutterFlow I have an AR navigation page with a full-screen ARNavigationWidget custom widget. I want to add a category filter row above the camera view. Build the FlutterFlow widget structure that shows a horizontal scrollable Row of filter chips (one per POI category), each chip updates a page state variable selectedCategory, and the custom widget receives the filtered list of POIs from a Firestore query filtered by the selected category.

Frequently asked questions

Does this AR navigation approach use ARKit or ARCore?

No. This tutorial uses a simpler GPS + compass overlay approach — it does not use ARKit (iOS) or ARCore (Android) plane detection, world anchors, or depth sensing. It is essentially a camera preview with a data overlay calculated from GPS and compass bearings. True ARKit/ARCore integration for navigation requires the ar_flutter_plugin package, which is significantly more complex and requires code export.

How accurate are the directional arrows?

Outdoors with a clear sky view, GPS accuracy is 3-5 metres and compass accuracy is 5-10 degrees. This means arrows point in the roughly correct direction but are not precise enough for guiding someone through a doorway. They work well for directing users toward buildings, parking areas, or outdoor landmarks from 20+ metres away.

Why does the compass heading drift when I move the phone near metal objects?

The device compass uses a magnetometer, which is affected by magnetic fields from metal objects, electronics, and even the device's own speaker and battery. This is called magnetic interference. The low-pass filter helps reduce noise but cannot eliminate magnetic distortion. For better compass accuracy, calibrate the device by moving it in a figure-8 pattern, and move away from large metal surfaces.

Can I use this for indoor navigation in a shopping mall?

No — GPS does not work reliably indoors. For indoor navigation, you need a different positioning system: BLE beacons (small Bluetooth transmitters placed around the building), Wi-Fi fingerprinting, or ultra-wideband (UWB) positioning. Building an indoor AR navigation system with beacons using flutter_blue_plus is possible in FlutterFlow via Custom Actions, but it requires the venue owner to install and maintain the beacon infrastructure.

How do I add voice directions to the AR navigation?

Use the flutter_tts package in a Custom Action. When the bearing to the nearest POI changes significantly (more than 30 degrees) or when the user gets within 50 metres of a POI, trigger a text-to-speech announcement like 'The coffee shop is 45 metres ahead on your right.' Call the speech action from the AR widget's update callback with appropriate debouncing to avoid speaking too frequently.

Does the camera drain battery quickly?

Yes, keeping the camera active continuously drains battery significantly (50-80% faster than normal use). Inform users of this in an onboarding message. Optionally add an energy-saving mode that pauses the camera and shows only a compass rose when the user is not actively looking at the AR view (detect this using the accelerometer to determine if the phone is held up or down).

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.