# How to Create a Virtual Store with a 3D Product Viewer in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 55-70 min
- Compatibility: FlutterFlow Pro+ (code export required for custom package integration)
- Last updated: March 2026

## TL;DR

Build a virtual store in FlutterFlow by storing products in Firestore with a model3dUrl field pointing to .glb files in Firebase Storage. A Custom Widget wraps the model_viewer Flutter package to render interactive 3D models with rotate, zoom, and pan controls. Color variant chips swap the model and textures. An AR try-on button uses model_viewer's built-in AR mode on supported devices.

## E-Commerce with Interactive 3D and AR Product Views

Standard product images show one angle. A 3D product viewer lets customers rotate, zoom, and inspect a product from every side — dramatically reducing return rates for items like furniture, shoes, and electronics. This tutorial adds a 3D product viewer to a FlutterFlow store app. Products are stored in Firestore with a model3dUrl field. A Custom Widget wrapping the model_viewer package renders .glb 3D files with built-in rotate, zoom, and auto-rotation. Color variant chips update both the 3D model URL and product images simultaneously. On Android and iOS devices with ARCore or ARKit support, an AR button places the product in the real world using model_viewer's built-in AR capabilities.

## Before you start

- FlutterFlow Pro account with Firebase Storage and Firestore enabled
- At least one .glb 3D model file (free models available at sketchfab.com or poly.pizza)
- Basic familiarity with FlutterFlow Custom Widgets
- A Firestore products collection with at least one product document

## Step-by-step guide

### 1. Set Up the Firestore Product Schema with 3D Model Fields

