FlutterFlow supports three types of AR/VR experiences through Custom Widgets: product 3D viewer (flutter_3d_controller — easiest, no native setup), AR object placement (ar_flutter_plugin — native ARKit/ARCore, requires code export and iOS/Android config), and 360-degree tour (panorama_viewer — works on all devices, no AR hardware needed). Choose based on your use case and target audience.
Three AR/VR Project Types You Can Build in FlutterFlow
AR and VR in FlutterFlow are built entirely through Custom Widgets — there are no drag-and-drop AR components in the widget palette. The good news is that Flutter's package ecosystem has mature solutions for each major AR/VR use case. This guide covers three distinct project types based on real-world commercial needs. Product Viewer apps (e-commerce, real estate, furniture) need interactive 3D models that users can rotate and zoom — flutter_3d_controller is the right tool with minimal setup. AR Placement apps (interior design, retail try-on, gaming) need to anchor virtual objects to real-world surfaces — ar_flutter_plugin provides ARKit (iOS) and ARCore (Android) wrappers. 360-degree Tour apps (real estate walkthrough, travel, education) need immersive panoramic viewing — the panorama_viewer package works on all devices without special hardware.
Prerequisites
- FlutterFlow Pro plan (Custom Widgets and code export required for AR configuration)
- 3D model files (GLB format) hosted on Firebase Storage for the product viewer
- 360-degree panorama images (equirectangular JPEG) hosted on Firebase Storage for the tour
- Physical iOS or Android device for AR placement testing
- Basic understanding of FlutterFlow Custom Widgets and the Custom Code panel
Step-by-step guide
Set up Firebase Storage for 3D assets
Set up Firebase Storage for 3D assets
All three AR/VR project types load their assets (3D models, panorama images) from URLs at runtime rather than bundling them in the app. Open Firebase Console → Storage and create a folder named 'ar-assets'. Upload your GLB model files (for product viewer and AR placement) and equirectangular panorama JPEG files (for 360 tours) to this folder. After uploading, click each file, go to the 'Details' tab, and copy the download URL. Paste these URLs into Firestore documents in a collection like 'products' or 'locations' — your FlutterFlow pages will query this collection and pass the asset URL to the Custom Widget. Set Firebase Storage rules to allow read access for authenticated users.
Expected result: 3D model GLB files and panorama JPEG files are uploaded to Firebase Storage with download URLs stored in Firestore documents.
Build the Product Viewer with flutter_3d_controller
Build the Product Viewer with flutter_3d_controller
Open the Custom Code panel and add 'flutter_3d_controller: ^2.0.0' to pubspec.yaml. Tap Get Packages. Create a Custom Widget named 'ProductViewer3D' with parameters: modelUrl (String), width (double), height (double). Write the widget as a StatefulWidget that creates a Flutter3DController, returns a Flutter3DViewer with enableTouch true, and disposes the controller in dispose(). Add the widget to your product detail page and bind the modelUrl parameter to the Firestore document's model URL field. Users can then rotate, zoom, and inspect the 3D model with touch gestures. This path has no iOS/Android native configuration — it works immediately in Run Mode.
1import 'package:flutter/material.dart';2import 'package:flutter_3d_controller/flutter_3d_controller.dart';34class ProductViewer3D extends StatefulWidget {5 const ProductViewer3D({6 super.key,7 required this.modelUrl,8 required this.width,9 required this.height,10 });11 final String modelUrl;12 final double width;13 final double height;1415 @override16 State<ProductViewer3D> createState() => _ProductViewer3DState();17}1819class _ProductViewer3DState extends State<ProductViewer3D> {20 late final Flutter3DController _ctrl = Flutter3DController();2122 @override23 void dispose() {24 _ctrl.dispose();25 super.dispose();26 }2728 @override29 Widget build(BuildContext context) {30 return SizedBox(31 width: widget.width,32 height: widget.height,33 child: Flutter3DViewer(34 controller: _ctrl,35 src: widget.modelUrl,36 enableTouch: true,37 onLoad: () => debugPrint('Model loaded: ${widget.modelUrl}'),38 onError: () => debugPrint('Model error: ${widget.modelUrl}'),39 ),40 );41 }42}Expected result: The ProductViewer3D Custom Widget displays an interactive 3D model loaded from the Firebase Storage URL with pinch-to-zoom and rotation gestures.
Build the AR Placement view with ar_flutter_plugin
Build the AR Placement view with ar_flutter_plugin
Add 'ar_flutter_plugin: ^0.7.3' to pubspec.yaml and tap Get Packages. Create a Custom Widget named 'ARPlacementView' with parameters: modelUrl (String), width (double), height (double). The widget uses ARView from ar_flutter_plugin — it shows the device camera feed, detects horizontal planes (floors, tables), and allows tapping to place a 3D model on a detected surface. Export your FlutterFlow project code (Pro plan required) and complete the native configuration: add NSCameraUsageDescription to ios/Runner/Info.plist, add ARKit to the Podfile, add CAMERA permission and ar.required meta-data to android/app/src/main/AndroidManifest.xml. Then build and run from Xcode/Android Studio.
1import 'package:flutter/material.dart';2import 'package:ar_flutter_plugin/ar_flutter_plugin.dart';3import 'package:ar_flutter_plugin/datatypes/config_planedetection.dart';4import 'package:ar_flutter_plugin/datatypes/node_types.dart';5import 'package:ar_flutter_plugin/managers/ar_location_manager.dart';6import 'package:ar_flutter_plugin/managers/ar_session_manager.dart';7import 'package:ar_flutter_plugin/managers/ar_object_manager.dart';8import 'package:ar_flutter_plugin/managers/ar_anchor_manager.dart';9import 'package:ar_flutter_plugin/models/ar_node.dart';10import 'package:vector_math/vector_math_64.dart';1112class ARPlacementView extends StatefulWidget {13 const ARPlacementView({14 super.key,15 required this.modelUrl,16 required this.width,17 required this.height,18 });19 final String modelUrl;20 final double width;21 final double height;2223 @override24 State<ARPlacementView> createState() => _ARPlacementViewState();25}2627class _ARPlacementViewState extends State<ARPlacementView> {28 ARSessionManager? arSessionManager;29 ARObjectManager? arObjectManager;3031 @override32 void dispose() {33 arSessionManager?.dispose();34 super.dispose();35 }3637 @override38 Widget build(BuildContext context) {39 return SizedBox(40 width: widget.width,41 height: widget.height,42 child: ARView(43 onARViewCreated: _onARViewCreated,44 planeDetectionConfig: PlaneDetectionConfig.horizontalAndVertical,45 ),46 );47 }4849 void _onARViewCreated(50 ARSessionManager session,51 ARObjectManager objects,52 ARAnchorManager anchors,53 ARLocationManager location,54 ) {55 arSessionManager = session;56 arObjectManager = objects;57 arSessionManager!.onInitialize(58 showFeaturePoints: false,59 showPlanes: true,60 );61 arObjectManager!.onInitialize();62 arSessionManager!.onPlaneOrPointTap = _onPlaneTap;63 }6465 Future<void> _onPlaneTap(List<ARHitTestResult> hits) async {66 if (hits.isEmpty) return;67 final hit = hits.first;68 final node = ARNode(69 type: NodeType.webGLB,70 uri: widget.modelUrl,71 scale: Vector3(0.2, 0.2, 0.2),72 position: Vector3(0, 0, 0),73 rotation: Vector4(1, 0, 0, 0),74 );75 await arObjectManager?.addNode(node, planeAnchor: hit);76 }77}Expected result: On a physical device, the ARPlacementView shows camera feed with detected planes highlighted, and tapping a plane places the 3D model.
Build the 360-degree tour viewer with panorama_viewer
Build the 360-degree tour viewer with panorama_viewer
Add 'panorama_viewer: ^1.0.3' to pubspec.yaml and tap Get Packages. Create a Custom Widget named 'PanoramaViewer360' with parameters: imageUrl (String), width (double), height (double). The panorama_viewer package takes an equirectangular image (a 2:1 ratio panorama photo) and renders it as an interactive spherical environment — users drag to look around in all directions. The widget uses an Image.network to load the image from your Firebase Storage URL. This path requires no native configuration and works on iOS, Android, and Web. It is ideal for virtual property tours, destination previews, and educational environments.
1import 'package:flutter/material.dart';2import 'package:panorama_viewer/panorama_viewer.dart';34class PanoramaViewer360 extends StatelessWidget {5 const PanoramaViewer360({6 super.key,7 required this.imageUrl,8 required this.width,9 required this.height,10 });11 final String imageUrl;12 final double width;13 final double height;1415 @override16 Widget build(BuildContext context) {17 return SizedBox(18 width: widget.width,19 height: widget.height,20 child: Panorama(21 child: Image.network(22 imageUrl,23 fit: BoxFit.cover,24 errorBuilder: (ctx, err, stack) =>25 const Center(child: Text('Failed to load panorama')),26 ),27 ),28 );29 }30}Expected result: The PanoramaViewer360 widget displays a fully interactive 360-degree photo that users can drag to explore in all directions.
Wire the AR widgets to Firestore data in FlutterFlow
Wire the AR widgets to Firestore data in FlutterFlow
Add the Custom Widget to a FlutterFlow page that queries Firestore for the relevant content. For a product page, add a Backend Query on the page to fetch the product document by its ID. Then drag your ProductViewer3D or ARPlacementView Custom Widget onto the page and bind the modelUrl parameter to the product document's 'modelUrl' Firestore field. Add a loading state to show a CircularProgressIndicator while the query runs. For the panorama tour, add a Repeating Group with a Backend Query on the 'locations' collection and place the PanoramaViewer360 widget inside each list item, binding imageUrl to the location's 'panoramaUrl' field.
Expected result: The AR/VR widget is bound to Firestore data, loads the correct model or panorama URL for each product or location, and shows a loading indicator while assets are fetched.
Test all three experience types on physical devices
Test all three experience types on physical devices
Run Mode is required for all three paths. For ProductViewer3D and PanoramaViewer360, connect any recent iOS or Android phone via USB and click Run — these work on all devices. For ARPlacementView, you specifically need an ARKit-compatible iPhone (6s or later) or ARCore-compatible Android — older budget devices may not support AR. On the AR view, walk around a flat surface until the plane detection grid appears. Then tap the grid to place the model. Test scaling, rotation, and repositioning. Check the DevTools Logging tab for any ar_flutter_plugin initialization errors or model loading failures.
Expected result: All three experience types load correctly on physical devices, models render from Firebase Storage URLs, and AR plane detection and model placement work on AR-capable devices.
Complete working example
1// ─── Custom Widget: PanoramaViewer360 ─────────────────────────────────────────2// Displays an interactive 360-degree panoramic photo.3// Required parameters: imageUrl (String), width (double), height (double)4// pubspec.yaml: panorama_viewer: ^1.0.35//6// No native iOS/Android configuration needed.7// Works on iOS, Android, and Web.8// Use equirectangular images (2:1 aspect ratio) from Firebase Storage.910import 'package:flutter/material.dart';11import 'package:panorama_viewer/panorama_viewer.dart';1213class PanoramaViewer360 extends StatefulWidget {14 const PanoramaViewer360({15 super.key,16 required this.imageUrl,17 required this.width,18 required this.height,19 this.showSensors = true,20 });2122 final String imageUrl;23 final double width;24 final double height;25 final bool showSensors; // true = gyroscope-assisted panning2627 @override28 State<PanoramaViewer360> createState() => _PanoramaViewer360State();29}3031class _PanoramaViewer360State extends State<PanoramaViewer360> {32 bool _loading = true;33 bool _error = false;3435 @override36 Widget build(BuildContext context) {37 if (widget.imageUrl.isEmpty) {38 return SizedBox(39 width: widget.width,40 height: widget.height,41 child: const Center(child: Text('No panorama URL provided.')),42 );43 }4445 return SizedBox(46 width: widget.width,47 height: widget.height,48 child: Stack(49 children: [50 if (_error)51 const Center(52 child: Column(53 mainAxisSize: MainAxisSize.min,54 children: [55 Icon(Icons.broken_image_outlined, size: 48),56 SizedBox(height: 8),57 Text('Failed to load panorama'),58 ],59 ),60 )61 else62 Panorama(63 sensitivity: 2.0,64 sensorControl: widget.showSensors65 ? SensorControl.orientation66 : SensorControl.none,67 child: Image.network(68 widget.imageUrl,69 fit: BoxFit.cover,70 loadingBuilder: (ctx, child, progress) {71 if (progress == null) {72 WidgetsBinding.instance.addPostFrameCallback(73 (_) => setState(() => _loading = false));74 return child;75 }76 return const SizedBox.shrink();77 },78 errorBuilder: (ctx, err, stack) {79 WidgetsBinding.instance.addPostFrameCallback(80 (_) => setState(() => _error = true));81 return const SizedBox.shrink();82 },83 ),84 ),85 if (_loading && !_error)86 const Center(child: CircularProgressIndicator()),87 ],88 ),89 );90 }91}9293// ─── pubspec.yaml ─────────────────────────────────────────────────────────────94// dependencies:95// panorama_viewer: ^1.0.3Common mistakes when creating Custom AR/VR Experiences in FlutterFlow
Why it's a problem: Hosting 3D model files as base64-encoded strings in Firestore document fields instead of Firebase Storage URLs
How to avoid: Upload 3D model files to Firebase Storage (not Firestore) and store only the string download URL in the Firestore document. Firebase Storage is designed for large binary files, has CDN caching, and has no per-file size limit on Blaze plan.
Why it's a problem: Using ar_flutter_plugin without completing native iOS and Android configuration
How to avoid: Export your FlutterFlow project (Pro plan) and follow the ar_flutter_plugin setup guide on pub.dev exactly. The Podfile, Info.plist, and AndroidManifest modifications are all documented there.
Why it's a problem: Using non-equirectangular images for the PanoramaViewer360
How to avoid: Use dedicated 360-degree camera equipment or apps (like Google Street View, Ricoh Theta) to capture equirectangular panoramas. The resulting images typically have a 2:1 aspect ratio (e.g., 4096x2048 pixels).
Why it's a problem: Setting AR model scale to Vector3(1, 1, 1) which places full-size (1 meter) models
How to avoid: Start with Vector3(0.2, 0.2, 0.2) as a default scale and add pinch-to-scale gesture handling to let users adjust. Document the expected scale of each model in your Firestore document (e.g., 'arScale': 0.15) and read it as a parameter.
Best practices
- Start with the simplest path that meets your requirements — PanoramaViewer360 (no native config) before ar_flutter_plugin (requires code export and native setup).
- Store AR asset metadata (model URL, default scale, thumbnail URL, supported platforms) in Firestore so you can update assets without an app update.
- Show a loading indicator while assets download from Firebase Storage — 3D model and panorama image downloads can take several seconds on slower connections.
- Provide a non-AR fallback (2D images or regular 3D viewer) for devices that do not support ARKit/ARCore — check availability before launching the AR session.
- Limit AR sessions to user-initiated actions (tap 'View in AR' button) rather than launching AR automatically on page load — AR consumes significant battery and CPU.
- Test on the oldest and least-powerful supported device in your target audience — AR performance degrades sharply on older hardware, and what runs smoothly on a flagship phone may be unusable on a budget device.
- Compress panorama images to under 4MB and GLB models to under 5MB for acceptable loading times on mobile networks — large assets are the number one cause of poor AR experience ratings.
- Add a brief onboarding overlay when AR launches for the first time explaining how to detect surfaces and place objects — AR UX is still unfamiliar to a significant portion of mobile users.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building a FlutterFlow app for a furniture store and want to add an AR feature where users can place furniture in their room using their phone camera. I need a Flutter Custom Widget that uses ar_flutter_plugin to show the camera feed, detect horizontal planes, and let users tap to place a 3D model from a GLB URL parameter. Please write the complete StatefulWidget class including the ARView setup, onARViewCreated handler, and tap-to-place logic. The widget should accept parameters: modelUrl (String), width (double), height (double).
Write a FlutterFlow Custom Widget called 'PanoramaViewer360' using the panorama_viewer ^1.0.3 package. Parameters: imageUrl (String, required), width (double, required), height (double, required). The widget should show a loading indicator while the image loads, display an error state with an icon if loading fails, and use SensorControl.orientation for gyroscope-assisted panning. Format as a complete StatefulWidget ready to paste into the FlutterFlow Custom Code editor.
Frequently asked questions
What is the difference between the three AR/VR approaches in this guide?
Product Viewer (flutter_3d_controller) shows a 3D model that users rotate and zoom — no real-world camera integration, works on all devices. AR Placement (ar_flutter_plugin) uses the camera to detect real surfaces and places a virtual 3D model on them — requires ARKit/ARCore hardware. 360-degree Tour (panorama_viewer) wraps a spherical panorama photo into an immersive viewer — no special hardware needed.
Can I use all three AR/VR types in the same app?
Yes. Each type is a separate Custom Widget. You can have a product list page that shows the ProductViewer3D on product detail, an 'View in AR' button on that same page that opens the ARPlacementView, and a separate 'Virtual Tour' page that uses PanoramaViewer360. They are independent widgets with different package dependencies — just add all required packages to pubspec.yaml.
Do I need to pay for AR features in FlutterFlow?
The Flutter packages (ar_flutter_plugin, flutter_3d_controller, panorama_viewer) are free open-source packages. FlutterFlow Pro plan is required for Custom Widgets and code export (needed for native AR configuration). Firebase Storage costs for hosting 3D assets are minimal (free tier includes 5GB storage). There are no AR-specific per-use fees.
How do I get 3D models for product visualization?
Options include: hiring a 3D artist to create models from product photos or CAD files ($50-500 per model), using free model repositories (Google Poly archive, Sketchfab free section), using Polycam or Luma AI to scan physical objects with your iPhone, or using AI-based 3D generation tools like Meshy.ai. Always export in GLB format and compress with Draco.
Will the AR features work on the web version of my FlutterFlow app?
flutter_3d_controller works on Flutter Web. ar_flutter_plugin does NOT support Flutter Web — it requires native ARKit/ARCore. panorama_viewer works on Flutter Web. For web-based AR, use the WebAR approach: load an A-Frame or 8th Wall experience in a WebView widget.
How large should my panorama images be for good quality?
For mobile apps, 4096x2048 pixels (4K equirectangular) provides excellent quality at a manageable file size (typically 2-5MB as a JPEG). Anything smaller than 2048x1024 will look blurry when the user zooms in. For web delivery, use 8192x4096 (8K) if bandwidth allows. Compress panorama JPEGs to quality level 85 — this saves 40-50% file size with minimal visible quality loss.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation