# How to Create Custom AR/VR Experiences in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Advanced
- Time required: 90-120 min
- Compatibility: FlutterFlow Pro+ (code export required for AR placement iOS/Android configuration)
- Last updated: March 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

### 2. 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.

```
import 'package:flutter/material.dart';
import 'package:flutter_3d_controller/flutter_3d_controller.dart';

class ProductViewer3D extends StatefulWidget {
  const ProductViewer3D({
    super.key,
    required this.modelUrl,
    required this.width,
    required this.height,
  });
  final String modelUrl;
  final double width;
  final double height;

  @override
  State<ProductViewer3D> createState() => _ProductViewer3DState();
}

class _ProductViewer3DState extends State<ProductViewer3D> {
  late final Flutter3DController _ctrl = Flutter3DController();

  @override
  void dispose() {
    _ctrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: widget.width,
      height: widget.height,
      child: Flutter3DViewer(
        controller: _ctrl,
        src: widget.modelUrl,
        enableTouch: true,
        onLoad: () => debugPrint('Model loaded: ${widget.modelUrl}'),
        onError: () => debugPrint('Model error: ${widget.modelUrl}'),
      ),
    );
  }
}
```

**Expected result:** The ProductViewer3D Custom Widget displays an interactive 3D model loaded from the Firebase Storage URL with pinch-to-zoom and rotation gestures.

### 3. 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.

```
import 'package:flutter/material.dart';
import 'package:ar_flutter_plugin/ar_flutter_plugin.dart';
import 'package:ar_flutter_plugin/datatypes/config_planedetection.dart';
import 'package:ar_flutter_plugin/datatypes/node_types.dart';
import 'package:ar_flutter_plugin/managers/ar_location_manager.dart';
import 'package:ar_flutter_plugin/managers/ar_session_manager.dart';
import 'package:ar_flutter_plugin/managers/ar_object_manager.dart';
import 'package:ar_flutter_plugin/managers/ar_anchor_manager.dart';
import 'package:ar_flutter_plugin/models/ar_node.dart';
import 'package:vector_math/vector_math_64.dart';

class ARPlacementView extends StatefulWidget {
  const ARPlacementView({
    super.key,
    required this.modelUrl,
    required this.width,
    required this.height,
  });
  final String modelUrl;
  final double width;
  final double height;

  @override
  State<ARPlacementView> createState() => _ARPlacementViewState();
}

class _ARPlacementViewState extends State<ARPlacementView> {
  ARSessionManager? arSessionManager;
  ARObjectManager? arObjectManager;

  @override
  void dispose() {
    arSessionManager?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: widget.width,
      height: widget.height,
      child: ARView(
        onARViewCreated: _onARViewCreated,
        planeDetectionConfig: PlaneDetectionConfig.horizontalAndVertical,
      ),
    );
  }

  void _onARViewCreated(
    ARSessionManager session,
    ARObjectManager objects,
    ARAnchorManager anchors,
    ARLocationManager location,
  ) {
    arSessionManager = session;
    arObjectManager = objects;
    arSessionManager!.onInitialize(
      showFeaturePoints: false,
      showPlanes: true,
    );
    arObjectManager!.onInitialize();
    arSessionManager!.onPlaneOrPointTap = _onPlaneTap;
  }

  Future<void> _onPlaneTap(List<ARHitTestResult> hits) async {
    if (hits.isEmpty) return;
    final hit = hits.first;
    final node = ARNode(
      type: NodeType.webGLB,
      uri: widget.modelUrl,
      scale: Vector3(0.2, 0.2, 0.2),
      position: Vector3(0, 0, 0),
      rotation: Vector4(1, 0, 0, 0),
    );
    await arObjectManager?.addNode(node, planeAnchor: hit);
  }
}
```

**Expected result:** On a physical device, the ARPlacementView shows camera feed with detected planes highlighted, and tapping a plane places the 3D model.

### 4. 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.

```
import 'package:flutter/material.dart';
import 'package:panorama_viewer/panorama_viewer.dart';

class PanoramaViewer360 extends StatelessWidget {
  const PanoramaViewer360({
    super.key,
    required this.imageUrl,
    required this.width,
    required this.height,
  });
  final String imageUrl;
  final double width;
  final double height;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: widget.width,
      height: widget.height,
      child: Panorama(
        child: Image.network(
          imageUrl,
          fit: BoxFit.cover,
          errorBuilder: (ctx, err, stack) =>
              const Center(child: Text('Failed to load panorama')),
        ),
      ),
    );
  }
}
```