In Firestore, open your products collection (or create it) and add these fields to each product document: model3dUrl (String — Firebase Storage URL to the .glb file), variants (Array of Maps — each map has color (String), hexCode (String), model3dUrl (String for this color variant's model), imageUrls (Array of Strings for 2D photos of this variant)), isArEnabled (Boolean — whether AR is available), modelSizeKb (Integer — store the file size to warn users on mobile data). Upload your .glb files to Firebase Storage under a models/ folder. In FlutterFlow's Firestore panel, import the updated schema. Keep .glb files under 5MB — use tools like gltf-transform or Blender to compress models before uploading.

**Expected result:** Products in Firestore have model3dUrl fields pointing to .glb files in Firebase Storage. FlutterFlow shows the updated ProductsRecord Document Type.

### 2. Build the 3D Viewer Custom Widget

In FlutterFlow, go to Custom Widgets and create a widget named ProductViewer3D. In the pubspec dependencies, add model_viewer_plus: ^1.6.0. The widget accepts parameters: modelUrl (String — the Firebase Storage URL to the .glb file), autoRotate (Boolean, default true), backgroundColor (Color). The widget renders a ModelViewer widget from the model_viewer_plus package. Set the src property to the modelUrl, enable ar mode, set autoRotate and autoRotateDelay. The ModelViewer widget renders the .glb file using the device's WebView (Android) or Quick Look (iOS) with built-in touch controls for rotate, zoom, and pan. Place this Custom Widget in the product detail page's upper section with a fixed height of 350px.

```
// Custom Widget: ProductViewer3D
// pubspec: model_viewer_plus: ^1.6.0
import 'package:flutter/material.dart';
import 'package:model_viewer_plus/model_viewer_plus.dart';

class ProductViewer3D extends StatefulWidget {
  final String modelUrl;
  final bool autoRotate;
  final Color backgroundColor;
  final double width;
  final double height;

  const ProductViewer3D({
    Key? key,
    required this.modelUrl,
    this.autoRotate = true,
    this.backgroundColor = Colors.white,
    required this.width,
    required this.height,
  }) : super(key: key);

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

class _ProductViewer3DState extends State<ProductViewer3D> {
  @override
  Widget build(BuildContext context) {
    if (widget.modelUrl.isEmpty) {
      return Container(
        width: widget.width,
        height: widget.height,
        color: widget.backgroundColor,
        child: const Center(child: Text('No 3D model available')),
      );
    }
    return SizedBox(
      width: widget.width,
      height: widget.height,
      child: ModelViewer(
        src: widget.modelUrl,
        ar: true,
        arModes: const ['scene-viewer', 'webxr', 'quick-look'],
        autoRotate: widget.autoRotate,
        autoRotateDelay: 2000,
        cameraControls: true,
        backgroundColor: Color.fromARGB(
          widget.backgroundColor.alpha,
          widget.backgroundColor.red,
          widget.backgroundColor.green,
          widget.backgroundColor.blue,
        ),
        shadowIntensity: 1,
        shadowSoftness: 1,
      ),
    );
  }
}
```

**Expected result:** The product detail page shows the 3D model rotating automatically. Users can touch to rotate manually, pinch to zoom, and drag to pan.

### 3. Implement Color Variant Switching

On the product detail page, below the 3D viewer, add a Row of color chip buttons. Each chip is a GestureDetector wrapping a Container with the variant's hexCode as its background color and a checkmark overlay for the selected variant. Create a Page State variable selectedVariantIndex (Integer, default 0). Bind the ProductViewer3D widget's modelUrl parameter to the currently selected variant's model3dUrl: use a Custom Function named getVariantModelUrl that takes the variants list and selectedVariantIndex and returns the model3dUrl string. When a color chip is tapped, update selectedVariantIndex. The 3D viewer, main product image, and price will all update via their data bindings. If a variant does not have its own .glb model, fall back to the base product's model3dUrl.

**Expected result:** Tapping a color chip updates the 3D model to that variant's version (if available) and changes the product images to match.

### 4. Add AR Try-On Mode

The model_viewer_plus package's ar: true parameter automatically adds an AR button to the 3D viewer on supported devices. On Android, it uses Google Scene Viewer via the scene-viewer AR mode. On iOS, it uses Apple Quick Look via the quick-look AR mode. These are OS-level AR experiences — no custom AR code required. Add a separate View in AR button below the 3D viewer that is conditionally visible only when the product's isArEnabled field is true. When tapped, call a Custom Action named launchArViewer that opens the .glb URL directly using the launch URL action with the special Google Scene Viewer URL format for Android and the .usdz file URL for iOS. Note that AR works best for physical products with accurate real-world dimensions — set the scale attribute in ModelViewer to match actual product dimensions.

**Expected result:** On ARCore/ARKit-enabled devices, the AR button places the product in the camera view at real-world scale. The OS AR viewer opens natively.

### 5. Optimize 3D Models for Mobile Performance

Unoptimized 3D models are the most common cause of poor performance in 3D stores. Use Blender (free) or the gltf-transform CLI to compress .glb files before uploading. Target file sizes: under 2MB for mobile, under 5MB for tablet/desktop. In Blender, use Draco compression when exporting to .glb — this reduces file sizes by 60-80% with minimal visual quality loss. In FlutterFlow, show the model file size (from the modelSizeKb Firestore field) as a download notice if it exceeds 3MB: 'Large 3D model (4.2MB) — load anyway?' with Yes/No buttons. Cache the loaded model by keeping the ProductViewer3D widget mounted in the widget tree rather than rebuilding it on tab switch.

**Expected result:** 3D models load within 2-3 seconds on a 4G connection. Models under 2MB load without a warning prompt.

## Complete code example

File: `product_viewer_3d.dart`

```dart
// Full Custom Widget: ProductViewer3D for FlutterFlow
// pubspec.yaml dependencies:
//   model_viewer_plus: ^1.6.0

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

class ProductViewer3D extends StatefulWidget {
  final String modelUrl;
  final bool autoRotate;
  final Color backgroundColor;
  final bool arEnabled;
  final double width;
  final double height;
  final Future<dynamic> Function()? onArLaunched;

  const ProductViewer3D({
    Key? key,
    required this.modelUrl,
    this.autoRotate = true,
    this.backgroundColor = Colors.grey,
    this.arEnabled = false,
    required this.width,
    required this.height,
    this.onArLaunched,
  }) : super(key: key);

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

class _ProductViewer3DState extends State<ProductViewer3D> {
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    // Simulate model load time for UX feedback
    Future.delayed(const Duration(seconds: 3), () {
      if (mounted) setState(() => _isLoading = false);
    });
  }

  @override
  void didUpdateWidget(covariant ProductViewer3D oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.modelUrl != oldWidget.modelUrl) {
      setState(() => _isLoading = true);
      Future.delayed(const Duration(seconds: 3), () {
        if (mounted) setState(() => _isLoading = false);
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: widget.width,
      height: widget.height,
      child: Stack(
        children: [
          if (widget.modelUrl.isNotEmpty)
            ModelViewer(
              src: widget.modelUrl,
              ar: widget.arEnabled,
              arModes: const ['scene-viewer', 'webxr', 'quick-look'],
              autoRotate: widget.autoRotate,
              autoRotateDelay: 1500,
              cameraControls: true,
              shadowIntensity: 0.8,
              shadowSoftness: 0.8,
              exposure: '1.0',
              backgroundColor: Color.fromARGB(
                widget.backgroundColor.alpha,
                widget.backgroundColor.red,
                widget.backgroundColor.green,
                widget.backgroundColor.blue,
              ),
            )
          else
            Container(
              color: Colors.grey.shade200,
              child: const Center(
                child: Icon(Icons.view_in_ar_outlined, size: 64, color: Colors.grey),
              ),
            ),
          if (_isLoading)
            Container(
              color: Colors.black26,
              child: const Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    CircularProgressIndicator(color: Colors.white),
                    SizedBox(height: 12),
                    Text('Loading 3D model...', style: TextStyle(color: Colors.white)),
                  ],
                ),
              ),
            ),
        ],
      ),
    );
  }
}
```

## Common mistakes

- **Loading unoptimized 50MB+ 3D models directly from Firebase Storage** — A 50MB file takes 25-50 seconds to download on a typical mobile connection and often causes the WebView to crash or show a blank screen. It also incurs significant Firebase Storage egress costs. Fix: Compress all 3D models to under 5MB using Blender's Draco compression or the gltf-transform CLI before uploading. Store the file size in Firestore and warn users before loading large models.
- **Rebuilding the ProductViewer3D Custom Widget every time the parent page rebuilds** — Each rebuild discards the loaded 3D model and restarts the download and render process, causing a blank screen flicker and reloading the entire model file. Fix: Wrap the ProductViewer3D widget in a FlutterFlow Component and keep it mounted. Use the modelUrl parameter change (via didUpdateWidget) to detect model switches rather than rebuilding the widget from scratch.
- **Assuming AR mode works on all devices without checking hardware support** — AR requires ARCore (Android) or ARKit (iOS). Many budget Android phones and older iPhones do not support these frameworks. Showing an AR button on unsupported devices leads to confusing errors. Fix: Set isArEnabled per product in Firestore and conditionally show the AR button. Add a Custom Function that checks the device's platform version and shows a 'AR not available on this device' message when appropriate.

## Best practices

- Compress all .glb files to under 5MB before uploading — use Blender with Draco compression or gltf-transform.
- Show a loading indicator for the first 3 seconds while the model initializes in the WebView.
- Store modelSizeKb in Firestore and warn users before loading models over 10MB on mobile data.
- Validate .glb files with the Khronos glTF Validator before uploading to catch rendering issues early.
- Provide 2D image fallbacks for products that do not yet have 3D models using Conditional Visibility.
- Set real-world scale dimensions in ModelViewer using the scale attribute so AR mode places objects at the correct size.
- Cache the page state after loading a model so navigating back to a product does not re-download the .glb file.

## Frequently asked questions

### Which 3D file formats does model_viewer_plus support?

model_viewer_plus supports .glb and .gltf files. For iOS AR mode (Quick Look), you also need a .usdz version of the model. Use Blender or Reality Converter (macOS) to convert .glb to .usdz. Store both URLs in Firestore and serve the appropriate format based on the device platform.

### Where can I get free 3D models for testing?

Sketchfab (sketchfab.com) offers thousands of free downloadable .glb models. Poly Pizza (poly.pizza) has simple low-poly models ideal for mobile. Google's model-viewer examples repo on GitHub includes optimized sample .glb files. Always check the license before using models in a commercial app.

### Does the 3D viewer work in FlutterFlow web apps?

model_viewer_plus works on Flutter Web by using the model-viewer web component from Google. You need to add the model-viewer script tag to your web/index.html file. AR mode does not work in web browsers — it requires native iOS or Android app installation.

### How do I add lighting and environment maps to the 3D viewer?

ModelViewer supports an environmentImage property that accepts an HDR image URL for lighting. Set skyboxImage for a visible 360-degree background, or set environmentImage with neutralBackground for product-style lighting. High-quality .hdr environment maps are available free at Poly Haven (polyhaven.com).

### Can I let users take screenshots of the 3D model?

model_viewer_plus does not expose a screenshot API directly. Use Flutter's RepaintBoundary widget wrapped around the ModelViewer and call toImage() to capture the current frame. Alternatively, use the screenshot package to capture the widget area. Note that capturing WebView content may have limitations on some Android versions.

### How do I handle devices where the 3D viewer crashes or shows a blank screen?

Wrap the ProductViewer3D widget in a try-catch error boundary using FlutterFlow's Custom Widget error handling. Provide a fallback Image widget that shows the 2D product photos when the 3D viewer fails. Track crash rates by logging failures to Firestore or Firebase Crashlytics.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-a-virtual-store-with-a-3d-product-viewer-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-a-virtual-store-with-a-3d-product-viewer-in-flutterflow