**Expected result:** The PanoramaViewer360 widget displays a fully interactive 360-degree photo that users can drag to explore in all directions.

### 5. 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.

### 6. 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 code example

File: `panorama_viewer_widget.dart`

```dart
// ─── Custom Widget: PanoramaViewer360 ─────────────────────────────────────────
// Displays an interactive 360-degree panoramic photo.
// Required parameters: imageUrl (String), width (double), height (double)
// pubspec.yaml: panorama_viewer: ^1.0.3
//
// No native iOS/Android configuration needed.
// Works on iOS, Android, and Web.
// Use equirectangular images (2:1 aspect ratio) from Firebase Storage.

import 'package:flutter/material.dart';
import 'package:panorama_viewer/panorama_viewer.dart';

class PanoramaViewer360 extends StatefulWidget {
  const PanoramaViewer360({
    super.key,
    required this.imageUrl,
    required this.width,
    required this.height,
    this.showSensors = true,
  });

  final String imageUrl;
  final double width;
  final double height;
  final bool showSensors; // true = gyroscope-assisted panning

  @override
  State<PanoramaViewer360> createState() => _PanoramaViewer360State();
}

class _PanoramaViewer360State extends State<PanoramaViewer360> {
  bool _loading = true;
  bool _error = false;

  @override
  Widget build(BuildContext context) {
    if (widget.imageUrl.isEmpty) {
      return SizedBox(
        width: widget.width,
        height: widget.height,
        child: const Center(child: Text('No panorama URL provided.')),
      );
    }

    return SizedBox(
      width: widget.width,
      height: widget.height,
      child: Stack(
        children: [
          if (_error)
            const Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Icon(Icons.broken_image_outlined, size: 48),
                  SizedBox(height: 8),
                  Text('Failed to load panorama'),
                ],
              ),
            )
          else
            Panorama(
              sensitivity: 2.0,
              sensorControl: widget.showSensors
                  ? SensorControl.orientation
                  : SensorControl.none,
              child: Image.network(
                widget.imageUrl,
                fit: BoxFit.cover,
                loadingBuilder: (ctx, child, progress) {
                  if (progress == null) {
                    WidgetsBinding.instance.addPostFrameCallback(
                        (_) => setState(() => _loading = false));
                    return child;
                  }
                  return const SizedBox.shrink();
                },
                errorBuilder: (ctx, err, stack) {
                  WidgetsBinding.instance.addPostFrameCallback(
                      (_) => setState(() => _error = true));
                  return const SizedBox.shrink();
                },
              ),
            ),
          if (_loading && !_error)
            const Center(child: CircularProgressIndicator()),
        ],
      ),
    );
  }
}

// ─── pubspec.yaml ─────────────────────────────────────────────────────────────
// dependencies:
//   panorama_viewer: ^1.0.3
```

## Common mistakes

- **Hosting 3D model files as base64-encoded strings in Firestore document fields instead of Firebase Storage URLs** — Firestore documents have a 1MB size limit per document. Even a small GLB model (500KB-5MB) will exceed this limit as base64 (which inflates binary data by ~33%). The document write fails silently or throws a quota error, and the model never loads. Fix: 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.
- **Using ar_flutter_plugin without completing native iOS and Android configuration** — ar_flutter_plugin is a Flutter plugin with native code that requires Info.plist entries on iOS (NSCameraUsageDescription, ARKit capabilities) and AndroidManifest entries (CAMERA permission, ARCore meta-data). Without these, the plugin throws MissingPluginException on iOS and crashes on Android when the AR view initializes. Fix: 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.
- **Using non-equirectangular images for the PanoramaViewer360** — The panorama_viewer package expects images in equirectangular projection (2:1 aspect ratio, capturing the full 360x180-degree sphere). Regular photos, fisheye photos, or incorrectly projected images will display as distorted or stretched sphere surfaces. Fix: 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).
- **Setting AR model scale to Vector3(1, 1, 1) which places full-size (1 meter) models** — ARKit and ARCore use meters as their coordinate system unit. A model at scale 1.0 is rendered at real-world size — a chair model designed at 1-unit height will appear 1 meter tall, which is correct. But furniture models designed in centimeters will appear 100x too large, and small product models (jewelry, electronics) may appear microscopically small. Fix: 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.

## 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.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-custom-ar-vr-experiences-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-custom-ar-vr-experiences-in-flutterflow
